В [предыдущей части](https://www.mql5.com/ru/articles/20150) мы изменили способ получения информации о терминалах: теперь в каждом экземпляре работает сервис на MQL5, который записывает состояние терминала и торгового счёта в общую базу SQLite, а веб-приложение читает из неё последние данные. Это позволило отказаться от последовательного подключения к терминалам через библиотеку `MetaTrader5` для Python, причём карточки на странице менеджера продолжили работать практически без изменений.
Однако сами экземпляры мы пока подготавливали вручную. Создание нового терминала требуется не так уж часто: обычно после первоначальной настройки мы большую часть времени просто наблюдаем за уже работающими экземплярами. Поэтому автоматизация этой операции вряд ли даст заметную экономию усилий, но, поскольку терминалы предполагается размещать на удалённом сервере, было бы удобно и эту работу выполнять через веб-интерфейс, не подключаясь к серверу через удалённый рабочий стол. Особенно если в дальнейшем мы будем использовать несколько серверов и объединим управление всеми экземплярами в одном окне браузера.
Поэтому в этой части займёмся созданием новых экземпляров терминала, то есть отдельных копий MetaTrader 5, каждая из которых размещается в своей папке и имеет собственные настройки и программы. Добавим страницу настроек, через которую можно передать необходимые файлы на сервер, научимся подготавливать папку для новой копии и запускать в ней терминал вместе с нашим сервисом. Заодно разберёмся, как при первом подключении сохранить параметры торгового счёта, чтобы при последующих запусках терминал самостоятельно подключался к тому же счёту, не требуя повторного ввода пароля.
Историю показателей, о которой мы говорили в заключении прошлой части, пока отложим. Сейчас нам хотелось бы получить законченный путь от нажатия кнопки «Создать» до работающего терминала, состояние которого уже видно на странице менеджера.
Для начала посмотрим, чем отличается обычный запуск от создания нового экземпляра терминала. В первом случае у нас уже есть папка терминала, его конфигурация и, возможно, сохранённые параметры счёта. Во втором случае всего этого ещё нет, поэтому сначала нужно получить файлы программы, выбрать место для новой копии и подготовить сервис.
Счёт при этом тоже может появиться в разное время. Если мы подключаем уже существующий счёт, его реквизиты можно сразу указать в настройках менеджера, и новый экземпляр должен самостоятельно подключиться к нему. Но иногда удобнее сначала создать терминал, а уже затем открыть в нём демо-счёт. В таком случае оставим реквизиты пустыми и позволим задать их позже.
Основным списком экземпляров остаётся `config.json`. База SQLite по-прежнему нужна для получения состояния: наличие записи в ней само по себе не означает, что менеджер должен считать терминал добавленным. В конфигурации будем сохранять базовую папку терминалов и имена экземпляров, а также реквизиты счёта, когда пользователь решит их указать.
Количество устанавливаемых терминалов ограничивать не будем. Производительность процессора и объём оперативной памяти на разных компьютерах и серверах могут заметно различаться, поэтому подходящее количество одновременно работающих терминалов пользователь сможет определить исходя из своих условий. А вот свободное место перед копированием проверить стоит: если места не хватает даже на исходные файлы, операция не имеет смысла, а если после копирования остаётся небольшой запас, покажем предупреждение. После начала работы терминал будет загружать значительный объём исторических данных, которые со временем будут пополняться, поэтому занимаемое им место на диске существенно увеличится по сравнению с размером двух исходных EXE-файлов.
Первый вариант, который приходит в голову, — скачать официальный дистрибутив и каждый раз запускать установщик. Тогда пришлось бы управлять его диалогами, выбирать папку, отслеживать завершение установки и разбираться с дополнительными действиями, которые для нашего сценария не обязательны.
Можно поступить проще. Возьмём из уже установленного MetaTrader 5 два файла, `terminal64.exe` и `MetaEditor64.exe`, и скопируем их в новую папку. Затем запустим терминал так:
Параметр `/portable` здесь существенен. В таком режиме данные хранятся в самой папке установленной копии. Для этого пользователь Windows, от которого запущен процесс, должен иметь право записи в выбранную папку. Подробности описаны в [справке по запуску платформы](https://www.metatrader5.com/ru/terminal/help/start_advanced/start).
В качестве родительской папки для всех экземпляров терминала по-прежнему используем `C:\MT5`, а в настройках менеджера сохраним её как `mt5_folder`. Все экземпляры будем размещать непосредственно внутри неё, каждый в своей подпапке. Если впоследствии понадобится перенести терминалы, можно будет переместить их в другую базовую папку и указать её в настройках. Само изменение настройки папки на диске не перемещает.
Сам проект менеджера можно, как и раньше, разместить в папке `MQL5/Shared Projects` одного из терминалов, но теперь это не обязательное условие: репозиторий можно клонировать и в отдельную папку на сервере. Веб-приложение запускается на том же сервере, где будут работать терминалы, а со своего компьютера пользователь обращается к нему через браузер. В наших примерах обе роли выполняет один компьютер.
При отдельном размещении проекта можно указать в настройках уже имеющуюся установку MT5, из которой менеджер возьмёт необходимые файлы. Если на сервере ещё нет ни одного терминала, сначала запустим веб-приложение, затем через браузер зададим базовую папку и загрузим со своего компьютера `terminal64.exe` и `MetaEditor64.exe`. Понадобится также подготовленный `servers.dat`, о котором поговорим ниже, а сервис можно будет загрузить в виде готового EX5 или скомпилировать из исходного кода проекта.
С созданием папки мы разобрались, однако первый опыт с подключением к счёту другого брокера оказался менее успешным. Для проверки мы создали небольшой скрипт на Python, в котором разместили вызов функции `mt5.initialize()`, передав ей в качестве параметров путь к исполняемому файлу новой копии терминала, логин, пароль и имя торгового сервера. После запуска скрипта терминал открылся, начал первоначальную подготовку и показывал номер счёта в заголовке, но подключения не было. Вызов `mt5.initialize()` завершился с сообщением:
Согласно [справочнику ошибок Python API](https://www.mql5.com/ru/docs/python_metatrader5/mt5lasterror_py), это тайм-аут внутреннего взаимодействия с терминалом. Для ошибки авторизации предусмотрен другой код, поэтому по одному этому сообщению нельзя было заключить, что неверен пароль. Кроме того, при первом запуске терминал создаёт в папке данных стандартные примеры советников, индикаторов и других MQL5-программ, после чего автоматически запускается их компиляция. Эта подготовка занимает некоторое время и заметно нагружает компьютер, поэтому сначала было неясно, связана ли задержка подключения с компиляцией примеров или у неё есть другая причина.
Терминал, который мы использовали, был взят с сайта MetaQuotes, и изначально в его списке был только сервер MetaQuotes. Поэтому во время ожидания мы открыли диалог создания счёта и выполнили поиск нужной брокерской компании, чтобы добавить сведения о её торговых серверах. Сам порядок поиска описан в [справке по открытию счетов](https://www.metatrader5.com/ru/terminal/help/startworking/acc_open). После того как компания появилась в списке, её серверы стали доступны для выбора при подключении к существующему счёту. Вскоре терминал подключился, и повторный вызов Python-скрипта завершился успешно, однако оставалось неясно, помог ли поиск брокера или просто прошло достаточно времени.
Чтобы проверить, не было ли дело только во времени ожидания, мы повторили опыт с чистой копией терминала и на этот раз ждали длительное время, не выполняя поиск брокера. Однако самостоятельно найти нужный сервер и подключиться к нему терминал так и не смог. Поэтому в нашем случае предварительное заполнение списка серверов оказалось обязательным. В связи с этим возник вопрос: можно ли сделать это другим способом, чтобы не искать брокера вручную в каждом новом терминале?
Оказалось, что сведения для подключения можно перенести в другую копию файлом `config/servers.dat`. В [описании структуры каталогов](https://www.metatrader5.com/ru/terminal/help/start_advanced/structure) он как раз указан как файл настроек торговых серверов. Здесь важно не перепутать расположение: это каталог `config` в папке данных терминала, а не подкаталог внутри `MQL5`. В используемом нами portable-режиме папка данных совпадает с папкой установки.
Для следующего опыта мы снова подготовили чистую папку, скопировали два EXE и заранее создали каталог `config`, положив в него обновлённый `servers.dat` из исходного терминала. Предварительно запускать новую копию для создания каталогов не стали. После этого та же Python-авторизация прошла примерно за пять секунд, без поиска компании в новой копии.
Сам файл `servers.dat` не является текстовым списком, в который можно дописать ещё одну строку. Открытого описания его структуры нам найти не удалось. В исследованном файле читался заголовок, но названия известных серверов обычным поиском строк не обнаруживались. Этого недостаточно, чтобы установить конкретный способ кодирования или шифрования, зато достаточно, чтобы пока отказаться от его самостоятельного формирования и просто переносить готовый файл целиком.
Успешное подключение через Python ещё не означало, что следующий запуск получится выполнить без него. Мы закрыли терминал и запустили с одним параметром `/portable`, после чего он снова попросил пароль. Логин и сервер сохранились, а пароль — нет.
В справке по конфигурационному файлу есть параметр `KeepPrivate`, который управляет сохранением пароля между подключениями. Поэтому сначала мы попробовали заранее записать нужное значение в `config/common.ini`:
Затем повторили Python-авторизацию на новой чистой копии, штатно закрыли терминал и запустили его обычным способом. В этом варианте он опять показал окно ввода пароля, хотя настройка оставалась в файле. Значит, надеяться, что добавление `KeepPrivate=1` само по себе решит вопрос после первого подключения к торговому счёту через вызов Python API, не приходится.
Это штатный способ задания параметров, описанный в [справке по запуску с собственной конфигурацией](https://www.metatrader5.com/ru/terminal/help/start_advanced/start). Во время работы такой файл используется в режиме чтения, то есть изменения из интерфейса не переписывают переданный INI. В командной строке при этом указывается только путь к файлу, а реквизиты находятся внутри него.
В результате такого запуска терминал успешно подключился к счёту, затем мы штатно закрыли его, удалили временный INI и повторно запустили только с `/portable`. На этот раз пароль не потребовался, а журнал подтвердил новую авторизацию и синхронизацию счёта. Значит, такую последовательность действий и будем использовать в менеджере терминалов. Python-модуль `MetaTrader5`, с которого начиналась проверка, оказался пока не нужен.
Обратим внимание на штатное закрытие между двумя запусками. При таком завершении терминал может сохранить настройки и корректно закончить работу, поэтому здесь оно предпочтительнее принудительной остановки через `terminate()`. Для этого добавим функцию `graceful_close()`, которая запрашивает закрытие терминала и ожидает завершения процесса. Если на этом этапе штатное закрытие не сработало, менеджер завершает процесс принудительно и сообщает об ошибке настройки счёта, поскольку сохранение параметров в таком случае не подтверждено.
Теперь вернёмся к пользователю за домашним компьютером. У него уже может быть установлено несколько терминалов MetaTrader 5 от разных брокерских компаний, с заранее подготовленными списками торговых серверов. Если нужный сервер уже есть в одном из таких терминалов, достаточно взять файл `servers.dat` из его папки данных, и выполнять поиск компании вообще не придётся. Если же нужного сервера в списке нет, найти брокера можно в локальном терминале, после чего передать подготовленный файл на удалённый сервер через браузер. Если два EXE-файла терминала и редактора мы тоже загрузим с компьютера пользователя, то дальнейшее создание экземпляров будет обходиться без мастера установки и удалённого рабочего стола.
Для этого организуем папку-образец со следующими файлами:
```text
terminal-template/
terminal64.exe
MetaEditor64.exe
config/
servers.dat
```
Файл `servers.dat` должен содержать сведения о торговом сервере, к которому собираемся подключать новый экземпляр. Если позднее понадобится другой брокер, пользователь обновит список на своём компьютере и загрузит файл заново. Работающие экземпляры это не затронет: у них уже есть сведения о собственных серверах, а новый образец используется при последующем создании копий.
Загрузка файлов необязательна, если на сервере уже есть установка MT5, из которой их можно взять. Её папку зададим в настройке `mt5_installation`. Если она не указана, менеджер сможет определить установку автоматически, но только когда сам проект находится внутри её папки `MQL5/Shared Projects`. Для каждого файла сначала проверим папку-образец и лишь при его отсутствии обратимся к выбранной установке. Это позволяет, например, загрузить только новый `servers.dat`, а исполняемые файлы использовать из уже установленного терминала.
Отдельно нужен скомпилированный `ServiceStateWriter.ex5`. Можно указать путь к готовому файлу в настройках или загрузить его на той же странице. Если ни тот, ни другой вариант не задан, перед созданием экземпляра менеджер подготовит сервис из исходного кода текущего проекта. Для этого в `deploy_service.py` выделим функцию `service_build_paths()`, которая определяет пути к MetaEditor, исходнику и результату сборки, и функцию `compile_service()`, выполняющую компиляцию. MetaEditor сначала будем искать среди загруженных файлов, затем — в выбранной установке MT5. При размещении проекта внутри `MQL5/Shared Projects` результат, как и прежде, попадёт в каталог сервисов терминала, а в отдельной папке проекта EX5 будет создан рядом с исходником.
Если EX5 уже не старее исходника, повторная компиляция не потребуется. Полученный файл скопируем в новый терминал, поэтому клонировать туда репозиторий и компилировать один и тот же исходник заново не придётся. Если MetaEditor по какой-то причине не смог скомпилировать сервис, создание нового экземпляра терминала остановится с сообщением об ошибке. В этом случае сервис можно скомпилировать вручную, открыв MetaEditor, или загрузить готовый EX5.
Загрузку файлов реализуем через маршрут `PUT /api/files/{kind}`. По значению `kind` менеджер определяет, какой файл загружается и под каким именем его сохранить. Тело запроса читается порциями во временный файл, поэтому загрузка большого EXE не требует держать его целиком в памяти. По завершении временный файл заменяет прежний файл образца. В уже установленных экземплярах терминалов такое обновление ничего не меняет.
Раньше класс `Config` только читал `config.json` при запуске приложения. Теперь настройки можно изменять во время работы, поэтому добавим `ConfigStore`, который хранит проверенную конфигурацию и записывает её обратно в тот же файл. Для проверки отдельного документа используем обычную модель Pydantic `ConfigData`: нам важно проверять именно переданные настройки, не подмешивая при этом значения из другого источника `BaseSettings`.
Относительные пути считаем от папки конфигурационного файла. Запас места задаётся в МиБ, а время ожидания — в секундах. Значение 1024 МиБ здесь служит изменяемым порогом предупреждения, а не оценкой того, сколько обязательно требуется каждому терминалу.
Чтобы не оставить обрезанный JSON при неудачной записи, мы сначала полностью записываем новый документ во временный файл рядом с исходным, сбрасываем буферы и только затем заменяем основной файл:
Рабочая конфигурация в памяти обновляется после успешной замены. Переименования файла недостаточно для общей транзакции со всеми папками и процессами, но оно защищает нас от частично записанного JSON. Для более длинной операции создания экземпляра понадобится отдельный откат.
Отдельный путь для каждого экземпляра сохранять не будем: поскольку все терминалы находятся внутри одной базовой папки, достаточно соединить `mt5_folder` с именем экземпляра, которое служит ключом в словаре `terminals`. Например, при `mt5_folder`, равном `C:\MT5`, экземпляр `MetaTrader5.5` всегда будем искать в `C:\MT5\MetaTrader5.5`. Если базовую папку поменять в настройках, новое расположение сразу будет использоваться для всех экземпляров. При этом сами папки должны уже находиться там: если указать неверное место, менеджер просто не найдёт терминалы. Старое поле `path`, если оно осталось в конфигурации, при чтении игнорируем, а при следующем сохранении убираем.
Пароль, который пользователь задаёт менеджеру, тоже хранится в `config.json`, как мы и договорились для текущей реализации. При этом выдавать его обратно браузеру для заполнения формы не требуется. В ответе настроек заменяем его признаком наличия:
В словари карточек пароли вообще не включаются. Это стало особенно существенным после появления записи настроек: прежний отладочный вывод полного словаря экземпляров мы убрали.
## Создаём экземпляр
Чтение состояния и поиск процессов остаются в `MT5_Control`, а последовательность подготовки вынесем в новый класс `TerminalManager`. Ему нужны конфигурация, работа с файлами и возможность дождаться результата запуска, поэтому помещать всю эту работу в обработчик HTTP-запроса было бы неудобно.
Начнём с имени. Предложим первое свободное значение вида `MetaTrader5.5`, но оставим пользователю возможность его изменить. Имя экземпляра является именем папки и ключом связи с сервисом, поэтому допускаем только один корректный компонент пути Windows. Учтём также, что Windows не различает регистр: занятая папка `MetaTrader5.5` должна мешать созданию `metatrader5.5`.
После проверки исходных файлов и свободного места создадим папку. Здесь намеренно не используем `exist_ok=True`: новая операция должна владеть именно созданной ею папкой, иначе при последующем откате можно было бы удалить чужие данные.
```python
target.mkdir()
```
Копируем EXE, `config/servers.dat` и сервис. Для сервиса сохраняем путь из четвёртой части:
Затем создаём `config/services.ini` по нашему образцу, подставляя имя базы из настройки `database`. Как и прежде, MQL5-сервис открывает файл в общей папке `Common/Files`, поэтому путь, который использует Python, должен указывать на этот же файл того же пользователя Windows. Сам механизм запуска через `services.ini` подробно разобран в прошлой статье.
Когда подготовка завершена, добавляем экземпляр в конфигурацию и обновляем объект управления в памяти:
```python
updated['terminals'][folder]={
'name':record.get('name')orfolder,
'login':login,
'server':server,
'password':password,
}
```
Теперь можно запускать процесс, однако успешный `Popen()` ещё не означает, что всё готово. Дождёмся свежей записи сервиса, причём проверим не только время, но и путь терминала. Это важно при повторном использовании имени: в базе могли остаться сведения о ранее удалённой копии. Если требуется подключение к конкретному счёту, дополнительно проверим флаг соединения, логин и сервер.
При неудаче нового создания завершаем принадлежащие этому экземпляру процессы, удаляем созданную папку и убираем запись из конфигурации. Если сам откат не удался, сообщаем об этом отдельно. Удаление применяется именно к новой папке, созданной текущей операцией; ошибка смены счёта у существующего экземпляра такого отката не вызывает.
## Выбираем счёт при запуске
Пустые реквизиты означают, что менеджер не задаёт счёт. Тогда запустим терминал с `/portable`: он использует собственные сохранённые настройки либо работает без авторизации. Для создания экземпляра в таком режиме достаточно появления свежих данных сервиса, а отсутствие подключения к брокеру отображается отдельно.
Если счёт указан, номер сравниваем вместе с именем сервера. Один и тот же номер может встречаться у разных брокеров, поэтому совпадения только `login` недостаточно.
Перед подключением подготовим и основную конфигурацию экземпляра: в секции `[Common]` файла `config/common.ini` сохраним `Login`, `Server` и `KeepPrivate=1`. Это делает функция `prepare_account_defaults()`. Она сохраняет остальные настройки и не записывает туда пароль: он будет передан отдельным временным файлом. Так мы явно задаём выбор счёта и для последующего запуска без `/config`, не рассчитывая, что все параметры временной конфигурации перенесутся в основную сами.
Полные реквизиты позволяют выполнить первую настройку через временный INI. В `start()` соответствующий фрагмент выглядит так:
При проверке в папке с пробелами обнаружилась ещё одна особенность: передача `/config:путь` обычным элементом списка аргументов приводила к кавычкам вокруг всего ключа, и терминал сообщал, что не может загрузить INI. Поэтому для этого запуска формируем строку с кавычками именно вокруг значения: `/config:"C:\папка с пробелами\first-login.ini"`. Оболочку командной строки при этом не вызываем: `Popen()` по-прежнему запускает сам терминал.
После подтверждения счёта штатно закрываем терминал, удаляем временный файл и повторяем запуск без `/config`. Ещё раз ждём подключение к тому же счёту. Только теперь считаем настройку завершённой.
Есть небольшая особенность с изменением пароля. Сервис сообщает логин и сервер, но по ним нельзя определить, соответствует ли сохранённый пароль тому, который только что ввёл пользователь. Поэтому в памяти менеджера запоминаем успешно применённый набор реквизитов. Если пароль изменился, повторяем настройку, даже когда номер счёта остался прежним. После перезапуска самого менеджера полные реквизиты также один раз применяются заново: подтверждение предыдущего процесса мы в памяти уже не найдём.
Старые конфигурации могут содержать логин и сервер без пароля. Если терминал самостоятельно подключился к нужному счёту, этого достаточно. Если же нужна перенастройка, попросим указать пароль. Для нового экземпляра с заданным счётом требуем полный набор реквизитов сразу, поскольку использовать сохранённый пароль в ещё не созданной копии неоткуда.
Для существующего экземпляра настройка счёта выполняется в его прежней папке. Его историю и программы сохраняем, а при ошибке показываем результат пользователю. Автоматически перезапускать терминал до бесконечности не будем: неверные реквизиты от этого вряд ли станут правильными.
На странице «Настройки» появились общие параметры размещения терминалов (рис. 1). Здесь задаются базовая папка всех экземпляров и папка с исходными файлами, а при необходимости — установка MT5, из которой можно взять недостающие файлы, и путь к готовому сервису.
Следующий блок предназначен для загрузки исходных файлов, которые будут использоваться при создании терминалов (рис. 2). Под списком поясняется, откуда менеджер возьмёт файлы, если они не загружены, и в каком случае самостоятельно скомпилирует сервис.

*Рис. 2. Файлы для новых экземпляров*
В отдельном блоке находятся команды обновления проекта, сборки и распространения сервиса (рис. 3). К их работе мы вернёмся в следующем разделе.

*Рис. 3. Обновление проекта и распространение сервиса*
Ниже находится список счетов уже добавленных экземпляров (рис. 4). Для каждого из них показаны логин, торговый сервер и наличие сохранённого пароля в менеджере.
Если раскрыть нужный экземпляр, появится форма изменения его отображаемого имени и реквизитов счёта (рис. 5). Пустое поле пароля сохраняет прежнее значение, а для удаления реквизитов менеджера предусмотрен отдельный флажок. При этом сохранённые настройки самого терминала продолжают действовать, как и в рассмотренном сценарии без заданного счёта.
На главной странице каждый экземпляр представлен отдельной карточкой с состоянием, финансовыми показателями и кнопками управления (рис. 6). Поиск и фильтр помогают выбрать нужный терминал, когда их становится больше. На снимке показаны первые два работающих терминала с данными их демо-счетов.

*Рис. 6. Главная страница с карточками терминалов*
Нажмём «Новый терминал», изменим предложенное имя, если это требуется, и при необходимости заполним параметры счёта (рис. 7). Форма открывается в отдельном окне, а реквизиты счёта объединены в раскрываемый раздел. Отображаемое имя вынесено отдельно: его изменение не переименовывает папку и не меняет ключ связи с сервисом.

Общую навигацию вынесем в шаблон `base.html`, от которого наследуются обе страницы. Стили и SVG-иконки находятся в самом проекте; для этих страниц больше не требуется загружать Bootstrap, jQuery и набор иконок с внешних сайтов.
Создание и первое подключение могут занимать заметное время, поэтому обычного ответа «процесс запущен» здесь мало. HTTP-запрос регистрирует фоновую задачу и возвращает код 202 вместе с её идентификатором. JavaScript затем запрашивает `GET /api/jobs/{id}`, показывая текущий этап и ожидая состояния `done` или `error`.
Маршруты собраны следующим образом:
| Запрос | Назначение |
|---|---|
| `GET /api/settings` | Настройки, сведения о файлах и предлагаемое имя |
| `PUT /api/settings` | Сохранение общих настроек |
| `PUT /api/files/{kind}` | Загрузка файла образца |
| `POST /api/terminals` | Создание и запуск экземпляра |
| `POST /create/{name}` | Создание с именем в пути запроса |
| `POST /start/{name}` | Запуск и проверка заданного счёта |
| `POST /stop/{name}` | Остановка экземпляра |
| `PUT /api/terminals/{name}/account` | Сохранение и применение реквизитов |
| `GET /api/jobs/{id}` | Этап и результат операции |
Это меняет и поведение прежних маршрутов запуска и остановки: их ответ 202 означает принятую задачу, а окончательный результат нужно получить отдельно. Код главной страницы обновлён с учётом этой последовательности. После успешного создания перечитываем страницу, чтобы новая карточка построилась из актуального списка экземпляров; перезапускать веб-приложение для этого не требуется.
Длительные операции выполняются последовательно в одном рабочем потоке, а короткие запросы состояния продолжают обслуживаться. Это ограничивает одновременное создание, но не количество экземпляров. Очередь задач и подтверждения реквизитов пока находятся в памяти одного процесса приложения, поэтому несколько worker-процессов с общей конфигурацией для этой реализации не используем.
## Обновляем проект и сервис
После создания нескольких экземпляров нам понадобится ещё одна операция. Допустим, мы изменили сервис и отправили новую версию в репозиторий. Исходный код в папке проекта можно обновить, но от этого EX5 в остальных терминалах сам не изменится. Нужно собрать сервис заново и разложить полученный файл по установленным копиям. Такой же порядок потребуется, если исходники обновлены вручную через Git.
Добавим в настройки раздел «Обновление проекта и сервиса», который уже показан на рис. 3. Сначала кнопка «Проверить обновления» выполнит `git fetch` для удалённого репозитория текущей ветки. Рабочие файлы при этом остаются на месте: мы только узнаём, появились ли новые коммиты в upstream. Если наша ветка отстаёт, интерфейс предложит загрузить обновление отдельной кнопкой.
Само обновление выполняем только в чистом рабочем дереве и только как fast-forward. Если у пользователя есть незакоммиченные правки, неотслеживаемые файлы или собственные коммиты, попросим сначала разобраться с ними обычными средствами Git. Прятать изменения в stash или сбрасывать их ради работы кнопки не будем. Для этих операций используем Git, уже установленный на сервере и настроенный для доступа к репозиторию.
После обновления исходника сервис нужно перекомпилировать. В интерфейсе сравним время изменения MQ5 и EX5, а содержимое готового EX5 — с файлами установленных экземпляров. Заодно проверим, настроен ли у них автозапуск нашего сервиса. Это позволит показать, где сборка устарела, ещё не разложена или требуется исправить конфигурацию запуска. Кнопка «Собрать и разложить сервис» принудительно вызывает `compile_service(..., force=True)` и копирует результат в папки экземпляров, пути к которым строятся из `mt5_folder` и их имён. Она работает с исходным кодом текущего проекта, поэтому загруженный на той же странице готовый EX5 в этой операции не используется.
Как и в скрипте прошлой части, успешного завершения MetaEditor недостаточно: проверяем, что файл сборки действительно обновился. Если этого не произошло, старый сервис по терминалам не раскладываем. Если же сборка прошла, но для одного из экземпляров не удалось завершить развёртывание, показываем, на каком этапе и по какому пути возникла ошибка; успешно выполненные копирования сохраняются.
Одного EX5 в папке сервиса ещё недостаточно для его автоматического запуска. Поэтому при раскладывании также проверим `config/services.ini`: если записи ServiceStateWriter нет, добавим её по образцу, а у существующей записи при необходимости исправим путь к программе, включим автозапуск и подставим имя базы. Другие сервисы и уже заданные параметры интервала сохраним. Если запись и так настроена правильно, перезаписывать файл не потребуется.
Если `services.ini` нужно изменить у работающего терминала, попросим сначала остановить этот экземпляр и повторить раскладывание. Терминал может сохранить собственные настройки при завершении работы, поэтому менять этот файл будем, когда он остановлен. Тот же способ настройки автозапуска используем и в скрипте `deploy_service.py`.
Работающий терминал продолжает исполнять уже загруженный сервис. Поэтому после копирования нужно перезапустить соответствующие экземпляры; их список выводим в результате операции. Автоматически останавливать все терминалы по нажатию кнопки сборки пока не будем.
Есть и особенность обновления самого веб-приложения. Раньше мы всегда запускали Uvicorn с `reload=True`, что было удобно при разработке. Но во время загрузки новой версии изменение Python-файлов может прервать ту самую задачу, которая их обновляет. Поэтому сделаем обычный запуск без автоматической перезагрузки, а для разработки добавим отдельный ключ `--reload`. В этом режиме кнопка загрузки Git-обновления недоступна. После обновления кода менеджер нужно перезапустить, чтобы он загрузил новую версию; изменение зависимостей из `requirements.txt` также потребует их установки.
| `POST /api/project/update` | Обновление рабочего дерева через fast-forward |
| `POST /api/project/deploy` | Сборка и распространение сервиса |
Все три команды используют ту же очередь фоновых задач и возвращают 202. На время глобальной операции новые команды терминалам не принимаются, а само обновление не начинается, пока другие задачи ещё выполняются. Просмотр страницы и чтение состояния при этом остаются доступны.
## Тестирование
Для запуска веб-приложения теперь достаточно зависимостей из `requirements.txt`:
```text
pip install -r requirements.txt
python run.py
```
Модуль `MetaTrader5` остаётся полезен для отдельных экспериментов, но рабочий сценарий этой части от него уже не зависит: состояние поступает от сервиса, а параметры первого подключения передаются самому терминалу.
Сначала загрузим исходные файлы и проверим создание экземпляра без счёта. В конфигурации должна появиться запись с именем экземпляра и пустыми строками для реквизитов, в базовой папке — новая подпапка с файлами терминала, сервисом и его настройками, а на главной странице — новая карточка. После первой записи сервиса менеджер видит работающий процесс, хотя соединения с торговым сервером может ещё не быть.
Затем создадим экземпляр с существующим счётом. В ходе операции должны последовательно пройти первое подключение, штатное закрытие и повторный запуск без файла авторизации. После завершения в папке экземпляра не должно остаться файлов `manager-login-*.ini`, а данные сервиса должны подтверждать нужный логин и сервер.
Отдельно проверим остановку и запуск уже созданной копии, а затем изменение её счёта через страницу настроек. Ошибка авторизации у существующего экземпляра должна оставлять его папку на месте. Ошибка при создании новой копии, напротив, должна приводить к удалению её папки и записи конфигурации. Занятое имя нужно отклонять до копирования, сохраняя прежнее содержимое каталога.
Эти случаи включены в автоматические проверки `tests/test_provisioning.py`. В них граница с настоящим процессом и сервисом заменена имитацией, поэтому они не запускают терминалы и не обращаются к брокеру. Они также проверяют, что неудачная замена JSON сохраняет исходную конфигурацию, ответы не содержат пароль, а загрузка нового `servers.dat` не меняет файл уже установленного экземпляра. Дополнительные HTTP-проверки находятся в `tests/test_http.py`: через настоящий локальный веб-сервер проверяются формы, загрузка файлов и получение результата фоновой задачи.
Обновление Git проверяем отдельно в `tests/test_project_updates.py`, создав временный локальный remote и две рабочие копии. Так можно проверить наличие новой версии, fast-forward и отказ при локальных правках, не меняя рабочий репозиторий проекта. Компиляцию и копирование проверяем с имитацией MetaEditor, включая случай, когда он завершился, но EX5 не обновил. Проверки `services.ini` включают добавление отсутствующей записи, исправление автозапуска и сохранение других сервисов. При ручной проверке на одном из тестовых терминалов мы восстановили запись автозапуска, получили свежие данные сервиса и повторили запуск без изменения конфигурации: сервис снова начал записывать состояние.
Заодно уточним способ чтения SQLite на стороне Python: менеджер открывает существующую базу только для чтения. Создание файла и таблиц остаётся задачей MQL5-сервиса, поэтому неверный путь в настройках больше не приводит к появлению пустой базы. Если база недоступна или в ней нет нужных таблиц, в консоли покажем её путь и причину ошибки. При проверке запуска это помогает отличить отсутствие данных сервиса от проблемы с подключением к торговому счёту.
Для проверки настоящего процесса предусмотрен отдельный инструмент `tools/smoke_create_terminal.py`. Он использует изолированную папку и отдельную базу, получает свежую запись сервиса, а после завершения убирает тестовую копию. Через него проверены создание без счёта и полный цикл со счётом: авторизация через INI, штатное закрытие, удаление временного файла и успешный повторный вход без него. Папка проверки находилась внутри пути с пробелами, поэтому исправленное формирование `/config` тоже участвовало в этом опыте.
Эксперименты в этой части выполнены на одном компьютере между разными папками. Перенос подготовленного файла с домашнего компьютера на другой сервер также нужно включить в окончательную проверку перед публикацией.
## Заключение
В этой части мы подошли к созданию экземпляра как к отдельной операции: подготовили исходные файлы, сохранили конфигурацию, разложили сервис и дождались его данных. Веб-интерфейс теперь позволяет менять настройки и загружать файлы, поэтому для каждой новой копии терминала уже не требуется повторять установку вручную.
Заодно выяснилось, что первое подключение состоит из двух разных задач. Терминалу нужны сведения о торговом сервере, которые в нашем случае удалось перенести файлом `servers.dat`, а параметры счёта нужно не только передать, но и сохранить для последующих запусков. Проверенный способ с временным конфигурационным файлом позволил выполнить это без Python API MetaTrader5.
Список следующих задач остаётся довольно большим: история показателей и изменение схемы базы, журнал операций, подробные позиции, работа с несколькими серверами и управление советниками. Массовую замену `servers.dat` в уже установленных терминалах пока тоже оставим на будущее: для текущего сценария достаточно обновлять образец, из которого создаются новые экземпляры.
Архив `MQL5.zip` распаковывается в папку данных терминала: все файлы проекта находятся внутри `MQL5/Shared Projects/mt5-manager`. Скомпилированные EX5, исполняемые файлы терминала и личные настройки в него не входят. Файл `config.json` в архиве создан из безопасного примера `config.example.json` с пустым списком счетов.