mt5-manager/tools/smoke_create_terminal.py
Yuriy Bykov Yuriy Bykov 779efef59f Improve terminal settings and operation notifications
Separate configuration saving from connection testing, preserve terminal process state after tests, and support deleting stopped instances.

Simplify account fields and optional servers.dat uploads. Show operation progress per terminal and dismiss results after five seconds with a circular countdown.

Update documentation and tests for the revised behavior.
2026-11-01 19:40:01 +03:00
115 líneas
5,5 KiB
Python
'''Проверка создания настоящего MT5 без счёта в изолированной папке.
Берёт файлы из первого терминала. После проверки удаляет только собственные
временные папки и отдельную тестовую базу Common/Files.
'''
import argparse
import getpass
import json
import shutil
import sys
import time
import uuid
from pathlib import Path
import psutil
PROJECT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT))
from config import ConfigStore
from terminal_manager import TerminalManager
class DiagnosticManager(TerminalManager):
def wait_state(self, folder, since, account=None):
try:
return super().wait_state(folder, since, account)
except RuntimeError:
row = self.control.read_state(folder)
if row is not None:
print('Last service state:', json.dumps({key: row[key] for key in
('updated_at', 'connected', 'login', 'server', 'terminal_path')}, ensure_ascii=False), flush=True)
log_dir = Path(self.control.instance_path(folder)).parent / 'Logs'
for path in sorted(log_dir.glob('*.log')):
if not path.stem.isdecimal():
continue
for line in path.read_text(encoding='utf-16', errors='replace').splitlines():
if ('\tNetwork\t' in line or '\tStartup\t' in line) and 'previous successful' not in line:
print(line, flush=True)
raise
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--with-account', action='store_true',
help='Запросить реквизиты счёта при запуске (пароль вводится скрыто)')
parser.add_argument('--login', type=int, help='Логин для --with-account')
parser.add_argument('--server', help='Торговый сервер для --with-account')
parser.add_argument('--timeout', type=int, default=180)
parser.add_argument('--servers', type=Path, help='Необязательный servers.dat для тестового экземпляра')
args = parser.parse_args()
if args.servers is not None and (not args.servers.is_file() or not 0 < args.servers.stat().st_size <= 16 * 1024 * 1024):
parser.error('Укажите непустой servers.dat размером не более 16 МиБ.')
account = {}
if args.with_account:
try:
login = args.login or int(input('Логин счёта: ').strip())
server = args.server or input('Торговый сервер: ').strip()
password = getpass.getpass('Пароль счёта (не отображается): ')
except (ValueError, EOFError):
parser.error('Не удалось прочитать реквизиты счёта.')
if login <= 0 or not server or not password:
parser.error('Укажите положительный логин, сервер и непустой пароль.')
account = {'login': login, 'server': server, 'password': password}
elif args.login is not None or args.server:
parser.error('--login и --server используются вместе с --with-account.')
token = uuid.uuid4().hex[:8]
if args.servers is not None:
account['_servers_dat'] = args.servers.read_bytes()
check_root = (PROJECT / '.checks' / token).resolve()
check_root.mkdir(parents=True)
common = Path(json.loads((PROJECT / 'config.json').read_text(encoding='utf-8-sig'))['database']).parent
database = common / f'manager-article5-check-{token}.sqlite'
path = check_root / 'config.json'
path.write_text(json.dumps({'terminals': {}, 'mt5_folder': str(check_root / 'instances'),
'mt5_exe': 'terminal64.exe', 'database': str(database),
'terminal_template': r'C:\MT5\MetaTrader5.1',
'startup_timeout_sec': args.timeout}), encoding='utf-8')
manager = DiagnosticManager(ConfigStore(path))
try:
job = manager.submit('create', f'Article5Check-{token}', account)
previous = None
while True:
job = manager.job(job['id'])
if job['stage'] != previous:
print(job['stage'], flush=True)
previous = job['stage']
if job['status'] in ('done', 'error'):
break
time.sleep(1)
print(json.dumps(job, ensure_ascii=False), flush=True)
if job['status'] != 'done':
return 1
info = manager.info(job['folder'])
assert info['pid'] and info['status'] == 'running', info
print('Real terminal process and service heartbeat: OK', flush=True)
manager.stop(job['folder'])
return 0
finally:
manager.close()
for process in psutil.process_iter(['exe']):
try:
if process.info['exe'] and Path(process.info['exe']).resolve().is_relative_to(check_root):
process.terminate()
process.wait(10)
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
assert check_root.parent == (PROJECT / '.checks').resolve()
shutil.rmtree(check_root)
for suffix in ('', '-wal', '-shm'):
Path(str(database) + suffix).unlink(missing_ok=True)
if __name__ == '__main__':
raise SystemExit(main())