Автоматическая публикация новой версии библиотеки с использованием TFS 2010 и NuGetter

  • Tutorial

Дано


  1. Одна маленькая, но очень полезная библиотека. Включает в себя общую функциональность — логирование, работа с Windows Azure, и т.д.
  2. Большое количество проектов(solutions), где используется данная библиотека.
  3. Распределённая команда разработчиков, часть которой библиотеку пишет и поддерживает, а другая часть только пользуется.

Проблемы, которые хочется решить

  1. Необходимость копировать из проекта в проект исходники/бинарники — неудобно, долго, велика вероятность ошибки при обновлении.
  2. Невозможность использования разных версий для разных проектов — поиск и сборка конкретной версии «из прошлого» неудобны, опять же велика вероятность ошибки при обновлении.
  3. Необходимость следить за актуальностью зависимостей библиотеки — особенно это касается Azure SDK, который сейчас регулярно обновляется, не всегда у всех разработчиков стоит последняя версия, и обновление SDK не всегда возможно.
  4. Использование существующего проекта на разных машинах — ещё одно «тонкое» место, порождающее много ненужных ошибок. Для корректной работы необходимо полное совпадение путей для проектов, чего очень сложно добиться.

Способ решения и возникшие проблемы

Сразу стало понятно, что получение пакета с последней/конкретной версией библиотеки при сборке какого-либо решения проще всего сделать через NuGet — работа с Azure SDK располагает к подобному подходу.

Однако после развёртывания репозитория выяснилось, что просто так публикацию в NuGet настроить не получится — пришлось писать отдельный проект, который бы собирал пакет для публикации и заливал бы его в хранилище.
Так же выяснилось, что номер версии в файле AssemblyInfo не очень удобен — нумерация по умолчанию не включает в себя дату публикации, что несколько затрудняет решение проблем, появляющихся после обновления библиотеки (не всегда легко можно отловить, когда именно перестала работать та или иная часть функционала).

В итоге было решено перенести процесс публикации на TFS server, добавив Build Definition(билд) для библиотеки. Все указанные действия производились в Team Explorer VS 2010, но особых различий при переходе на VS 2012 я не заметил.

Поиск лучшего

В качестве вспомогательного проекта был выбран NuGetter (для автоматической публикации в репозиторий во время билда на сервере) с добавлением TFSVersioning (для редактирования файла AssemblyInfo во время билда на сервере), автор у проектов один, и проблем с интеграцией не должно было возникнуть.

Пошаговое описание личного опыта внедрения

Хотел бы сразу отметить, что всё написанное ниже — следование документации из обоих проектов, плюс описание некоторых side-эффектов, с которыми я столкнулся.
Раньше настраивать билд лично мне не приходилось, так что все шаги будут описаны достаточно подробно и занудно.

0. Подготовка к настройке — заливка на сервер.

Оба проекта содержат библиотеки с определёнными Custom Actions для рабочего процесса билда на сервере и xaml-файлы с шаблонами рабочего процесса (Workflow), расширяющего возможности билда по умолчанию. Всё это требуется залить на сервер: шаблоны проектов для возможности выбрать их при создании нового билда, а библиотеки — для возможности их найти при очередном проведении этого самого билда.

Шаблоны рабочих процессов рекомендуется выложить в папку для хранения шаблонов по умолчанию $/(Solution Name)/BuildProcessTemplates.
Библиотеки и файл NuGet.exe с его настройками рекомендуется положить в отдельную папку (на нашем сервере она имеет очень оригинальное название .nuget — зато всегда вверху, что удобно при настройке билда).

Для того чтобы данные библиотеки были найдены в процессе билда, требуется настроить контроллер билдов (Build Controller) для данного решения. Делается это так:
Team Explorer --> (Solution Name) --> Builds --> правый клик --> Manage Build Controllers...


Выбор контроллера:


Редактирование контроллера для (Solution Name):


В поле Version control path to custom assemblies требуется указать путь к вашей папке с общими библиотеками.

Здесь у меня возникла первая маленькая проблема — я пытался редактировать свойства не контроллера, а одного из его агентов сборки. Будьте внимательны.
На всякий случай проверьте подключение к папке (кнопка Test Connection).
После проверки подключения сохраните изменения.

1. Добавление нового билда на основе шаблона.

Теперь нужно добавить новый билд по шаблону, который был загружен в предыдущем шаге. Делается это так:
Team Explorer --> (Solution Name) --> Builds --> правый клик --> New Build Definition...


Название и описание билда:


Расписание для запуска билда:

Варианты:
  • Запуск только вручную.
  • После каждого check-in от разработчиков.
  • Ждать завершения предыдущего билда.
  • Собирая check-in, с возможностью поставить время запуска «не чаще чем N минут».
  • Принимать check-in только если был удачный merge и сборка на сервере после этого была успешной.
  • По расписанию.

Код, который должен быть собран во время билда:

Иногда здесь присутствует слишком много проектов — удалите лишние. Так же можно указать какой проект в какую папку должен быть скопирован во время билда.

Определение параметров по умолчанию для билда:

Необходимо выбрать:
  • Контроллер, который был настроен в предыдущем пункте.
  • Куда нужно поместить код:
    • Не копировать результат сборки никуда.
    • Скопировать результат сборки в общую папку
      Требуется корректный UNC-адрес вида \\server\share.
    • Скопировать результат сборки в папку TFS-сервера
      Вариант не всегда доступен, в зависимости от прав пользователя, создающего билд.

Настройки билда:

Первые три пункта — это настройки, доступные всегда и для любого билда. Сейчас для нас представляет интерес только первый блок — выбор проектов или решений для сборки (Projects to Build).

Так же стоит обратить внимание на Build number format — именно этот параметр отвечает за имя папки с результатом билда (для проектов с большой вложенностью папок должен быть не очень длинным).

Настройки сохранения результатов билдов:


2. Выбор шаблона для билда.

На вкладке Process в верхней части в выпадающем списке нужно выбрать требуемый шаблон. Если в выпадающем списке не видно нужного, значит, он будет использоваться в первый раз, и его необходимо «показать» серверу с помощью кнопки New... (выбрать или скопировать уже загруженный на TFS-сервер файл). Пудинг, это — Алиса, Алиса, это — пудинг.

Выбор шаблона:


Варианты:
  • [Все установленные на TFS-сервере шаблоны билдов].
  • VersioningBuildTemplate.xaml — базовый шаблон для замены версий в файле AssemblyInfo.cs.
  • VersioningBuildTemplate15.xaml — шаблон для замены в файле AssemblyInfo.cs — дополнительные возможности редактирования свойств библиотеки, доступные в версии TFSVersioning 1.5.
  • NuGetterStandardBuildTemplate.xaml — базовый шаблон для публикации результатов билда в NuGet.
  • NuGetterVersioningBuildTemplate.xaml — базовый шаблон для публикации результатов билда в NuGet и замены версий в файле AssemblyInfo.cs.
  • NuGetterVersioningBuildTemplate15.xaml — шаблон для публикации результатов билда в NuGet и замены в файле AssemblyInfo.cs с дополнительными возможностями из версии TFSVersioning 1.5.


3. Настройка замен в файле AssemblyInfo.cs.

Если был выбран шаблон с участием TFSVersioning, в настройках билда появится
пункт № 4:

Что и для чего нужно:
  • Первые две строки отвечают за шаблоны номеров версий для AssemblyFileVersion и AssemblyVersion.
  • Правила замены для шаблона номеров версий:
    • Номер, встречаемый в любом месте шаблона, остаётся неизменным.
    • B заменяется на номер билда в рамках одного дня.
    • YYYY заменяются на 4-значное представление текущего года.
    • YY заменяются на 2-значное представление текущего года (две последние цифры).
    • MM или M заменяются на номер текущего месяца (при замене MM ноль впереди не ставится).
    • DD или D заменяются на номер текущего дня в месяце (при замене DD ноль впереди не ставится).
    • J заменяется на дату в формате YYDDD, где DDD — порядковый номер дня с начала года.

    Предлагаемый шаблон по умолчанию — 1.0.J.B — обеспечивает практически уникальный номер версии для библиотеки. Сложности начнутся через 100 лет при условии сохранения мажорной и минорной версии проекта. Legacy-код наносит ответный удар.
  • Третья строка — это маска для поиска файлов AssemblyInfo в собираемых проектах. Обычно редактирования не требует.
  • Четвёртая строка — это число, которое будет прибавлено к номеру B. Может применяться в случае настройки нескольких билдов для одного проекта — к одному прибавляем 100, к другому — 200, все счастливы. Максимальное значение для суммы номера билда и префикса — 65535. Автор проектов так же просит связаться с ним, если вы производите более 999 билдов в день, вы же явно чем-то интересным заняты.
  • Пятая строка указывает, нужно ли создавать аттрибуты AssemblyFileVersion и AssemblyVersion при их отсутствии.
  • Шестая строка указывает, нужно ли заливать в хранилище данных изменённые файлы AssemblyInfo.

Если вы собираете более чем один проект, или вы по результатам билда создаёте ещё и NuGet-пакет, шаблоны для номеров версий можно вынести в отдельный XML-файл, залить его на сервер и включить его использование в седьмой строке, указав путь к нему в восьмой.
XML выглядит так:
<?xml version="1.0" encoding="utf-8" ?>
<VersionSeed>
    <Solution name="Default">
        <AssemblyVersionPattern>1.8.j.b</AssemblyVersionPattern>
        <AssemblyFileVersionPattern>1.8.j.b</AssemblyFileVersionPattern>
    </Solution>
    <NuGetPackage id="ServiceLib">
        <VersionPattern>1.8.j.b</VersionPattern>
    </NuGetPackage>
</VersionSeed>


Если вы используете шаблон билда для TFSVersioning 1.5, вам так же будет доступна
вкладка с дополнительными возможностями автозамен в файле AssemblyInfo:


4. Настройка публикации в NuGet.

При использовании шаблона для NuGetter доступны ещё три вкладки.
NuGetter (A) – Pre-Packaging:

Первая из вкладок отвечает за подготовку собранного проекта к запаковке и публикации. При необходимости вы можете добавить на сервер свой PowerShell-скрипт (для создания папок, указанных в NuGet-спецификации, например).

Будьте внимательны: исполняемые PowerShell-скрипты могут быть запрещены к исполнению на сервере. В этом случае билд не выдаст ошибку, а просто попробует запустить NuGet так, как если бы скрипт отработал верно.
Политику относительно выполнения PowerShell-скриптов можно проверить и исправить через команды
Get-ExecutionPolicy
Set-ExecutionPolicy

NuGetter (B) – Package:

Что и для чего нужно:
  • Первая строка — какие-либо дополнительные параметры для запуска NuGet-файла.
  • Вторая строка — название базовой папки для выполнения команд NuGet при запаковке и публикации.
  • Третья строка — путь к файлу NuGet на сервере (файл должен находиться в хранилище или в папках, определённых в PATH в Windows на сервере).
  • Четвёртая строка — путь к файлу NuGet-спецификации на сервере (файл должен находиться в хранилище).
  • Пятая строка — название папки, в которой будет создан пакет (если сборка пройдёт удачно).
  • Шестая строка — адрес файла с шаблонами версий для проекта — как уже упоминалось выше, можно вынести в отдельный XML-файл все номера версий для проектов и пакетов, и не изменять их постоянно в свойствах билда.

У меня не получилось настроить рабочий процесс так, чтобы файл NuGet вызывал обновление самого себя перед выполнением — пришлось просто залить на сервер новую версию NuGet.exe. Думаю, нужно создавать свой шаблон на основе предоставленных, где добавлять Custom Action с ещё одним вызовом NuGet. Хотя, возможно, я изобретаю велосипед, и всё делается гораздо проще.

NuGetter ( C ) – Push and Publish:

Что и для чего нужно:
  • Первая строка — API key, если он определён для NuGet-репозитория, в который идёт публикация пакета. Автор ожидает в качестве ключа GUID в записи aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee, если указанный ключ не проходит ReGex-проверку, строка считается относительным путём к файлу, в котором указан API key.
  • Вторая строка указывает, нужно ли просто создать пакет и скопировать его в репозиторий или требуется публикация на удалённый сервер. Фактически при включённом флаге добавляет параметр -co к параметрам запуска NuGet, чем рушит весь билд, так как в версии 2.1 параметр не распознаётся.
  • Третья строка указывает, нужно ли предпринимать попытку публикации пакета в репозиторий после его создания.
  • Четвёртая строка — адрес NuGet-репозитория для публикации.
    Возможные значения:
    • URL удалённого репозитория.
    • UNC-имя папки в локальной сети.
    • Локальный адрес папки на сервере, где запускается билд.

У нас используется стандартный NuGet Server, без каких-либо дополнений.

Для успешной публикации в NuGet-репозиторий обязательно нужны права на запись в папку packages для пользователя, под которым запущен Application Pool. Памяти на сервере тоже должно хватать, так как ошибка при нехватке места на диске в EventLog очень странная и абсолютно неинформативная.

5. Запуск билда.

Кроме автоматического запуска билда согласно установкам из пункта 1, билд можно запустить руками.
Team Explorer --> (Solution Name) --> Builds --> Билд для запуска --> правый клик --> Queue New Build ...


Таблица запущенных билдов с текущим статусом:


6. Итого.

Задача решилась, причём гораздо быстрее, чем была написана эта статья. Более того, после ознакомления с техникой создания билдов на TFS, оказалось, что настраивать Continious Integration не так уж и сложно. Важно помнить, что сборка билда происходит на сервере, поэтому настройки проекта, файлы и другие изменения нужно не забывать заливать в хранилище.

При неудачной сборке TFS автоматически создаёт баг, со срочностью Critical, и вешает его на разработчика, залившего код на сервер последним. После «починки» баг назначается на Network Service, так что именно этому пользователю нужно выдавать права на запись.

Надеюсь, статья сэкономит кому-то время и нервы.
Спасибо за внимание.
Поделиться публикацией

Комментарии 22

    0
    А теперь расскажите, пожалуйста, как вы, собственно, решаете следующие проблемы:

    1. Необходимость копировать из проекта в проект исходники/бинарники — неудобно, долго, велика вероятность ошибки при обновлении.
    2. Невозможность использования разных версий для разных проектов — поиск и сборка конкретной версии «из прошлого» неудобны, опять же велика вероятность ошибки при обновлении.
    3. Необходимость следить за актуальностью зависимостей библиотеки — особенно это касается Azure SDK, который сейчас регулярно обновляется, не всегда у всех разработчиков стоит последняя версия, и обновление SDK не всегда возможно.
    4. Использование существующего проекта на разных машинах — ещё одно «тонкое» место, порождающее много ненужных ошибок. Для корректной работы необходимо полное совпадение путей для проектов, чего очень сложно добиться.



    А то я весь пост вижу только разговор про то, как автоматически опубликовать nuget-package.

    Особенно мне интересно, конечно, как вы решаете проблему «библиотека хочет одну версию зависимостей, проект хочет другую версию зависимостей».
      0
      1. Подключаем пакет через NuGet-менеджер — обращаемся к своему NuGet-репозиторию, получаем последнюю версию проекта во время сборки.
      2. Через NuGet-менеджер можно запросить конкретную версию пакета, и производить сборку именно через него.
      3. В пакет закладывается не только наша библиотека, но и все её зависимости (те же азуровские библиотеки).
      4. Библиотеки при получении через NuGet во время сборки корректно находятся компилятором.

      Собственно, после подключения к своему NuGet-фиду проблемы были решены (по крайней мере, у нас).

      Пожалуйста, уточните ваш последний вопрос — я его не очень понял.
        0
        Подключаем пакет через NuGet-менеджер — обращаемся к своему NuGet-репозиторию, получаем последнюю версию проекта во время сборки.

        Вот прямо из коробки? Без каких-либо дополнительных настроек, особенно на TFS?

        Через NuGet-менеджер можно запросить конкретную версию пакета, и производить сборку именно через него.

        … а что будет, если разные проекты в солюшне используют разные версии пакета?

        В пакет закладывается не только наша библиотека, но и все её зависимости (те же азуровские библиотеки).

        А что будет, если разные версии пакета смотрят на разные версии зависимостей?

        Библиотеки при получении через NuGet во время сборки корректно находятся компилятором.

        Ну вообще нет. Компилятор про nuget ничего не знает.

        Пожалуйста, уточните ваш последний вопрос — я его не очень понял.

        Очень просто.

        У вас есть ваш собственный проект, он смотрит на, скажем, Entity Framework 4. У вас есть подключенный nuget-package, который смотрит на Entity Framework 5. Что получится?
          0
          Вот прямо из коробки? Без каких-либо дополнительных настроек, особенно на TFS?

          Не очень понятно, при чём тут TFS — или вы имеете ввиду VS на машине разработчика?
          Там, конечно, должен стоять расширение NuGet, для корректной работы с пакетами.

          … а что будет, если разные проекты в солюшне используют разные версии пакета?

          Так как в разных проектах прописаны разные NuGet-targets, проекты и получат разные пакеты (у нас сейчас в solution есть около 10 версий, всё корректно собирается).

          А что будет, если разные версии пакета смотрят на разные версии зависимостей?

          Всё зависит от того, на какой пакет настроен ваш проект, и включено ли у вас обновление пакетов при сборке — если да, то при обновлении библиотек могут возникнуть ошибки сборки, да.

          Ну вообще нет. Компилятор про nuget ничего не знает.

          Согласен, неверно сформулировал. Я имел ввиду, что при сборке проекта NuGet-расширение исследует файл *.targets на предмет всех нужных пакетов, и если какие-то не получены, и включена их подгрузка — оно их загружает/обновляет, после чего сборка проекта идёт дальше.

          У вас есть ваш собственный проект, он смотрит на, скажем, Entity Framework 4. У вас есть подключенный nuget-package, который смотрит на Entity Framework 5. Что получится?


          Для решения таких случаев можно, например, в PowerShell скрипт добавить код прописывания bindingRedirect, например:
          <dependentAssembly>
              <assemblyIdentity name="EntityFramework" publicKeyToken="b77a5c561934e089" culture="neutral" />
              <bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
          </dependentAssembly>
          

          Данный код из web.config взят с реального проекта. Такой bindingRedirect прописывается пакетами Azure SDK. Разумеется, данный подход так же может породить ошибки компиляции, так как в новой EntityFramework может не быть какого-то либо функционала из прошлых версий.

          Что будет без подобного редиректа — не проверял, но, видимо, будет следующее:
          Код проекта, пытающийся вызвать функционал из пакета, завязанный на Entity Framework, породит ошибку компиляции.
            0
            Не очень понятно, при чём тут TFS — или вы имеете ввиду VS на машине разработчика?

            При том, что проекты собираются не только локально.

            Так как в разных проектах прописаны разные NuGet-targets, проекты и получат разные пакеты (у нас сейчас в solution есть около 10 версий, всё корректно собирается).

            И не попадают в общий bin при результирующей сборке?

            Я имел ввиду, что при сборке проекта NuGet-расширение исследует файл *.targets на предмет всех нужных пакетов, и если какие-то не получены, и включена их подгрузка — оно их загружает/обновляет, после чего сборка проекта идёт дальше.

            … и это тоже не из коробки.

            Разумеется, данный подход так же может породить ошибки компиляции, так как в новой EntityFramework может не быть какого-то либо функционала из прошлых версий.

            Именно. Нарушение обратной совместимости в полный рост.

            Код проекта, пытающийся вызвать функционал из пакета, завязанный на Entity Framework, породит ошибку компиляции.

            Именно.

            Что, в общем-то, означает, что nuget-пакеты не решают проблемы реальной работы с разными версиями одной зависимости. Проверено на практике.
              0
              Прошу прощения, вклинюсь.

              Я обычно для этих целей использую субрепозитории в Mercurial или Git.
              Каждая подключаемая библиотека вервионируется в своем репо, который подключается к проекту- потребителю.
              Смена версии библиотеки, при этом, это отдельный коммит с тестированием.
                0
                Конечно, для TFS это не применимо.
                Вот как раз и было интересно решение автора…
                Но все равно, спасибо за статью.
                  0
                  Что не применимо?
                    0
                    Субрепозитории. У меня чего-то комменты самоотравляются…
                      0
                      В TFS такие вещи спокойно делаются через отдельные папки и бранчи. В чем проблема-то?
                    0
                    Пожалуйста.
                    В документации к NuGetter есть пример создания пакета из двух решений с разными версиями .NET Framework.
                    Там библиотеки рассовываются в разные папки в одном пакете.

                    Соответственно, можно запаковать и старую, и новую версии.
                      0
                      Вы, похоже, вообще не понимаете, о чем говорите.

                      Разные версии .net framework в nuget-пакете — это разные референсы для проектов с разной целевой версией .net. При этом если у вас будет два таких (разных) проекта в одном солюшне и они, не дай б-г, будут потом пореференсены из одного проекта — у вас будет все тот же самый цирк с невозможностью разобрать зависимости.
                        0
                        А насколько часто лично вам приходилось встречаться с тем, что в рамках одного solution встречались проекты, работающие с разными версиями одной и той же библиотеки?
                          0
                          За последнюю неделю — трижды. И это, что характерно, был нюгетовский пакет.
                            0
                            Надо признаться, вы меня сильно задели, указав на неполное решение задачи.
                            Долго думал, обращался к первоисточникам, и хотел бы доформулировать свою мысль насчёт разных папок для библиотек пакета.

                            1. Согласно известной книге Рихтера, глава 2, для каждой сборки в конфигурационном файле приложения можно указать относительный PrivatePath, где CLR будет искать файлы, относящиеся к данной сборке:
                            <configuration>
                              <runtime>
                                <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
                                  <probing privatePath="AuxFiles" />
                                </assemblyBinding>
                              </runtime>
                            </configuration>
                            


                            2. Прописать данную настройку в .config файле можно с помощью PowerShell скрипта, идущего вместе с package.

                            3. При сборке пакета можно запаковать библиотеки именно в указанную Private Path.

                            Соответственно, во время сборки солюшена с разными версиями пакета зависимости будут храниться в отдельных папках, и друг друга не перетрут.

                            Единственная пока проблема — не смог найти поддержку работы с Private Path в документации NuGet. Спросил в их блоге, посмотрим, что ответят. Если будет работать, обновлю статью.
                              0
                              Согласно известной книге Рихтера, глава 2, для каждой сборки в конфигурационном файле приложения можно указать относительный PrivatePath, где CLR будет искать файлы, относящиеся к данной сборке:

                              Нельзя. Это общий путь приложения, а не каждой сборки. Это даже из примера видно — там сама сборка нигде не указывается.
                                0
                                Да, не всё так радужно, как показалось.
                                Согласно MSDN, в параметр Private Path можно указывать не одну папку, а несколько.
                                Правда, не уверен, что всё это будет корректно собираться, буду пробовать.
                                  0
                                  Согласно MSDN, в параметр Private Path можно указывать не одну папку, а несколько.

                                  Можно. Будет искаться в нескольких папках. Найдется все равно конкретная версия, а дальше будут конфликты.
                    0
                    При том, что проекты собираются не только локально.

                    Я не настраивал билды проектов, кроме «общей» библиотеки, так что опыт не очень большой — но никаких особых действий для включения поддержки NuGet на TFS-сервере не производилось.
                    Библиотека собралась со всеми пакетными зависимостями.

                    И не попадают в общий bin при результирующей сборке?

                    Здесь наврал, извините. В рамках одного solution — одна версия пакета.

                    … и это тоже не из коробки.

                    Да, подразумевается, что на машине разработчика стоит NuGet-расширение — работа с Azure намекает на необходимость данного условия.

                    Именно. Нарушение обратной совместимости в полный рост.

                    Да, выбор сделан не в пользу поддержки старых версий — по примеру Azure SDK.

                    Что, в общем-то, означает, что nuget-пакеты не решают проблемы реальной работы с разными версиями одной зависимости. Проверено на практике.

                    Спасибо за пищу для размышлений. У нас ситуация пока проще, чем вы описали.
                    Может быть, есть что-либо почитать на эту тему?
                      0
                      Я не настраивал билды проектов, кроме «общей» библиотеки, так что опыт не очень большой

                      Ну то есть вы, на самом деле, не решали проблему работы с зависимостями в разных проектах.

                      Здесь наврал, извините. В рамках одного solution — одна версия пакета.

                      А это значит, что вы не решали проблему работы с разными версиями.

                      Да, подразумевается, что на машине разработчика стоит NuGet-расширение — работа с Azure намекает на необходимость данного условия.

                      Я снова про TFS, если что.

                      Спасибо за пищу для размышлений. У нас ситуация пока проще, чем вы описали.

                      … но при этом в начале статьи-то вы замахиваетесь на больше, чем у вас сейчас есть. О чем я и говорил.

                      Может быть, есть что-либо почитать на эту тему?

                      Если бы было, я бы вам дал ссылку сразу.
                        0
                        В следующий раз буду более аккуратен в выражениях, спасибо.
            0
            Промахнулся.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое