mt5-manager/docs/articles/part-02/publish/article.html
468 lines
60 KiB
HTML
<!doctype html>
<meta charset="utf-8">
<h1>Разрабатываем менеджер терминалов (Часть 2): Запуск нескольких экземпляров</h1>
<div class="inner formatted-content">
<h3>Введение</h3><p>В первой <a href="https://www.mql5.com/ru/articles/19804" target="_blank">части</a> серии "Менеджер терминалов своими руками" мы поставили перед собой задачу — создать веб-интерфейс для управления запуском и остановкой экземпляров MetaTrader 5 на локальном компьютере. Мы определили архитектуру системы, начав с разработки минимально жизнеспособного продукта (MVP) — веб-сервера, который может принимать HTTP-запросы и управлять одним экземпляром терминала. Заодно мы рассмотрели такие ключевые понятия как API, REST, HTTP-запросы, декораторы.</p><p>Для реализации проекта был выбран язык Python, благодаря его богатой экосистеме библиотек и простоте интеграции с внешними API — в данном случае с библиотекой MetaTrader 5.</p><p>Фреймворк FastAPI позволяет создавать веб-приложения с минимальными затратами кода, предоставляя из коробки поддержку типизации, асинхронности и автоматической документации через Swagger UI.</p><p>В связке с ним используется Uvicorn, лёгкий и быстрый ASGI-сервер, обеспечивающий высокую производительность даже при большом количестве одновременных запросов.</p><p>Для работы с процессами терминалов применяются библиотеки psutil и subprocess, что позволяет удобно получать сведения о запущенных экземплярах и управлять ими.</p><p>В дальнейшем планируется добавить использование SQLite или другой базы данных для хранения состояния экземпляров, а также фронтенд-слой на JavaScript или Vue.js для более интерактивного интерфейса.</p><p>Теперь пришло время расширять функциональность и переходить к следующим этапам — реализации более сложных возможностей, таких как управление несколькими экземплярами, хранение состояния, интеграция с MetaTrader5 API и веб-интерфейс с полной информацией о терминалах.</p><p><br></p><h3>Намечаем путь<br></h3><p>Попробуем очертить примерные границы следующего кусочка реализации. Во-первых, пока у нас нет реального разделения общей функциональности на два разных веб-сервера, придётся в каком-то минимальном виде добавлять интерфейсные части в тот веб-сервер, который мы уже начали делать. Эта работа не пропадёт зря, так как в дальнейшем мы сможем перенести уже имеющийся код на другой веб-сервер с некоторыми правками. Поэтому начнём с того, что создадим дополнительные файлы для размещения разных частей кода проекта, придав им некоторый налёт организованност
<span class="keyword">from</span> fastapi.templating <span class="keyword">import</span> Jinja2Templates</pre><p>Затем, после создания объекта приложения (<i>app</i>), мы подключаем новую конечную точку <i>GET /static</i>, при обработке которой любые имена, идущие в URL после <i>static/</i>, будут восприниматься как файл из папки <i>static</i> нашего проекта:</p><pre class="code"><span class="preprocessor"># </span>Создаём объект приложения
app = FastAPI()
<span class="preprocessor"># </span>Подключаем статические файлы
app.mount(<span class="string">"/static"</span>, StaticFiles(directory=<span class="string">"static"</span>), name=<span class="string">"static"</span>)
</pre><p>Заметьте, что в данном случае мы не создаём отдельную функцию-обработчик маршрута <i>GET /static</i>, а вызываем метод объекта приложения, который сам создаст необходимую функцию с нужным содержимым.</p><p>Теперь создадим объект, который будет использоваться при необходимости передать в качестве ответа HTML-код, сформированный по определённому шаблону из папки <i>templates</i>:</p><pre class="code"><span class="preprocessor"># </span>Создаём объект для работы с шаблонами HTML-кода
templates = Jinja2Templates(directory=<span class="string">"templates"</span>)</pre><p>Обработчик маршрута GET / перепишем таким образом:</p><pre class="code">@app.get(<span class="string">'/'</span>, response_class=HTMLResponse)
<span class="keyword">async</span> <span class="keyword">def</span> index(request: Request):
    <span class="string">'''Обработчик GET / (корневой каталог)'''</span>
    instances = load_instances()
    <span class="keyword">return</span> templates.TemplateResponse(<span class="string">"index.html"</span>, {<span class="string">"request"</span>: request, <span class="string">"instances"</span>: instances})</pre><p>В нём мы получаем словарь <i>instances</i> с информацией о зарегистрированных экземплярах терминалов и их текущем статусе (запущен/остановлен). Далее мы возвращаем шаблонный ответ, получаемый из файла <i>index.html</i> с учётом информации из словаря <i>instances</i>. Какая именно информация будет присутствовать в словаре <i>instances,</i> и как она будет трансформироваться в HTML-код, мы посмотрим чуть позже.</p><p>В два оставшихся обработчика маршрутов мы тоже внесем небольшие изменения. Они будут однотипны, то есть для обработчика запуска экземпляра мы сделаем то же самое, что и для обработчика остановки. Необходимость изменений вызвана тем, что теперь нужно в обработчик передавать информацию о том, какой экземпляр мы хотим сейчас запустить или остановить. Когда экземпляр был единственный, такой проблемы не возникало.</p><p>Добавим в URL этих маршрутов изменяемую часть, обозначенную как <i><span style="background-color:rgb(255, 246, 200);">{name}</span></i>. Это обязывает нас добавить в список аргументов функции-обработчика новый аргумент с именем <i><span style="background-color:rgb(216, 232, 194);">name</span></i> и типом <i><span style="background-color:rgb(216, 232, 194);">str</span></i> (так как имя экземпляра — это строка):</p><pre class="code">@app.post(<span class="string">'/start/<span style="background-color:rgb(255, 246, 200);">{name}</span>'</span>)
<span class="keyword">async</span> <span class="keyword">def</span> start_instance(<span style="background-color:rgb(216, 232, 194);">name: <span class="built_in">str</span></span>):
    <span class="string">'''Обработчик POST /start - запуск терминала'''</span>
    result = start_mt5(<span style="background-color:rgb(164, 192, 228);">name</span>)
    <span class="keyword">return</span> JSONResponse(result)
@app.post(<span class="string">'/stop/<span style="background-color:rgb(255, 246, 200);">{name}</span>'</span>)
<span class="keyword">async</span> <span class="keyword">def</span> stop_instance(<span style="background-color:rgb(216, 232, 194);">name: <span class="built_in">str</span></span>):
    <span class="string">'''Обработчик POST /stop - остановка терминала'''</span>
    result = stop_mt5(<span style="background-color:rgb(164, 192, 228);">name</span>)
    <span class="keyword">return</span> JSONResponse(result)
</pre><p>Это приведёт к тому, что при запуске обработчика ему в качестве аргумента будет передаваться строка, вырезанная из той части URL, которая соответствует положению <i><span style="background-color:rgb(255, 246, 200);">{name}</span></i>. А далее мы уже можем с <span style="background-color:rgb(164, 192, 228);">ней</span> работать внутри функций-обработчиков.</p><p>Обновим нашу таблицу маршрутов в связи со сделанными изменениями:</p><table cellspacing="0" cellpadding="3" width="100%" class="standart"><thead><tr><th>#</th><th>Метод</th><th>URL</th><th>Описание, параметры</th><th>Формат<br>ответа </th></tr></thead><tbody><tr><td>1</td><td>GET</td><td>/</td><td>Отображает главную страницу с веб-интерфейсом для управления экземплярами MetaTrader 5<br>Пока что пусть отображается только заголовок с названием проекта.</td><td>HTML</td></tr><tr><td>2 </td><td>POST</td><td style="background-color:rgb(216, 232, 194);">/start/{name} </td><td>Запускает экземпляр терминала MetaTrader 5 с именем {name}</td><td>JSON</td></tr><tr><td>3</td><td>POST<br></td><td style="background-color:rgb(216, 232, 194);">/stop/{name}</td><td>Останавливает запущенный процесс MetaTrader 5 с именем {name}</td><td>JSON</td></tr></tbody></table><p>Даже несмотря на то, что проект пока находится на стадии активной разработки, важно уже сейчас формировать устойчивую архитектуру. В идеале код приложения должен быть легко расширяемым — чтобы добавление новых функций, маршрутов или логики управления не требовало переписывания существующих частей.</p><p>Именно поэтому имеет смысл придерживаться принципов разделения ответственности и чистой архитектуры, когда:</p><ul><li>Модуль mt5_control.py отвечает только за работу с процессами терминалов.</li><li>Файл main.py концентрируется на маршрутах и веб-интерфейсе.</li><li>Папки templates и static изолируют визуальную часть приложения.</li></ul><p>Такое разделение делает проект более устойчивым к изменениям и облегчает тестирование. В будущем, при необходимости, эти части можно будет вынести в отдельные микросервисы.</p><p><br></p><p></p><h3>Экземпляры терминалов<br></h3><p>Чтобы можно было запускать несколько экземпляров терминалов MetaTrader 5 на сервере, он там должны физически присутствовать. То есть на сервере должны быть несколько папок, в каждой из располагается запускаемый файл терминала. Существуют разные способы, как можно этого добиться, но сейчас для упрощения предположим, что эту часть работы пользователь будет пока выполнять вручную. </p><p>Проделаем эту операцию. Если на компьютере ещё нет установленного терминала, то необ<D0BE>
MT5_FOLDER = <span class="string">'C:/MT5'</span>
<span class="comment"># Имя запускаемого файла терминала</span>
MT5_EXE = <span class="string">'terminal64.exe'</span></pre><p>Теперь можно сделать функцию, которая по имени будет формировать полный путь к запускаемому файлу нужного экземпляра терминала. Сейчас мы просто будем соединять вместе три части полного пути: общую папку, имя экземпляра и имя запускаемого файла:</p><pre class="code"><span class="keyword">def</span> instance_path(name: <span class="built_in">str</span>) -&gt; <span class="built_in">str</span>:
    <span class="string">'''
    Формирует полный путь по имени экземпляра терминала
    Args:
        name (str): Имя экземпляра терминала.
    Returns:
        path (str): Полый путь к запускаемому файлу экземпляра терминала.
    '''</span>
    <span class="keyword">return</span> <span class="string">f'<span class="subst">{MT5_FOLDER}</span>/<span class="subst">{name}</span>/<span class="subst">{MT5_EXE}</span>'</span></pre><p>Для хранения списка имён экземпляров будем использовать пока обычный список, в котором будут находиться выбранные нами названия папок:</p><pre class="code"># Список имён экземпляров (пока фиксированный)
instances_names = ['MetaTrader5.1', 'MetaTrader5.2', 'MetaTrader5.3']
</pre><p>По готовому списку имён мы можем однозначно получить полные пути к исполняемым файлам каждого экземпляра терминала. Поскольку они пока тоже будут постоянными, то можно заранее сформировать два вспомогательных словаря, которые будут использованы для получения пути по заданному имени и имени по заданному пути:</p><pre class="code"><span class="comment"># Словарь соответствия имя -&gt; путь</span>
instances_paths = {name: instance_path(name) <span class="keyword">for</span> name <span class="keyword">in</span> instances_names}
<span class="comment"># Словарь соответствия путь -&gt; имя</span>
paths_instances = {instance_path(name): name <span class="keyword">for</span> name <span class="keyword">in</span> instances_names}</pre><p>После длительных размышлений мы решили отказаться от повторного использования информации о состоянии экземпляров, которая была получена при запуске сервера. Вместо этого, мы при каждой операции будем актуализировать состав запущенных экземпляров. Для этого напишем функцию <i>load_instances()</i>, которая будет формировать словарь <i>instances.</i> В этом словаре ключом будет имя экземпляра, а значением — ещё один словарь с двумя ключами <i>pid</i> и <i>status</i>. В них мы будем хранить уникальный идентификатор запущенного процесса (PID) и статус экземпляра с заданным именем.</p><p>Возможно, что хранение этих двух видов информации является избыточным, так как в принципе, по значению PID мы можем судить о том, запущен ли данный экземпляр или нет. Если PID &gt; 0, то процесс выполняется. Но сейчас не совсем понятно, как сделать лучше, поэтому пусть будут храниться оба этих поля. Далее мы, скорее всего, будем расширять словарь с информацией об одном экземпляре.</p><p>Вначале мы инициализируем словарь <i>instances</i> для экземпляров одинаковыми значениями:</p><pre class="code">{<span class="string">'pid'</span>: <span class="number">0</span>, <span class="string">'status'</span>: <span class="string">'stopped'</span>}</pre><p>что соответствует незапущенному экземпляру. Затем, используя библиотеку psutil, организуем перебор всех запущенных на компьютере процессов. Для каждого из них мы будем смотреть, не является ли данный процесс одним из экземпляров нашего терминала. Если да, то определяем его имя, PID, и сохраняем их в соответствующем элементе словаря <i>instances</i>.</p><pre class="code"><span class="keyword">def</span> load_instances() -&gt; <span class="built_in">dict</span>:
    <span class="string">'''Получение информации о статусе экземпляров
    терминала MetaTrader 5 из заданных папок.
    Returns:
        instances (dict): Информация об экземплярах терминала.
    '''</span>
    <span class="comment"># Формируем словарь для всех экземпляров по списку имён</span>
    instances = {name: {<span class="string">'pid'</span>: <span class="number">0</span>, <span class="string">'status'</span>: <span class="string">'stopped'</span>}
                 <span class="keyword">for</span> name <span class="keyword">in</span> instances_names}
    <span class="comment"># Перебираем все запущенные процессы</span>
    <span class="keyword">for</span> proc <span class="keyword">in</span> psutil.process_iter([<span class="string">'pid'</span>, <span class="string">'name'</span>, <span class="string">'exe'</span>]):
        <span class="keyword">try</span>:
            <span class="comment"># Получаем путь к запускаемому файлу процесса</span>
            exe_path = <span class="built_in">str</span>(proc.info[<span class="string">'exe'</span>]).replace(<span class="string">'\\'</span>, <span class="string">'/'</span>)
            <span class="comment"># Если он есть среди путей экземпляров терминала</span>
            <span class="keyword">if</span> exe_path <span class="keyword">and</span> exe_path <span class="keyword">in</span> paths_instanses:
                <span class="comment"># Определяем имя экземпляра для данного пути</span>
                name = paths_instanses[exe_path]
                <span class="comment"># Берём нужный экземпляр</span>
                instance = instances[name]
                <span class="comment"># Запоминаем идентификатор и статус процесса</span>
                instance[<span class="string">'pid'</span>] = proc.info[<span class="string">'pid'</span>]
                instance[<span class="string">'status'</span>] = <span class="string">'running'</span>
        <span class="keyword">except</span> (psutil.NoSuchProcess, psutil.AccessDenied):
            <span class="keyword">continue</span>
    <span class="keyword">return</span> instances</pre><p>Эта функция будет использована в обработчике маршрута GET / для формирования актуального состояния всех зарегистрированных экземпляров. Но также она будет использована и при каждой операции запуска и остановки. С её помощью мы сможем не запускать уже запущенный ранее экземпляр и не останавливать уже остановленный.</p><p><br></p><h3>Запуск терминалов<br></h3><p>Посмотрим теперь на реализацию функции запуска экземпляра терминала с определённым именем, передаваемым в качестве аргумента. Сначала мы получаем актуальную информацию о состоянии всех зарегистрированных терминалов. Если переданное имя нашлось среди существующих имён, то проверим, существует ли запущенный процесс для этого экземпляра.</p><p>Если нет, то тогда запускаем новый процесс, используя функцию <i>Popen()</i> из библиотеки subprocess. В качестве аргумента ей надо передать список аргументов командной строки запуска нужного исполняемого файла. Первым в этом списке идёт полный путь к запускаемому файлу, а дальше — ключ <i>/portable</i> для использования текущей папки терминала в качестве рабочей.</p><p>После успешного запуска, мы получаем значение уникального идентификатора процесса в операционной системе (PID) и вместе со статусом "running" сохраняем его в элементе словаря экземпляров для возврата из функции:</p><pre class="code"><span class="keyword">def</span> start_mt5(name: <span class="built_in">str</span>) -&gt; <span class="built_in">dict</span>:
    <span class="string">'''
    Запуск экземпляра терминала
    Args:
        name (str): Имя экземпляра терминала.
    Returns:
        instance (dict): Информация о запущенном терминале.
    '''</span>
    <span class="comment"># Получаем информацию об экземплярах терминала</span>
    instances = load_instances()
    <span class="comment"># Если имя экземпляра есть среди имеющихся</span>
    <span class="keyword">if</span> name <span class="keyword">in</span> instances:
        <span class="comment"># Берём нужный экземпляр</span>
        instance = instances[name]
        <span class="comment"># Если для него нет запущенного процесса, то</span>
        <span class="keyword">if</span> <span class="keyword">not</span> instance[<span class="string">'pid'</span>]:
            <span class="comment"># Запускаем новый процесс</span>
            process = subprocess.Popen([instanсes_paths[name], <span class="string">'/portable'</span>])
            <span class="comment"># Идентификатор запущенного процесса</span>
            pid = process.pid
            <span class="comment"># Запоминаем идентификатор и статус процесса</span>
            instance[<span class="string">'pid'</span>] = pid
            instance[<span class="string">'status'</span>] = <span class="string">'running'</span>
        <span class="comment"># Возвращаем результат - успешный запуск</span>
        <span class="keyword">return</span> {name: instance}
    <span class="comment"># Возвращаем результат - имя не найдено</span>
    <span class="keyword">return</span> {name: {<span class="string">'status'</span>: <span class="string">'not found'</span>}}</pre><p>Если же в запросе пришло имя экземпляра, которого нет среди зарегистрированных имён, то ничего не делаем и возвращаем информацию о том, что такой экземпляр не найден.</p><p><br></p><h3>Остановка терминалов</h3>Функция остановки экземпляра терминалов работает похожим образом. Ей также передаётся имя экземпляра, полученное из запроса. Также формируется актуальный состав всех зарегистрированных экземпляров с их текущим статусом. Если имя экземпляра корректное, то берём его PID, и создаём объект, позволяющий управлять процессом с заданным идентификатором. Посылаем процессу команду остановки и ждем не более 10 секунд завершения его работы:<pre class="code"><span class="keyword">def</span> stop_mt5(name: <span class="built_in">str</span>) -&gt; <span class="built_in">dict</span>:
    <span class="string">'''
    Остановка терминала
    Args:
        name (str): Имя экземпляра терминала.
    Returns:
        instance (dict): Информация о запущенном терминале.
    '''</span>
    <span class="comment"># Получаем информацию об экземплярах терминала</span>
    instances = load_instances()
    <span class="comment"># Если имя экземпляра есть среди имеющихся</span>
    <span class="keyword">if</span> name <span class="keyword">in</span> instances:
        <span class="comment"># Берём нужный экземпляр</span>
        instance = instances[name]
        <span class="comment"># Берём идентификатор его процесса</span>
        pid = instance[<span class="string">'pid'</span>]
        <span class="comment"># Если ранее терминал был запущен</span>
        <span class="keyword">if</span> pid:
            <span class="keyword">try</span>:
                <span class="comment"># Получаем объект процесса терминала</span>
                p = psutil.Process(pid)
                <span class="comment"># Останавливаем процесс</span>
                p.terminate()
                p.wait(<span class="number">10</span>)
            <span class="keyword">except</span> psutil.NoSuchProcess:
                <span class="keyword">pass</span>
        <span class="comment"># Удаляем информацию о запущенном ранее процессе</span>
        instance[<span class="string">'pid'</span>] = <span class="number">0</span>
        instance[<span class="string">'status'</span>] = <span class="string">'stopped'</span>
        <span class="comment"># Возвращаем результат - успешная остановка</span>
        <span class="keyword">return</span> {name: instance}
    <span class="comment"># Возвращаем результат - процесс не найден</span>
    <span class="keyword">return</span> {name: {<span class="string">'status'</span>: <span class="string">'not found'</span>}}</pre><p>Если процесс благополучно завершился, то возвращаем информацию об этом. Если нет, то всё равно считаем его завершённым. В текущих условиях мы пока не столкнулись с какими-либо проблемами при завершении процессов, поэтому оставим эту функцию именно в таком виде. Когда же проблемы возникнут, тогда и подумаем о дальнейшей доработке.</p><p>Ну и если так оказалось, что мы пытаемся остановить уже завершённый процесс (например, мы могли вручную закрыть окно запущенного терминала), то просто вернётся сообщение о том, что процесс не найден.</p><p><br></p><h3>Главная страница<br></h3><p>Теперь настала очередь заняться HTML-шаблоном формирования главной страницы. В нём нам надо сделать несколько вещей. Во-первых, <span style="background-color:rgb(164, 192, 228);">подключим</span> внешние статические файлы с CSS и JS-кодом. Во-вторых, добавим на страницу ненумерованный список, каждый элемент которого будет представлять собой информацию об одном экземпляре терминала.</p><p>Для его формирования воспользуемся движком шаблонов Jinja2. В обработчике маршрута GET / мы передали объекту шаблона наш сформированный словарь с информацией о зарегистрированных экземплярах. Он выглядит примерно так:</p><pre class="code">{
    <span class="string">"MetaTrader5.1"</span>: {
        <span class="string">"pid"</span>: <span class="number">23820</span>,
        <span class="string">"status"</span>: <span class="string">"running"</span>
    },
    <span class="string">"MetaTrader5.2"</span>: {
        <span class="string">"pid"</span>: <span class="number">24888</span>,
        <span class="string">"status"</span>: <span class="string">"running"</span>
    },
    <span class="string">"MetaTrader5.3"</span>: {
        <span class="string">"pid"</span>: <span class="number">0</span>,
        <span class="string">"status"</span>: <span class="string">"stopped"</span>
    }
}</pre><p>Используя конструкцию <span style="background-color:rgb(222, 188, 133);">{% for name, data in instances.items() %}</span> ... <span style="background-color:rgb(222, 188, 133);">{% endfor %}</span>, мы можем организовать цикл, который будет генерировать однотипные куски HTML-кода, подставляя в <span style="background-color:rgb(216, 232, 194);">нужные места</span> значения переменных цикла. В данном случае в переменную <i>name</i> будут попадать по очереди названия наших экземпляров терминалов: 'MetaTrader5.1', 'MetaTrader5.2', 'MetaTrader5.3'. А в переменную <i>data</i> будет попадать словарь с ключами <i>pid</i> и <i>status</i> для каждого экземпляра из словаря <i>instances</i>.</p><p>Также при необходимости мы можем использовать конструкцию условного оператора <span style="background-color:rgb(255, 246, 200);">{% if data.pid &gt; 0 %}</span> ... <span style="background-color:rgb(255, 246, 200);">{% else %}</span> ... <span style="background-color:rgb(255, 246, 200);">{% endif %}</span>. Благодаря ей мы отображаем для каждого экземпляра только одну кнопку для выполнения возможного действия: запуска для остановленного экземпляра и остановки для запущенного.</p><pre class="code"><span class="doctype">&lt;!DOCTYPE html&gt;</span>
<span class="tag">&lt;<span class="keyword">html</span>&gt;</span>
<span class="tag">&lt;<span class="keyword">head</span>&gt;</span>
  <span class="tag">&lt;<span class="keyword">title</span>&gt;</span>MT5 Manager<span class="tag">&lt;/<span class="keyword">title</span>&gt;</span>
  <span class="tag" style="background-color:rgb(164, 192, 228);">&lt;<span class="keyword">link</span><span class="attribute"> rel=<span class="value">"stylesheet"</span></span><span class="attribute"> href=<span class="value">"/static/styles.css"</span></span>&gt;</span>
<span class="tag">&lt;/<span class="keyword">head</span>&gt;</span>
<span class="tag">&lt;<span class="keyword">body</span>&gt;</span>
  <span class="tag">&lt;<span class="keyword">h1</span>&gt;</span>MT5 Manager<span class="tag">&lt;/<span class="keyword">h1</span>&gt;</span>
  <span class="tag">&lt;<span class="keyword">div</span><span class="attribute"> class=<span class="value">"instances"</span></span>&gt;</span>
    <span class="tag">&lt;<span class="keyword">h2</span>&gt;</span>Instances<span class="tag">&lt;/<span class="keyword">h2</span>&gt;</span>
    <span class="tag">&lt;<span class="keyword">ul</span><span class="attribute"> id=<span class="value">"instanceList"</span></span>&gt;</span>
      <span style="background-color:rgb(222, 188, 133);">{% for name, data in instances.items() %}</span>
      <span class="tag">&lt;<span class="keyword">li</span><span class="attribute"> class=<span class="value">"<span style="background-color:rgb(216, 232, 194);">{{ data.status }}</span>"</span></span>&gt;</span>
        <span class="tag">&lt;<span class="keyword">strong</span>&gt;</span><span style="background-color:rgb(216, 232, 194);">{{ name }}</span><span class="tag">&lt;/<span class="keyword">strong</span>&gt;</span>
        <span style="background-color:rgb(255, 242, 153);">{% if data.pid &gt; 0 %}</span>
        (PID: <span style="background-color:rgb(216, 232, 194);">{{ data.pid }}</span>)
        <span class="tag">&lt;<span class="keyword">button</span><span class="attribute"> onclick=<span class="value">"stopInstance('<span style="background-color:rgb(216, 232, 194);">{{ name }}</span>')"</span></span>&gt;</span>Stop<span class="tag">&lt;/<span class="keyword">button</span>&gt;</span>
        <span style="background-color:rgb(255, 242, 153);">{% else %}</span>
        <span class="tag">&lt;<span class="keyword">button</span><span class="attribute"> onclick=<span class="value">"startInstance('<span style="background-color:rgb(216, 232, 194);">{{ name }}</span>')"</span></span>&gt;</span>Start<span class="tag">&lt;/<span class="keyword">button</span>&gt;</span>
        <span style="background-color:rgb(255, 242, 153);">{% endif %}</span>
      <span class="tag">&lt;/<span class="keyword">li</span>&gt;</span>
      <span style="background-color:rgb(222, 188, 133);">{% endfor %}</span>
    <span class="tag">&lt;/<span class="keyword">ul</span>&gt;</span>
  <span class="tag">&lt;/<span class="keyword">div</span>&gt;</span>
  <span style="background-color:rgb(164, 192, 228);"><span class="tag">&lt;<span class="keyword">script</span><span class="attribute"> src=<span class="value">"/static/script.js"</span></span>&gt;</span><span></span><span class="tag">&lt;/<span class="keyword">script</span>&gt;</span></span>
<span class="tag">&lt;/<span class="keyword">body</span>&gt;</span>
<span class="tag">&lt;/<span class="keyword">html</span>&gt;</span></pre><p>При нажатии на кнопки на главной странице, будет выполняться JS-код в функциях <i>startInstance()</i> и <i>stopInstance()</i>.</p><p><br></p><h3><h3>Обработка нажатий</h3></h3><p>Функции-обработчики событий нажатия на кнопки Start и Stop будут располагаться в отдельном файле <i>script.js</i>, который мы разместили в папке <i>/static</i>. Их код почти одинаков и отличается только тем, на какую конечную точку функция будет посылать POST-запрос. Переданное в аргументе имя экземпляра терминала подставляется в URL запроса и дальше ожидается получение ответа от нашего же веб-сервера. После получения ответа, он выводится в консоль JavaScript браузера, и текущая страница перезагружается:</p><pre class="code"><span class="comment">/**
* Запуск терминала
* @param {string} name - Имя экземпляра
*/</span>
async function startInstance(name) {
  <span class="comment">// Посылаем запрос по нужному маршруту</span>
  <span class="keyword">const</span> res = await fetch(`/start/${name}`, { method: <span class="string">"POST"</span> });
  <span class="comment">// Получаем ответ</span>
  <span class="keyword">const</span> data = await res.json();
  <span class="comment">// Здесь можно добавить дополнительные действия </span>
  console.<span class="functions">log</span>(data);
  <span class="comment">// Перезагружаем страницу</span>
  location.reload();
}</pre><pre class="code"><span class="comment">/**
* Остановка терминала
* @param {string} name - Имя экземпляра
*/</span>
async function stopInstance(name) {
  <span class="comment">// Посылаем запрос по нужному маршруту</span>
  <span class="keyword">const</span> res = await fetch(`/stop/${name}`, { method: <span class="string">"POST"</span> });
  <span class="comment">// Получаем ответ</span>
  <span class="keyword">const</span> data = await res.json();
  <span class="comment">// Здесь можно добавить дополнительные действия</span>
  console.<span class="functions">log</span>(data);
  <span class="comment">// Перезагружаем страницу</span>
  location.reload();
}</pre><p>В дальнейшем информацию, получаемую в переменной data, можно использовать для показа каких-либо дополнительных сообщений или обновления кода страницы без её полной перезагрузки. Но пока у нас на главной странице выводится очень мало информации, можно её перезагружать полностью. </p><p>Для улучшения внешнего вида главной страницы, добавим немного стилей в файл <i>styles.css</i>, располагающийся в папке <i>/static</i>. Не будем пока подтягивать мощные фреймворки стилизации HTML вроде Bootstrap, мы всегда сможем ими воспользоваться после того, как проект приобретёт более внятные очертания. Пока же мы только нащупываем, как мог бы выглядеть интерфейс взаимодействия с пользователем, и поэтому не стоит сразу уделять большого внимания внешнему виду. </p><p>Но немного — можно! Например, мы добавили к каждому элементу списка дополнительный CSS-класс, совпадающий со статусом экземпляра. А в файле <i>styles.css</i> добавили правила, задающие разный цвет фона для элемента в зависимости от наличия класса <i>running</i>:</p><pre class="code">.instances li.running {
    background: <span class="comment">#d1ffd1;</span>
    border: <span class="number">1</span>px solid <span class="comment">#89fd85;</span>
}</pre><p>Теперь запущенные и незапущенные экземпляры будут хорошо отличимы друг от друга.</p><p><br></p><h3>Тестирование<br></h3><p>Запустим веб-сервер командой из рабочей папки проекта:</p><div class="code"><p>uvicornmain:app--reload--no-use-colors</p></div><p>и откроем в браузере страницу по адресу http://127.0.0.1:8000. </p><p>Пока ни один экземпляр терминала не запущен, мы будем видеть примерно следующее:</p><p style="text-align:center;"><img width="745" height="400" src="../images/9955369d-5348011681570.png" loading="lazy" alt="" style="vertical-align:middle;"></p><p>Нажмём на кнопку Start для первого и второго экземпляра. Спустя мгновение мы увидим, что они перешли в состояние выполняющихся, что подтверждается Диспетчером задач Windows, где мы видим два процесса с такими же идентификаторами, как и на главной странице нашего веб-интерфейса:</p><p style="text-align:center;"><img width="745" height="400" src="../images/5dd3f466-3582312767553.png" loading="lazy" alt="" style="vertical-align:middle;"></p><p>Также можно проверить, что система автоматической документации подхватила сделанные нами комментарии и уточнения про маршруты и их параметры:</p><p style="text-align:center;"><img width="745" height="600" src="../images/2589a28e-3481539276901.png" loading="lazy" alt="" style="vertical-align:middle;"></p><p>Как видно, для маршрута POST /start/{name} есть описание как самого маршрута, так и параметра {name} с примером возможного значения. </p><p><br></p><p></p><h3>Заключение<br></h3><p></p><p>На данном этапе проект MetaTrader 5 Manager превратился из идеи и простого прототипа в полноценное приложение, которое уже способно управлять несколькими экземплярами торговых терминалов MetaTrader 5 через удобный веб-интерфейс. Мы шаг за шагом отделили логику работы с процессами от серверной части, добавили маршруты для управления экземплярами, реализовали простую визуализацию состояния терминалов и сделали первые шаги к структурированию кода проекта.</p><p>Несмотря на то, что текущее решение ещё далеко от окончательного, именно в таком виде оно уже выполняет свою основную задачу — позволяет централизованно запускать и останавливать терминалы, а также получать информацию об их текущем состоянии. Таким образом, у нас появился прочный фундамент, на котором можно строить более сложные уровни функциональности.</p><p>В дальнейшем мы будем развивать проект в нескольких направлениях:</p><ul><li>автоматизация регистрации новых экземпляров терминалов и хранение информации о них в базе данных;</li><li>расширение взаимодействия с Python MetaTrader 5 API для полу
</div>