2026-07-25 19:24:56 +03:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| ServiceStateWriter.mq5 |
|
|
|
|
|
//| Менеджер терминалов MetaTrader 5, ч. 4 |
|
|
|
|
|
//| |
|
|
|
|
|
//| Сервис периодически записывает состояние своего экземпляра |
|
|
|
|
|
//| терминала и подключённого торгового счёта в общую базу SQLite, |
|
|
|
|
|
//| расположенную в папке Common. Веб-приложение менеджера читает |
|
|
|
|
|
//| эти данные из базы, не обращаясь к терминалам напрямую. |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
#property service
|
2026-07-25 20:52:51 +03:00
|
|
|
#property version "1.00"
|
2026-07-25 19:24:56 +03:00
|
|
|
#property description "Запись состояния терминала в общую базу SQLite"
|
|
|
|
|
|
|
|
|
|
//--- Входные параметры экземпляра сервиса
|
|
|
|
|
input string InpDatabase = "mt5-manager.sqlite"; // Имя файла базы данных (папка Common)
|
|
|
|
|
input int InpIntervalSec = 2; // Интервал записи, секунд
|
|
|
|
|
input int InpJitterMs = 1000; // Разброс интервала, мс (0 - выключить)
|
|
|
|
|
input int InpBusyTimeout = 3000; // Таймаут занятости базы, мс
|
|
|
|
|
|
|
|
|
|
//--- Глобальные переменные
|
|
|
|
|
int g_db = INVALID_HANDLE; // Хэндл базы данных
|
|
|
|
|
int g_terminal_id = -1; // Идентификатор записи этого терминала
|
|
|
|
|
int g_skipped = 0; // Счётчик пропущенных записей
|
|
|
|
|
|
|
|
|
|
// DatabaseRead() на запросе без результата (INSERT/UPDATE) завершается
|
|
|
|
|
// этим кодом - это конец выборки, а не ошибка. Проверено на практике.
|
|
|
|
|
#define ERR_DB_NO_MORE_DATA 5126
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Ищет последнее вхождение подстроки (в MQL5 нет StringFindLast) |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
int FindLast(const string text, const string what)
|
|
|
|
|
{
|
|
|
|
|
int pos = -1;
|
|
|
|
|
int next = StringFind(text, what, 0);
|
|
|
|
|
while(next >= 0)
|
|
|
|
|
{
|
|
|
|
|
pos = next;
|
|
|
|
|
next = StringFind(text, what, next + 1);
|
|
|
|
|
}
|
|
|
|
|
return pos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Возвращает имя папки экземпляра терминала |
|
|
|
|
|
//| В portable-режиме это последний компонент пути запуска, |
|
|
|
|
|
//| например "MetaTrader5.1" для C:/MT5/MetaTrader5.1 |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
string InstanceFolder()
|
|
|
|
|
{
|
|
|
|
|
string path = TerminalInfoString(TERMINAL_PATH);
|
|
|
|
|
StringReplace(path, "\\", "/");
|
|
|
|
|
|
|
|
|
|
// Отсекаем завершающий слеш, если он есть
|
|
|
|
|
int len = StringLen(path);
|
|
|
|
|
if(len > 0 && StringGetCharacter(path, len - 1) == '/')
|
|
|
|
|
path = StringSubstr(path, 0, len - 1);
|
|
|
|
|
|
|
|
|
|
int pos = FindLast(path, "/");
|
|
|
|
|
return (pos >= 0) ? StringSubstr(path, pos + 1) : path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Выполняет PRAGMA, возвращающий строку результата |
|
|
|
|
|
//| DatabaseExecute() на таких запросах может вернуть ошибку, |
|
|
|
|
|
//| поэтому используем подготовленный запрос с чтением результата |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool ExecutePragma(const int db, const string sql)
|
|
|
|
|
{
|
|
|
|
|
int request = DatabasePrepare(db, sql);
|
|
|
|
|
if(request == INVALID_HANDLE)
|
|
|
|
|
{
|
|
|
|
|
PrintFormat("Не удалось подготовить запрос '%s', код ошибки: %d",
|
|
|
|
|
sql, GetLastError());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Забираем возвращённую строку, если она есть
|
|
|
|
|
DatabaseRead(request);
|
|
|
|
|
DatabaseFinalize(request);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Создаёт таблицы, если их ещё нет |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool EnsureSchema(const int db)
|
|
|
|
|
{
|
|
|
|
|
const string sql_terminals =
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS terminals ("
|
|
|
|
|
" id INTEGER PRIMARY KEY,"
|
|
|
|
|
" folder TEXT NOT NULL UNIQUE,"
|
|
|
|
|
" path TEXT,"
|
|
|
|
|
" company TEXT,"
|
|
|
|
|
" build INTEGER,"
|
|
|
|
|
" first_seen INTEGER,"
|
|
|
|
|
" last_seen INTEGER)";
|
|
|
|
|
|
|
|
|
|
const string sql_states =
|
|
|
|
|
"CREATE TABLE IF NOT EXISTS states ("
|
|
|
|
|
" terminal_id INTEGER PRIMARY KEY REFERENCES terminals(id),"
|
|
|
|
|
" updated_at INTEGER NOT NULL,"
|
|
|
|
|
" connected INTEGER,"
|
|
|
|
|
" ping_last INTEGER,"
|
|
|
|
|
" login INTEGER,"
|
|
|
|
|
" server TEXT,"
|
|
|
|
|
" currency TEXT,"
|
|
|
|
|
" leverage INTEGER,"
|
|
|
|
|
" trade_mode INTEGER,"
|
|
|
|
|
" trade_allowed INTEGER,"
|
|
|
|
|
" balance REAL,"
|
|
|
|
|
" equity REAL,"
|
|
|
|
|
" profit REAL,"
|
|
|
|
|
" margin REAL,"
|
|
|
|
|
" margin_free REAL,"
|
|
|
|
|
" margin_level REAL,"
|
|
|
|
|
" credit REAL,"
|
|
|
|
|
" positions INTEGER,"
|
2026-07-25 20:52:51 +03:00
|
|
|
" company TEXT,"
|
|
|
|
|
" holder_name TEXT,"
|
2026-07-25 19:24:56 +03:00
|
|
|
" error_code INTEGER DEFAULT 0,"
|
|
|
|
|
" error_text TEXT)";
|
|
|
|
|
|
|
|
|
|
if(!DatabaseExecute(db, sql_terminals))
|
|
|
|
|
{
|
|
|
|
|
PrintFormat("Не удалось создать таблицу terminals, код ошибки: %d", GetLastError());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!DatabaseExecute(db, sql_states))
|
|
|
|
|
{
|
|
|
|
|
PrintFormat("Не удалось создать таблицу states, код ошибки: %d", GetLastError());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Открывает базу и подготавливает соединение |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool OpenDatabase()
|
|
|
|
|
{
|
|
|
|
|
g_db = DatabaseOpen(InpDatabase,
|
|
|
|
|
DATABASE_OPEN_READWRITE
|
|
|
|
|
| DATABASE_OPEN_CREATE
|
|
|
|
|
| DATABASE_OPEN_COMMON);
|
|
|
|
|
|
|
|
|
|
if(g_db == INVALID_HANDLE)
|
|
|
|
|
{
|
|
|
|
|
PrintFormat("Не удалось открыть базу '%s', код ошибки: %d",
|
|
|
|
|
InpDatabase, GetLastError());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Таймаут занятости - свойство соединения, задаём при каждом открытии
|
|
|
|
|
ExecutePragma(g_db, StringFormat("PRAGMA busy_timeout=%d", InpBusyTimeout));
|
|
|
|
|
|
|
|
|
|
// Режим WAL запоминается в заголовке файла базы, но повторная
|
|
|
|
|
// установка безвредна и страхует от создания базы другим клиентом
|
|
|
|
|
ExecutePragma(g_db, "PRAGMA journal_mode=WAL");
|
|
|
|
|
|
|
|
|
|
if(!EnsureSchema(g_db))
|
|
|
|
|
{
|
|
|
|
|
DatabaseClose(g_db);
|
|
|
|
|
g_db = INVALID_HANDLE;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Регистрирует терминал в таблице terminals и возвращает его id |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
int RegisterTerminal()
|
|
|
|
|
{
|
|
|
|
|
const string folder = InstanceFolder();
|
|
|
|
|
const string path = TerminalInfoString(TERMINAL_PATH);
|
|
|
|
|
const string company = TerminalInfoString(TERMINAL_COMPANY);
|
|
|
|
|
const int build = (int)TerminalInfoInteger(TERMINAL_BUILD);
|
2026-07-25 20:52:51 +03:00
|
|
|
// TimeGMT(), не TimeLocal(): последняя возвращает цифры настенных часов,
|
|
|
|
|
// выданные за UTC-эпоху (смещение на часовой пояс машины), из-за чего
|
|
|
|
|
// разъезжается с временем на стороне Python. TimeGMT() - настоящий UTC,
|
|
|
|
|
// и при этом не зависит от времени сервера брокера, как TimeCurrent().
|
|
|
|
|
const long now = (long)TimeGMT();
|
2026-07-25 19:24:56 +03:00
|
|
|
|
|
|
|
|
const string sql =
|
|
|
|
|
"INSERT INTO terminals (folder, path, company, build, first_seen, last_seen) "
|
|
|
|
|
"VALUES (?1, ?2, ?3, ?4, ?5, ?5) "
|
|
|
|
|
"ON CONFLICT(folder) DO UPDATE SET "
|
|
|
|
|
" path = excluded.path,"
|
|
|
|
|
" company = excluded.company,"
|
|
|
|
|
" build = excluded.build,"
|
|
|
|
|
" last_seen = excluded.last_seen";
|
|
|
|
|
|
|
|
|
|
int request = DatabasePrepare(g_db, sql);
|
|
|
|
|
if(request == INVALID_HANDLE)
|
|
|
|
|
{
|
|
|
|
|
PrintFormat("Не удалось подготовить регистрацию терминала, код ошибки: %d",
|
|
|
|
|
GetLastError());
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DatabaseBind(request, 0, folder);
|
|
|
|
|
DatabaseBind(request, 1, path);
|
|
|
|
|
DatabaseBind(request, 2, company);
|
|
|
|
|
DatabaseBind(request, 3, build);
|
|
|
|
|
DatabaseBind(request, 4, now);
|
|
|
|
|
|
|
|
|
|
ResetLastError();
|
|
|
|
|
DatabaseRead(request);
|
|
|
|
|
const int error = GetLastError();
|
|
|
|
|
DatabaseFinalize(request);
|
|
|
|
|
|
|
|
|
|
if(error != 0 && error != ERR_DB_NO_MORE_DATA)
|
|
|
|
|
{
|
|
|
|
|
PrintFormat("Не удалось зарегистрировать терминал '%s', код ошибки: %d",
|
|
|
|
|
folder, error);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Забираем присвоенный идентификатор
|
|
|
|
|
int id = -1;
|
|
|
|
|
request = DatabasePrepare(g_db, "SELECT id FROM terminals WHERE folder = ?1");
|
|
|
|
|
if(request != INVALID_HANDLE)
|
|
|
|
|
{
|
|
|
|
|
DatabaseBind(request, 0, folder);
|
|
|
|
|
if(DatabaseRead(request))
|
|
|
|
|
DatabaseColumnInteger(request, 0, id);
|
|
|
|
|
DatabaseFinalize(request);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(id < 0)
|
|
|
|
|
PrintFormat("Не удалось получить идентификатор терминала '%s'", folder);
|
|
|
|
|
else
|
|
|
|
|
PrintFormat("Терминал '%s' зарегистрирован с id=%d", folder, id);
|
|
|
|
|
|
|
|
|
|
return id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Собирает состояние и записывает его в таблицу states |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
bool StoreState()
|
|
|
|
|
{
|
|
|
|
|
//--- Состояние терминала
|
|
|
|
|
const bool connected = (bool)TerminalInfoInteger(TERMINAL_CONNECTED);
|
|
|
|
|
const int ping_last = (int)TerminalInfoInteger(TERMINAL_PING_LAST);
|
|
|
|
|
|
|
|
|
|
//--- Состояние торгового счёта
|
|
|
|
|
const long login = AccountInfoInteger(ACCOUNT_LOGIN);
|
|
|
|
|
const string server = AccountInfoString(ACCOUNT_SERVER);
|
|
|
|
|
const string currency = AccountInfoString(ACCOUNT_CURRENCY);
|
|
|
|
|
const int leverage = (int)AccountInfoInteger(ACCOUNT_LEVERAGE);
|
|
|
|
|
const int trade_mode = (int)AccountInfoInteger(ACCOUNT_TRADE_MODE);
|
|
|
|
|
const int trade_allowed = (int)AccountInfoInteger(ACCOUNT_TRADE_ALLOWED);
|
|
|
|
|
|
|
|
|
|
const double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
|
|
|
const double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
|
|
|
const double profit = AccountInfoDouble(ACCOUNT_PROFIT);
|
|
|
|
|
const double margin = AccountInfoDouble(ACCOUNT_MARGIN);
|
|
|
|
|
const double margin_free = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
|
|
|
|
const double margin_level = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
|
|
|
|
|
const double credit = AccountInfoDouble(ACCOUNT_CREDIT);
|
|
|
|
|
|
|
|
|
|
const int positions = PositionsTotal();
|
2026-07-25 20:52:51 +03:00
|
|
|
const long updated_at = (long)TimeGMT();
|
2026-07-25 19:24:56 +03:00
|
|
|
|
2026-07-25 20:52:51 +03:00
|
|
|
//--- Данные о держателе счёта, для отображения в интерфейсе
|
|
|
|
|
const string account_company = AccountInfoString(ACCOUNT_COMPANY);
|
|
|
|
|
const string holder_name = AccountInfoString(ACCOUNT_NAME);
|
|
|
|
|
|
|
|
|
|
//--- Признак готовности данных. 1/-2 продолжают ряд кодов менеджера
|
|
|
|
|
// (-1 - "Manager: Terminal not found"), префикс в тексте показывает
|
|
|
|
|
// источник жалобы
|
|
|
|
|
const int error_code = connected ? 1 : -2;
|
|
|
|
|
const string error_text = connected ? "Success" : "Service: No connection to trade server";
|
2026-07-25 19:24:56 +03:00
|
|
|
|
|
|
|
|
const string sql =
|
|
|
|
|
"INSERT INTO states ("
|
|
|
|
|
" terminal_id, updated_at, connected, ping_last, login, server, currency,"
|
|
|
|
|
" leverage, trade_mode, trade_allowed, balance, equity, profit, margin,"
|
2026-07-25 20:52:51 +03:00
|
|
|
" margin_free, margin_level, credit, positions, company, holder_name,"
|
|
|
|
|
" error_code, error_text) "
|
|
|
|
|
"VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22) "
|
2026-07-25 19:24:56 +03:00
|
|
|
"ON CONFLICT(terminal_id) DO UPDATE SET "
|
|
|
|
|
" updated_at = excluded.updated_at,"
|
|
|
|
|
" connected = excluded.connected,"
|
|
|
|
|
" ping_last = excluded.ping_last,"
|
|
|
|
|
" login = excluded.login,"
|
|
|
|
|
" server = excluded.server,"
|
|
|
|
|
" currency = excluded.currency,"
|
|
|
|
|
" leverage = excluded.leverage,"
|
|
|
|
|
" trade_mode = excluded.trade_mode,"
|
|
|
|
|
" trade_allowed = excluded.trade_allowed,"
|
|
|
|
|
" balance = excluded.balance,"
|
|
|
|
|
" equity = excluded.equity,"
|
|
|
|
|
" profit = excluded.profit,"
|
|
|
|
|
" margin = excluded.margin,"
|
|
|
|
|
" margin_free = excluded.margin_free,"
|
|
|
|
|
" margin_level = excluded.margin_level,"
|
|
|
|
|
" credit = excluded.credit,"
|
|
|
|
|
" positions = excluded.positions,"
|
2026-07-25 20:52:51 +03:00
|
|
|
" company = excluded.company,"
|
|
|
|
|
" holder_name = excluded.holder_name,"
|
2026-07-25 19:24:56 +03:00
|
|
|
" error_code = excluded.error_code,"
|
|
|
|
|
" error_text = excluded.error_text";
|
|
|
|
|
|
|
|
|
|
int request = DatabasePrepare(g_db, sql);
|
|
|
|
|
if(request == INVALID_HANDLE)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
DatabaseBind(request, 0, g_terminal_id);
|
|
|
|
|
DatabaseBind(request, 1, updated_at);
|
|
|
|
|
DatabaseBind(request, 2, connected ? 1 : 0);
|
|
|
|
|
DatabaseBind(request, 3, ping_last);
|
|
|
|
|
DatabaseBind(request, 4, login);
|
|
|
|
|
DatabaseBind(request, 5, server);
|
|
|
|
|
DatabaseBind(request, 6, currency);
|
|
|
|
|
DatabaseBind(request, 7, leverage);
|
|
|
|
|
DatabaseBind(request, 8, trade_mode);
|
|
|
|
|
DatabaseBind(request, 9, trade_allowed);
|
|
|
|
|
DatabaseBind(request, 10, balance);
|
|
|
|
|
DatabaseBind(request, 11, equity);
|
|
|
|
|
DatabaseBind(request, 12, profit);
|
|
|
|
|
DatabaseBind(request, 13, margin);
|
|
|
|
|
DatabaseBind(request, 14, margin_free);
|
|
|
|
|
DatabaseBind(request, 15, margin_level);
|
|
|
|
|
DatabaseBind(request, 16, credit);
|
|
|
|
|
DatabaseBind(request, 17, positions);
|
2026-07-25 20:52:51 +03:00
|
|
|
DatabaseBind(request, 18, account_company);
|
|
|
|
|
DatabaseBind(request, 19, holder_name);
|
|
|
|
|
DatabaseBind(request, 20, error_code);
|
|
|
|
|
DatabaseBind(request, 21, error_text);
|
2026-07-25 19:24:56 +03:00
|
|
|
|
|
|
|
|
ResetLastError();
|
|
|
|
|
|
|
|
|
|
// Оборачиваем запись в явную транзакцию: без неё каждая операция
|
|
|
|
|
// становится отдельной транзакцией со сбросом на диск
|
|
|
|
|
if(!DatabaseTransactionBegin(g_db))
|
|
|
|
|
{
|
|
|
|
|
DatabaseFinalize(request);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DatabaseRead(request);
|
|
|
|
|
const int error = GetLastError();
|
|
|
|
|
DatabaseFinalize(request);
|
|
|
|
|
|
|
|
|
|
// Код ERR_DB_NO_MORE_DATA соответствует завершению выборки и ошибкой не является
|
|
|
|
|
if(error != 0 && error != ERR_DB_NO_MORE_DATA)
|
|
|
|
|
{
|
|
|
|
|
DatabaseTransactionRollback(g_db);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!DatabaseTransactionCommit(g_db))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Точка входа сервиса |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
void OnStart()
|
|
|
|
|
{
|
|
|
|
|
PrintFormat("Запуск сервиса для экземпляра '%s'", InstanceFolder());
|
|
|
|
|
|
|
|
|
|
// Разброс интервала, чтобы записи разных сервисов не совпадали по фазе
|
|
|
|
|
MathSrand((int)TimeLocal() + (int)GetTickCount());
|
|
|
|
|
|
|
|
|
|
const int interval_ms = InpIntervalSec * 1000;
|
|
|
|
|
|
|
|
|
|
while(!IsStopped())
|
|
|
|
|
{
|
|
|
|
|
// Подключение к базе - при неудаче пробуем снова на следующем витке
|
|
|
|
|
if(g_db == INVALID_HANDLE)
|
|
|
|
|
{
|
|
|
|
|
if(!OpenDatabase())
|
|
|
|
|
{
|
|
|
|
|
Sleep(interval_ms);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
g_terminal_id = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Регистрация терминала - тоже с повтором при неудаче
|
|
|
|
|
if(g_terminal_id < 0)
|
|
|
|
|
{
|
|
|
|
|
g_terminal_id = RegisterTerminal();
|
|
|
|
|
if(g_terminal_id < 0)
|
|
|
|
|
{
|
|
|
|
|
Sleep(interval_ms);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Основная работа
|
|
|
|
|
if(!StoreState())
|
|
|
|
|
{
|
|
|
|
|
g_skipped++;
|
|
|
|
|
if(g_skipped % 10 == 0)
|
|
|
|
|
PrintFormat("Пропущено записей: %d, последняя ошибка: %d",
|
|
|
|
|
g_skipped, GetLastError());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Пауза с разбросом
|
|
|
|
|
int pause = interval_ms;
|
|
|
|
|
if(InpJitterMs > 0)
|
|
|
|
|
pause += (int)(MathRand() % InpJitterMs);
|
|
|
|
|
Sleep(pause);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(g_db != INVALID_HANDLE)
|
|
|
|
|
DatabaseClose(g_db);
|
|
|
|
|
|
|
|
|
|
PrintFormat("Остановка сервиса, всего пропущено записей: %d", g_skipped);
|
|
|
|
|
}
|
|
|
|
|
//+------------------------------------------------------------------+
|