bifurcado de antekov/mt5-manager
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.
'''Проверка создания настоящего 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())
|