Compare commits
3 Commits
2026.07.29
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b6515795d | |||
| 5410aa4fb4 | |||
| 113871e82f |
@ -18,7 +18,9 @@ let common = {
|
|||||||
//Таймаут останова сервера (мс)
|
//Таймаут останова сервера (мс)
|
||||||
nTerminateTimeout: 60000,
|
nTerminateTimeout: 60000,
|
||||||
//Контролировать версию Системы
|
//Контролировать версию Системы
|
||||||
bControlSystemVersion: true
|
bControlSystemVersion: true,
|
||||||
|
//Директория размещения логов (не указано / пусто - протоколирование в файл не ведётся)
|
||||||
|
sLogPath: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
//Параметры подключения к БД
|
//Параметры подключения к БД
|
||||||
|
|||||||
28
core/app.js
28
core/app.js
@ -42,6 +42,8 @@ class ParusAppServer {
|
|||||||
this.bStopping = false;
|
this.bStopping = false;
|
||||||
//Таймаут останова сервера
|
//Таймаут останова сервера
|
||||||
this.terminateTimeout = null;
|
this.terminateTimeout = null;
|
||||||
|
//Код завершения процесса при завершении каскада останова (0 - естественное завершение)
|
||||||
|
this.nStopExitCode = 0;
|
||||||
//Список обслуживаемых сервисов
|
//Список обслуживаемых сервисов
|
||||||
this.services = [];
|
this.services = [];
|
||||||
//Привяжем методы к указателю на себя для использования в обработчиках событий
|
//Привяжем методы к указателю на себя для использования в обработчиках событий
|
||||||
@ -180,20 +182,32 @@ class ParusAppServer {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.logger.error(`Ошибка отключения от БД: ${e.sCode}: ${e.sMessage}`);
|
await this.logger.error(`Ошибка отключения от БД: ${e.sCode}: ${e.sMessage}`);
|
||||||
}
|
}
|
||||||
//Мы закончили останов - сброс таймера аварийного останова, процесс завершится самостоятельно
|
|
||||||
if (this.terminateTimeout) {
|
|
||||||
clearTimeout(this.terminateTimeout);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//Сообщим об останове (попадёт в файл до закрытия)
|
||||||
|
await this.logger.notifyServerStopped(this.nStopExitCode);
|
||||||
|
//Закрываем файл лога
|
||||||
|
await this.logger.closeLogFile();
|
||||||
|
//Мы закончили останов - сброс таймера аварийного останова
|
||||||
|
if (this.terminateTimeout) {
|
||||||
|
clearTimeout(this.terminateTimeout);
|
||||||
|
this.terminateTimeout = null;
|
||||||
|
}
|
||||||
|
//Принудительный выход только при ненулевом коде (ошибка старта и т.п.)
|
||||||
|
if (this.nStopExitCode) process.exit(this.nStopExitCode);
|
||||||
}
|
}
|
||||||
//Инициализация сервера
|
//Инициализация сервера
|
||||||
async init(prms) {
|
async init(prms) {
|
||||||
await this.logger.info("Инициализация сервера приложений...");
|
|
||||||
//Проверяем структуру переданного объекта конфигурации
|
//Проверяем структуру переданного объекта конфигурации
|
||||||
let sCheckResult = validateObject(prms, prmsAppSchema.init, "Параметры инициализации");
|
let sCheckResult = validateObject(prms, prmsAppSchema.init, "Параметры инициализации");
|
||||||
//Если настройки верны - будем стартовать
|
//Если настройки верны - будем стартовать
|
||||||
if (!sCheckResult) {
|
if (!sCheckResult) {
|
||||||
|
//Открываем файл лога при необходимости
|
||||||
|
this.logger.openLogFile(prms.config.common.sLogPath);
|
||||||
|
//Если файл лога открыт - отразим это только в консоли
|
||||||
|
this.logger.notifyFileLogConsole();
|
||||||
|
//Сообщим о начале инициализации
|
||||||
|
await this.logger.info("Инициализация сервера приложений...");
|
||||||
//Протоколируем версию и релиз
|
//Протоколируем версию и релиз
|
||||||
await this.logger.info(`Версия сервера приложений: ${prms.config.common.sVersion}, релиз: ${prms.config.common.sRelease}`);
|
await this.logger.info(`Версия сервера приложений: ${prms.config.common.sVersion}, релиз: ${prms.config.common.sRelease}`);
|
||||||
//Создаём подключение к БД
|
//Создаём подключение к БД
|
||||||
@ -278,12 +292,14 @@ class ParusAppServer {
|
|||||||
await this.dbConn.connect();
|
await this.dbConn.connect();
|
||||||
}
|
}
|
||||||
//Останов сервера
|
//Останов сервера
|
||||||
async stop(terminateTimeout) {
|
async stop(terminateTimeout, nExitCode) {
|
||||||
if (!this.bStopping) {
|
if (!this.bStopping) {
|
||||||
//Установим флаг - остановка в процессе
|
//Установим флаг - остановка в процессе
|
||||||
this.bStopping = true;
|
this.bStopping = true;
|
||||||
//Запомним таймер аварийного останова
|
//Запомним таймер аварийного останова
|
||||||
this.terminateTimeout = terminateTimeout;
|
this.terminateTimeout = terminateTimeout;
|
||||||
|
//Код завершения после каскада останова (по умолчанию - естественное завершение)
|
||||||
|
this.nStopExitCode = typeof nExitCode === "number" ? nExitCode : 0;
|
||||||
//Сообщаем, что начался останов сервера
|
//Сообщаем, что начался останов сервера
|
||||||
await this.logger.warn("Останов сервера приложений...");
|
await this.logger.warn("Останов сервера приложений...");
|
||||||
//Останов обслуживания очереди исходящих
|
//Останов обслуживания очереди исходящих
|
||||||
|
|||||||
@ -52,3 +52,9 @@ exports.SINFO_MQTT = "INFO_MQTT"; //Предупреждение
|
|||||||
exports.SCONSOLE_LOG_COLOR_PATTERN_ERR = "\x1b[31m%s\x1b[0m%s"; //Цвет для ошибок
|
exports.SCONSOLE_LOG_COLOR_PATTERN_ERR = "\x1b[31m%s\x1b[0m%s"; //Цвет для ошибок
|
||||||
exports.SCONSOLE_LOG_COLOR_PATTERN_WRN = "\x1b[33m%s\x1b[0m%s"; //Цвет для предупреждений
|
exports.SCONSOLE_LOG_COLOR_PATTERN_WRN = "\x1b[33m%s\x1b[0m%s"; //Цвет для предупреждений
|
||||||
exports.SCONSOLE_LOG_COLOR_PATTERN_INF = "\x1b[32m%s\x1b[0m%s"; //Цвет для информации
|
exports.SCONSOLE_LOG_COLOR_PATTERN_INF = "\x1b[32m%s\x1b[0m%s"; //Цвет для информации
|
||||||
|
|
||||||
|
//Префиксы строк протокола
|
||||||
|
exports.SLOG_PREFIX_INF = "ИНФОРМАЦИЯ";
|
||||||
|
exports.SLOG_PREFIX_WRN = "ПРЕДУПРЕЖДЕНИЕ";
|
||||||
|
exports.SLOG_PREFIX_ERR = "ОШИБКА";
|
||||||
|
exports.SLOG_PREFIX_PROTOCOL_ERR = "ОШИБКА ПРОТОКОЛИРОВАНИЯ";
|
||||||
|
|||||||
364
core/log_file_writer.js
Normal file
364
core/log_file_writer.js
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
/*
|
||||||
|
Сервис интеграции ПП Парус 8 с WEB API
|
||||||
|
Модуль ядра: запись протокола работы в файл
|
||||||
|
*/
|
||||||
|
|
||||||
|
//----------------------
|
||||||
|
// Подключение библиотек
|
||||||
|
//----------------------
|
||||||
|
|
||||||
|
const fs = require("fs"); //Файловая система
|
||||||
|
const path = require("path"); //Пути файловой системы
|
||||||
|
const { validateObject, getNowString, makeErrorText } = require("./utils"); //Вспомогательные функции
|
||||||
|
const { ServerError } = require("./server_errors"); //Типовая ошибка
|
||||||
|
const {
|
||||||
|
SCONSOLE_LOG_COLOR_PATTERN_ERR,
|
||||||
|
SCONSOLE_LOG_COLOR_PATTERN_INF,
|
||||||
|
SERR_COMMON,
|
||||||
|
SLOG_PREFIX_INF,
|
||||||
|
SLOG_PREFIX_ERR,
|
||||||
|
SLOG_PREFIX_PROTOCOL_ERR
|
||||||
|
} = require("./constants"); //Общие константы
|
||||||
|
const prmsLogFileWriterSchema = require("../models/prms_log_file_writer"); //Схемы валидации параметров функций модуля
|
||||||
|
|
||||||
|
//--------------------------
|
||||||
|
// Глобальные идентификаторы
|
||||||
|
//--------------------------
|
||||||
|
|
||||||
|
//Таймаут ожидания опустошения очереди записи в файл при закрытии (мс)
|
||||||
|
const NLOG_FILE_CLOSE_TIMEOUT = 10000;
|
||||||
|
|
||||||
|
//Интервал повторного сообщения об ошибке записи в файл (мс)
|
||||||
|
const NLOG_FILE_ERROR_REPORT_INTERVAL = 10000;
|
||||||
|
|
||||||
|
//Интервал повторного открытия файла лога после ошибки (мс)
|
||||||
|
const NLOG_FILE_REOPEN_INTERVAL = 10000;
|
||||||
|
|
||||||
|
//Максимум строк в очереди записи (защита памяти при высокой нагрузке / медленном диске)
|
||||||
|
const NLOG_FILE_MAX_PENDING = 2000;
|
||||||
|
|
||||||
|
//------------
|
||||||
|
// Тело модуля
|
||||||
|
//------------
|
||||||
|
|
||||||
|
//Дополнение числа ведущим нулём до двух знаков
|
||||||
|
const pad2 = nVal => String(nVal).padStart(2, "0");
|
||||||
|
|
||||||
|
//Формирование имени файла лога: log_yyyymmdd_hh24miss.log
|
||||||
|
const buildLogFileName = () => {
|
||||||
|
const dNow = new Date();
|
||||||
|
const sDate = `${dNow.getFullYear()}${pad2(dNow.getMonth() + 1)}${pad2(dNow.getDate())}`;
|
||||||
|
const sTime = `${pad2(dNow.getHours())}${pad2(dNow.getMinutes())}${pad2(dNow.getSeconds())}`;
|
||||||
|
return `log_${sDate}_${sTime}.log`;
|
||||||
|
};
|
||||||
|
|
||||||
|
//Проверка, что каталог существует и доступен для записи
|
||||||
|
const checkLogDirWritable = sPath => {
|
||||||
|
try {
|
||||||
|
//Каталог должен существовать
|
||||||
|
if (!fs.existsSync(sPath) || !fs.statSync(sPath).isDirectory()) return false;
|
||||||
|
//Права на запись
|
||||||
|
fs.accessSync(sPath, fs.constants.W_OK);
|
||||||
|
//Пробная запись
|
||||||
|
const sProbe = path.join(sPath, `.exs_log_write_probe_${process.pid}`);
|
||||||
|
fs.writeFileSync(sProbe, "");
|
||||||
|
fs.unlinkSync(sProbe);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Признак, что поток записи в файл непригоден
|
||||||
|
const isLogStreamBroken = stream => !stream || stream.destroyed || !stream.writable;
|
||||||
|
|
||||||
|
//Гарантия перевода строки в конце текста
|
||||||
|
const ensureLogLineEnd = sLine => (sLine.endsWith("\n") ? sLine : `${sLine}\n`);
|
||||||
|
|
||||||
|
//Безопасное уничтожение потока записи (removeAllListeners + destroy)
|
||||||
|
const destroyLogStream = stream => {
|
||||||
|
if (!stream) return null;
|
||||||
|
try {
|
||||||
|
stream.removeAllListeners("error");
|
||||||
|
if (!stream.destroyed) stream.destroy();
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Класс записи протокола в файл
|
||||||
|
class LogFileWriter {
|
||||||
|
//Конструктор класса
|
||||||
|
constructor() {
|
||||||
|
this.logStream = null;
|
||||||
|
this.sLogFile = null;
|
||||||
|
this.bFileLog = false;
|
||||||
|
this.bClosing = false;
|
||||||
|
this.writeChain = Promise.resolve();
|
||||||
|
this.nQueued = 0;
|
||||||
|
this.nLastFileWriteErrorAt = 0;
|
||||||
|
this.nLastLogFileReopenAt = 0;
|
||||||
|
this.sPendingLogLine = null;
|
||||||
|
}
|
||||||
|
//Признак активного протоколирования в файл
|
||||||
|
isEnabled() {
|
||||||
|
return this.bFileLog === true;
|
||||||
|
}
|
||||||
|
//Сброс состояния писателя (без уничтожения потока)
|
||||||
|
clearState() {
|
||||||
|
this.bFileLog = false;
|
||||||
|
this.bClosing = false;
|
||||||
|
this.logStream = null;
|
||||||
|
this.sLogFile = null;
|
||||||
|
this.writeChain = Promise.resolve();
|
||||||
|
this.nQueued = 0;
|
||||||
|
this.nLastFileWriteErrorAt = 0;
|
||||||
|
this.nLastLogFileReopenAt = 0;
|
||||||
|
this.sPendingLogLine = null;
|
||||||
|
}
|
||||||
|
//Создание потока записи в файл
|
||||||
|
createWriteStream(sFilePath) {
|
||||||
|
const stream = fs.createWriteStream(sFilePath, { flags: "a", encoding: "utf8" });
|
||||||
|
stream.on("error", e => this.handleWriteError(e));
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
//Уничтожение текущего потока записи без сброса пути файла
|
||||||
|
destroyStream() {
|
||||||
|
destroyLogStream(this.logStream);
|
||||||
|
this.logStream = null;
|
||||||
|
}
|
||||||
|
//Уведомление в консоль о протоколировании в файл (без записи в файл)
|
||||||
|
notifyOpenedConsole() {
|
||||||
|
if (!this.isEnabled() || !this.sLogFile) return;
|
||||||
|
const sNow = getNowString();
|
||||||
|
console.log(SCONSOLE_LOG_COLOR_PATTERN_INF, `${sNow} ${SLOG_PREFIX_INF}: `, `Протоколирование в файл: ${this.sLogFile}`);
|
||||||
|
}
|
||||||
|
//Уведомление в консоль о результате закрытия файла лога (без записи в файл)
|
||||||
|
notifyClosedConsole(sLogFile, e) {
|
||||||
|
const sNow = getNowString();
|
||||||
|
if (e) {
|
||||||
|
console.log(
|
||||||
|
SCONSOLE_LOG_COLOR_PATTERN_ERR,
|
||||||
|
`${sNow} ${SLOG_PREFIX_ERR}: `,
|
||||||
|
`Ошибка закрытия файла лога${sLogFile ? ` (${sLogFile})` : ""}: ${makeErrorText(e)}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!sLogFile) return;
|
||||||
|
console.log(SCONSOLE_LOG_COLOR_PATTERN_INF, `${sNow} ${SLOG_PREFIX_INF}: `, `Файл лога закрыт: ${sLogFile}`);
|
||||||
|
}
|
||||||
|
//Сообщение об ошибке записи в файл с ограничением частоты
|
||||||
|
reportWriteError(e) {
|
||||||
|
const nNow = Date.now();
|
||||||
|
//Ограничиваем спам повторных сообщений
|
||||||
|
if (nNow - this.nLastFileWriteErrorAt < NLOG_FILE_ERROR_REPORT_INTERVAL) return;
|
||||||
|
this.nLastFileWriteErrorAt = nNow;
|
||||||
|
const sNow = getNowString();
|
||||||
|
console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${sNow} ${SLOG_PREFIX_PROTOCOL_ERR} В ФАЙЛ: `, makeErrorText(e));
|
||||||
|
}
|
||||||
|
//Обработка ошибки записи в файл лога
|
||||||
|
handleWriteError(e, sFailedLine) {
|
||||||
|
//Если протоколирование в файл не активно - выходим
|
||||||
|
if (!this.bFileLog && !this.sLogFile) return;
|
||||||
|
//Сообщим в консоль (с ограничением спама)
|
||||||
|
this.reportWriteError(e);
|
||||||
|
//Запомним неудачную строку для однократного повтора после переоткрытия
|
||||||
|
if (sFailedLine) this.sPendingLogLine = sFailedLine;
|
||||||
|
//При закрытии не пытаемся восстановить поток - close/release завершат освобождение
|
||||||
|
if (this.bClosing) return;
|
||||||
|
//Повтор только после переоткрытия непригодного потока (без цикла на "живом" потоке)
|
||||||
|
if (isLogStreamBroken(this.logStream)) {
|
||||||
|
if (this.reopen()) this.retryPendingLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Открытие файла лога
|
||||||
|
open(sLogPath) {
|
||||||
|
//Если путь не задан - протоколирование в файл не ведётся
|
||||||
|
if (!sLogPath || !String(sLogPath).trim()) return;
|
||||||
|
//Нормализуем путь
|
||||||
|
const sPath = path.resolve(String(sLogPath).trim());
|
||||||
|
//Проверяем параметры
|
||||||
|
let sCheckResult = validateObject({ sLogPath: sPath }, prmsLogFileWriterSchema.open, "Параметры функции открытия файла лога");
|
||||||
|
if (sCheckResult) throw new ServerError(SERR_COMMON, sCheckResult);
|
||||||
|
//Каталог должен существовать и быть доступен для записи
|
||||||
|
if (!checkLogDirWritable(sPath))
|
||||||
|
throw new ServerError(SERR_COMMON, `Каталог размещения логов не существует или недоступен для записи: ${sPath}`);
|
||||||
|
//Если уже открыт - сначала освободим без сообщения в console
|
||||||
|
if (this.logStream || this.bFileLog) this.release(undefined, true);
|
||||||
|
//Формируем полный путь файла сессии
|
||||||
|
const sFullPath = path.join(sPath, buildLogFileName());
|
||||||
|
//Открываем поток записи
|
||||||
|
this.logStream = this.createWriteStream(sFullPath);
|
||||||
|
this.sLogFile = sFullPath;
|
||||||
|
this.bFileLog = true;
|
||||||
|
}
|
||||||
|
//Повторное открытие текущего файла лога
|
||||||
|
reopen() {
|
||||||
|
//Нет пути файла или идёт закрытие - не переоткрываем
|
||||||
|
if (!this.sLogFile || this.bClosing) return false;
|
||||||
|
//Ограничиваем частоту переоткрытия
|
||||||
|
const nNow = Date.now();
|
||||||
|
if (this.nLastLogFileReopenAt > 0 && nNow - this.nLastLogFileReopenAt < NLOG_FILE_REOPEN_INTERVAL) return false;
|
||||||
|
this.nLastLogFileReopenAt = nNow;
|
||||||
|
try {
|
||||||
|
//Освободим прежний поток, если он ещё есть
|
||||||
|
this.destroyStream();
|
||||||
|
//Откроем тот же файл на дозапись
|
||||||
|
this.logStream = this.createWriteStream(this.sLogFile);
|
||||||
|
this.bFileLog = true;
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
this.logStream = null;
|
||||||
|
this.reportWriteError(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Освобождение ресурсов файла лога
|
||||||
|
release(eNotify, bSilentNotify) {
|
||||||
|
const stream = this.logStream;
|
||||||
|
const sLogFile = this.sLogFile;
|
||||||
|
this.clearState();
|
||||||
|
const eClose = destroyLogStream(stream);
|
||||||
|
//Результат закрытия - только в console
|
||||||
|
if (!bSilentNotify && sLogFile) this.notifyClosedConsole(sLogFile, eNotify !== undefined ? eNotify : eClose);
|
||||||
|
}
|
||||||
|
//Запись одной строки в файл
|
||||||
|
writeLine(sData) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const stream = this.logStream;
|
||||||
|
//Уже поставленные в очередь строки дописываем даже при bClosing (close ждёт writeChain)
|
||||||
|
if (isLogStreamBroken(stream) || !this.bFileLog) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Пишем строку целиком
|
||||||
|
stream.write(sData, err => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//Постановка записи в сериализованную очередь (с ограничением глубины)
|
||||||
|
scheduleWrite(sData) {
|
||||||
|
//Защита от разрастания очереди при отставании диска
|
||||||
|
if (this.nQueued >= NLOG_FILE_MAX_PENDING) {
|
||||||
|
this.reportWriteError(new Error(`Переполнена очередь записи в файл лога (лимит ${NLOG_FILE_MAX_PENDING}), строка пропущена`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.nQueued++;
|
||||||
|
this.writeChain = this.writeChain
|
||||||
|
.then(() => this.writeLine(sData))
|
||||||
|
.catch(e => this.handleWriteError(e, sData))
|
||||||
|
.finally(() => {
|
||||||
|
if (this.nQueued > 0) this.nQueued--;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//Повторная запись строки, не попавшей в файл из-за ошибки
|
||||||
|
retryPendingLine() {
|
||||||
|
//Нечего повторять или нельзя писать
|
||||||
|
if (!this.sPendingLogLine || this.bClosing || isLogStreamBroken(this.logStream)) return;
|
||||||
|
const sData = this.sPendingLogLine;
|
||||||
|
this.sPendingLogLine = null;
|
||||||
|
this.scheduleWrite(sData);
|
||||||
|
}
|
||||||
|
//Постановка строки в очередь записи в файл (без ожидания записи вызывающей стороной)
|
||||||
|
enqueue(sLine) {
|
||||||
|
//Проверяем параметры
|
||||||
|
if (typeof sLine !== "string") return;
|
||||||
|
//Если файл не активен или идёт закрытие - ничего не делаем
|
||||||
|
if (!this.bFileLog || this.bClosing) return;
|
||||||
|
//Если поток потерян - попробуем переоткрыть
|
||||||
|
if (isLogStreamBroken(this.logStream)) {
|
||||||
|
if (!this.reopen()) return;
|
||||||
|
}
|
||||||
|
//Однократный повтор отложенной строки (если была ошибка записи)
|
||||||
|
this.retryPendingLine();
|
||||||
|
if (isLogStreamBroken(this.logStream)) return;
|
||||||
|
//Гарантируем перевод строки и ставим в очередь
|
||||||
|
this.scheduleWrite(ensureLogLineEnd(sLine));
|
||||||
|
}
|
||||||
|
//Синхронная дозапись строки в файл (авария / process.on("exit"))
|
||||||
|
appendSync(sLine) {
|
||||||
|
if (!this.sLogFile || typeof sLine !== "string") return;
|
||||||
|
try {
|
||||||
|
if (this.logStream) {
|
||||||
|
this.destroyStream();
|
||||||
|
this.bFileLog = false;
|
||||||
|
}
|
||||||
|
fs.appendFileSync(this.sLogFile, ensureLogLineEnd(sLine), "utf8");
|
||||||
|
} catch (e) {
|
||||||
|
this.reportWriteError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Корректное закрытие файла лога
|
||||||
|
async close() {
|
||||||
|
//Если файла нет - нечего закрывать
|
||||||
|
if (!this.logStream && !this.bFileLog && !this.sLogFile) return;
|
||||||
|
//Фиксируем режим закрытия (новые строки и reopen не принимаем)
|
||||||
|
this.bClosing = true;
|
||||||
|
this.sPendingLogLine = null;
|
||||||
|
const sLogFile = this.sLogFile;
|
||||||
|
let nDrainTimer = null;
|
||||||
|
try {
|
||||||
|
//Дождёмся опустошения очереди записи (с ограничением по времени)
|
||||||
|
const bDrained = await Promise.race([
|
||||||
|
this.writeChain.then(() => true).catch(() => true),
|
||||||
|
new Promise(resolve => {
|
||||||
|
nDrainTimer = setTimeout(() => resolve(false), NLOG_FILE_CLOSE_TIMEOUT);
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
if (nDrainTimer) {
|
||||||
|
clearTimeout(nDrainTimer);
|
||||||
|
nDrainTimer = null;
|
||||||
|
}
|
||||||
|
//Очередь не успела опустеть - принудительно освободим файл
|
||||||
|
if (!bDrained) {
|
||||||
|
this.reportWriteError(
|
||||||
|
new Error(`Очередь записи в файл лога не опустела за ${NLOG_FILE_CLOSE_TIMEOUT} мс, файл будет закрыт принудительно`)
|
||||||
|
);
|
||||||
|
this.release();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Закроем поток штатно
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
if (!this.logStream || this.logStream.destroyed) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logStream.end(err => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
//Только сообщение - без reopen при закрытии
|
||||||
|
this.reportWriteError(e);
|
||||||
|
this.release(e);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
if (nDrainTimer) clearTimeout(nDrainTimer);
|
||||||
|
}
|
||||||
|
//Освобождаем состояние без повторного destroy уже закрытого потока
|
||||||
|
if (this.logStream) {
|
||||||
|
try {
|
||||||
|
this.logStream.removeAllListeners("error");
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
this.clearState();
|
||||||
|
//Результат закрытия - только в console
|
||||||
|
this.notifyClosedConsole(sLogFile, null);
|
||||||
|
}
|
||||||
|
//Аварийное синхронное освобождение файла лога
|
||||||
|
closeSync() {
|
||||||
|
if (!this.logStream && !this.bFileLog && !this.sLogFile) return;
|
||||||
|
this.bClosing = true;
|
||||||
|
this.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----------------
|
||||||
|
// Интерфейс модуля
|
||||||
|
//-----------------
|
||||||
|
|
||||||
|
exports.LogFileWriter = LogFileWriter;
|
||||||
192
core/logger.js
192
core/logger.js
@ -7,22 +7,76 @@
|
|||||||
// Подключение библиотек
|
// Подключение библиотек
|
||||||
//----------------------
|
//----------------------
|
||||||
|
|
||||||
const { validateObject, getNowString, deepClone } = require("./utils"); //Вспомогательные функции
|
const { validateObject, getNowString, deepClone, makeErrorText } = require("./utils"); //Вспомогательные функции
|
||||||
const db = require("./db_connector"); //Модуль взаимодействия с БД
|
const db = require("./db_connector"); //Модуль взаимодействия с БД
|
||||||
const { SCONSOLE_LOG_COLOR_PATTERN_ERR, SCONSOLE_LOG_COLOR_PATTERN_WRN, SCONSOLE_LOG_COLOR_PATTERN_INF } = require("./constants"); //Общие константы
|
const { LogFileWriter } = require("./log_file_writer"); //Запись протокола в файл
|
||||||
|
const {
|
||||||
|
SCONSOLE_LOG_COLOR_PATTERN_ERR,
|
||||||
|
SCONSOLE_LOG_COLOR_PATTERN_WRN,
|
||||||
|
SCONSOLE_LOG_COLOR_PATTERN_INF,
|
||||||
|
SLOG_PREFIX_INF,
|
||||||
|
SLOG_PREFIX_WRN,
|
||||||
|
SLOG_PREFIX_ERR,
|
||||||
|
SLOG_PREFIX_PROTOCOL_ERR
|
||||||
|
} = require("./constants"); //Общие константы
|
||||||
const { NLOG_STATE_INF, NLOG_STATE_WRN, NLOG_STATE_ERR } = require("../models/obj_log"); //Схемы валидации записи журнала работы сервиса обмена
|
const { NLOG_STATE_INF, NLOG_STATE_WRN, NLOG_STATE_ERR } = require("../models/obj_log"); //Схемы валидации записи журнала работы сервиса обмена
|
||||||
const prmsLoggerSchema = require("../models/prms_logger"); //Схемы валидации параметров функций модуля
|
const prmsLoggerSchema = require("../models/prms_logger"); //Схемы валидации параметров функций модуля
|
||||||
|
|
||||||
|
//--------------------------
|
||||||
|
// Глобальные идентификаторы
|
||||||
|
//--------------------------
|
||||||
|
|
||||||
|
//Интервал повторного сообщения об ошибке передачи строки лога по IPC (мс)
|
||||||
|
const NLOG_IPC_ERROR_REPORT_INTERVAL = 10000;
|
||||||
|
|
||||||
|
//Сообщение об останове сервера
|
||||||
|
const SMSG_SERVER_STOPPED = "Сервер приложений остановлен";
|
||||||
|
|
||||||
//------------
|
//------------
|
||||||
// Тело модуля
|
// Тело модуля
|
||||||
//------------
|
//------------
|
||||||
|
|
||||||
|
//Признак работы в дочернем процессе обработчика
|
||||||
|
const isWorkerProcess = () => typeof process.send === "function" && !!process.connected;
|
||||||
|
|
||||||
|
//Формирование строки лога без ANSI-оформления
|
||||||
|
const formatLogLine = (sNow, sPrefix, sMsg) => `${sNow} ${sPrefix}: ${sMsg}`;
|
||||||
|
|
||||||
|
//Оформление сообщения по типу записи
|
||||||
|
const resolveLogStyle = nLogState => {
|
||||||
|
let sPrefix = SLOG_PREFIX_INF;
|
||||||
|
let sColorPattern = "";
|
||||||
|
switch (nLogState) {
|
||||||
|
case NLOG_STATE_ERR: {
|
||||||
|
sPrefix = SLOG_PREFIX_ERR;
|
||||||
|
sColorPattern = SCONSOLE_LOG_COLOR_PATTERN_ERR;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case NLOG_STATE_WRN: {
|
||||||
|
sPrefix = SLOG_PREFIX_WRN;
|
||||||
|
sColorPattern = SCONSOLE_LOG_COLOR_PATTERN_WRN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case NLOG_STATE_INF: {
|
||||||
|
sPrefix = SLOG_PREFIX_INF;
|
||||||
|
sColorPattern = SCONSOLE_LOG_COLOR_PATTERN_INF;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return { sPrefix, sColorPattern };
|
||||||
|
};
|
||||||
|
|
||||||
//Класс управления протоколом
|
//Класс управления протоколом
|
||||||
class Logger {
|
class Logger {
|
||||||
//Конструктор класса
|
//Конструктор класса
|
||||||
constructor() {
|
constructor() {
|
||||||
this.dbConnector = null;
|
this.dbConnector = null;
|
||||||
this.bLogDB = false;
|
this.bLogDB = false;
|
||||||
|
this.fileWriter = null;
|
||||||
|
this.bStopLogged = false;
|
||||||
|
this.nLastIpcErrorAt = 0;
|
||||||
}
|
}
|
||||||
//Включение/выключение записи протоколов в БД
|
//Включение/выключение записи протоколов в БД
|
||||||
setLogDB(bLogDB) {
|
setLogDB(bLogDB) {
|
||||||
@ -41,6 +95,109 @@ class Logger {
|
|||||||
this.dbConnector = null;
|
this.dbConnector = null;
|
||||||
this.setLogDB(false);
|
this.setLogDB(false);
|
||||||
}
|
}
|
||||||
|
//Открытие файла лога (пустой путь - не открываем)
|
||||||
|
openLogFile(sLogPath) {
|
||||||
|
//Нет пути - протоколирование в файл не ведётся
|
||||||
|
if (!sLogPath || !String(sLogPath).trim()) {
|
||||||
|
this.bStopLogged = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Создаём писатель при первом реальном открытии
|
||||||
|
if (!this.fileWriter) this.fileWriter = new LogFileWriter();
|
||||||
|
this.fileWriter.open(sLogPath);
|
||||||
|
this.bStopLogged = false;
|
||||||
|
}
|
||||||
|
//Уведомление в консоль о протоколировании в файл (без записи в файл)
|
||||||
|
notifyFileLogConsole() {
|
||||||
|
if (this.fileWriter) this.fileWriter.notifyOpenedConsole();
|
||||||
|
}
|
||||||
|
//Корректное закрытие файла лога
|
||||||
|
async closeLogFile() {
|
||||||
|
if (this.fileWriter) await this.fileWriter.close();
|
||||||
|
}
|
||||||
|
//Аварийное синхронное освобождение файла лога
|
||||||
|
closeLogFileSync() {
|
||||||
|
if (this.fileWriter) this.fileWriter.closeSync();
|
||||||
|
}
|
||||||
|
//Признак, что сообщение об останове уже выдано
|
||||||
|
wasStopLogged() {
|
||||||
|
return this.bStopLogged === true;
|
||||||
|
}
|
||||||
|
//Фиксация выдачи сообщения об останове
|
||||||
|
markStopLogged() {
|
||||||
|
this.bStopLogged = true;
|
||||||
|
}
|
||||||
|
//Подготовка сообщения об останове сервера
|
||||||
|
prepareServerStoppedMsg(nCode) {
|
||||||
|
const nExitCode = typeof nCode === "number" ? nCode : 0;
|
||||||
|
this.markStopLogged();
|
||||||
|
return `${SMSG_SERVER_STOPPED} (код: ${nExitCode})`;
|
||||||
|
}
|
||||||
|
//Выдача сообщения об останове сервера
|
||||||
|
async notifyServerStopped(nCode) {
|
||||||
|
await this.warn(this.prepareServerStoppedMsg(nCode));
|
||||||
|
}
|
||||||
|
//Синхронная выдача сообщения об останове сервера (авария / process.on("exit"))
|
||||||
|
notifyServerStoppedSync(nCode) {
|
||||||
|
this.warnSync(this.prepareServerStoppedMsg(nCode));
|
||||||
|
}
|
||||||
|
//Сообщение об ошибке передачи строки лога по IPC
|
||||||
|
reportIpcLogError(e) {
|
||||||
|
const nNow = Date.now();
|
||||||
|
if (nNow - this.nLastIpcErrorAt < NLOG_IPC_ERROR_REPORT_INTERVAL) return;
|
||||||
|
this.nLastIpcErrorAt = nNow;
|
||||||
|
console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${getNowString()} ${SLOG_PREFIX_PROTOCOL_ERR} (IPC): `, makeErrorText(e));
|
||||||
|
}
|
||||||
|
//Отправка строки лога родителю (воркер)
|
||||||
|
sendLogLineToParent(prms) {
|
||||||
|
if (typeof process.send !== "function" || !process.connected) return;
|
||||||
|
if (!prms || typeof prms.sMsg !== "string") return;
|
||||||
|
try {
|
||||||
|
process.send({ ...prms, bLogLine: true }, err => {
|
||||||
|
if (err) this.reportIpcLogError(err);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.reportIpcLogError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Приём строки лога от воркера
|
||||||
|
acceptWorkerLogLine(prms) {
|
||||||
|
if (!prms || typeof prms.sNow !== "string" || typeof prms.sPrefix !== "string" || typeof prms.sMsg !== "string") return;
|
||||||
|
this.emitLogLine(prms.nLogState, prms.sMsg, prms.sNow, prms.sPrefix);
|
||||||
|
}
|
||||||
|
//Выдача строки протокола: воркер - IPC; main - файл, затем console
|
||||||
|
emitLogLine(nLogState, sMsg, sNow, sPrefix, bSync) {
|
||||||
|
const style = resolveLogStyle(nLogState);
|
||||||
|
const sPrefixUse = sPrefix || style.sPrefix;
|
||||||
|
//Воркер - только IPC родителю (файл и console пишет main)
|
||||||
|
if (isWorkerProcess()) {
|
||||||
|
this.sendLogLineToParent({ nLogState, sNow, sPrefix: sPrefixUse, sMsg });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Main: сначала файл, затем console
|
||||||
|
const sLine = formatLogLine(sNow, sPrefixUse, sMsg);
|
||||||
|
if (this.fileWriter) {
|
||||||
|
if (bSync) this.fileWriter.appendSync(sLine);
|
||||||
|
else this.fileWriter.enqueue(sLine);
|
||||||
|
}
|
||||||
|
console.log(style.sColorPattern, `${sNow} ${sPrefixUse}: `, sMsg);
|
||||||
|
}
|
||||||
|
//Синхронное протоколирование без БД (авария / финал)
|
||||||
|
logSync(nLogState, sMsg) {
|
||||||
|
this.emitLogLine(nLogState, sMsg, getNowString(), undefined, true);
|
||||||
|
}
|
||||||
|
//Синхронное протоколирование ошибки
|
||||||
|
errorSync(sMsg) {
|
||||||
|
this.logSync(NLOG_STATE_ERR, sMsg);
|
||||||
|
}
|
||||||
|
//Синхронное протоколирование предупреждения
|
||||||
|
warnSync(sMsg) {
|
||||||
|
this.logSync(NLOG_STATE_WRN, sMsg);
|
||||||
|
}
|
||||||
|
//Синхронное протоколирование информации
|
||||||
|
infoSync(sMsg) {
|
||||||
|
this.logSync(NLOG_STATE_INF, sMsg);
|
||||||
|
}
|
||||||
//Протоколирование
|
//Протоколирование
|
||||||
async log(prms) {
|
async log(prms) {
|
||||||
//Фиксируем время
|
//Фиксируем время
|
||||||
@ -49,30 +206,7 @@ class Logger {
|
|||||||
let sCheckResult = validateObject(prms, prmsLoggerSchema.log, "Параметры функции протоколирования");
|
let sCheckResult = validateObject(prms, prmsLoggerSchema.log, "Параметры функции протоколирования");
|
||||||
//Если структура объекта в норме
|
//Если структура объекта в норме
|
||||||
if (!sCheckResult) {
|
if (!sCheckResult) {
|
||||||
//Определим оформление сообщения
|
this.emitLogLine(prms.nLogState, prms.sMsg, sNow);
|
||||||
let sPrefix = "ИНФОРМАЦИЯ";
|
|
||||||
let sColorPattern = "";
|
|
||||||
switch (prms.nLogState) {
|
|
||||||
case NLOG_STATE_ERR: {
|
|
||||||
sPrefix = "ОШИБКА";
|
|
||||||
sColorPattern = SCONSOLE_LOG_COLOR_PATTERN_ERR;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case NLOG_STATE_WRN: {
|
|
||||||
sPrefix = "ПРЕДУПРЕЖДЕНИЕ";
|
|
||||||
sColorPattern = SCONSOLE_LOG_COLOR_PATTERN_WRN;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case NLOG_STATE_INF: {
|
|
||||||
sPrefix = "ИНФОРМАЦИЯ";
|
|
||||||
sColorPattern = SCONSOLE_LOG_COLOR_PATTERN_INF;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
//Выдаём сообщение
|
|
||||||
console.log(sColorPattern, `${sNow} ${sPrefix}: `, prms.sMsg);
|
|
||||||
//Протоколируем в БД, если это необходимо
|
//Протоколируем в БД, если это необходимо
|
||||||
if (this.bLogDB) {
|
if (this.bLogDB) {
|
||||||
try {
|
try {
|
||||||
@ -81,15 +215,15 @@ class Logger {
|
|||||||
await this.dbConnector.putLog(prms);
|
await this.dbConnector.putLog(prms);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${sNow} ОШИБКА ПРОТОКОЛИРОВАНИЯ: `, e.sMessage);
|
this.emitLogLine(NLOG_STATE_ERR, e.sMessage, sNow, SLOG_PREFIX_PROTOCOL_ERR);
|
||||||
try {
|
try {
|
||||||
if (this.dbConnector && this.dbConnector.bConnected)
|
if (this.dbConnector && this.dbConnector.bConnected)
|
||||||
await this.dbConnector.putLog({ nLogState: NLOG_STATE_ERR, sMsg: `ОШИБКА ПРОТОКОЛИРОВАНИЯ: ${e.sMessage}` });
|
await this.dbConnector.putLog({ nLogState: NLOG_STATE_ERR, sMsg: `${SLOG_PREFIX_PROTOCOL_ERR}: ${e.sMessage}` });
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${sNow} ОШИБКА ПРОТОКОЛИРОВАНИЯ: `, sCheckResult);
|
this.emitLogLine(NLOG_STATE_ERR, sCheckResult, sNow, SLOG_PREFIX_PROTOCOL_ERR);
|
||||||
console.log(prms);
|
console.log(prms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -271,6 +271,11 @@ class OutQueue extends EventEmitter {
|
|||||||
const proc = ChildProcess.fork("core/out_queue_processor", { silent: false });
|
const proc = ChildProcess.fork("core/out_queue_processor", { silent: false });
|
||||||
//Перехват сообщений обработчика
|
//Перехват сообщений обработчика
|
||||||
proc.on("message", async result => {
|
proc.on("message", async result => {
|
||||||
|
//Строка лога от воркера - файл и console родителя (без завершения задачи)
|
||||||
|
if (result && result.bLogLine === true) {
|
||||||
|
self.logger.acceptWorkerLogLine(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
//Сбросим таймер принудительного останова
|
//Сбросим таймер принудительного останова
|
||||||
self.clearWorkerTerminateTimeout(proc);
|
self.clearWorkerTerminateTimeout(proc);
|
||||||
//Перечитывание не требуется, если выполнено успешно
|
//Перечитывание не требуется, если выполнено успешно
|
||||||
|
|||||||
@ -753,8 +753,13 @@ process.on("SIGTERM", () => {
|
|||||||
|
|
||||||
//Перехват ошибок
|
//Перехват ошибок
|
||||||
process.on("uncaughtException", e => {
|
process.on("uncaughtException", e => {
|
||||||
|
const sText = makeErrorText(e);
|
||||||
|
//Синхронно в console и (при необходимости) IPC родителю - до возможной гибели процесса
|
||||||
|
if (logger) {
|
||||||
|
logger.errorSync(`Непредвиденная ошибка обработчика исходящего сообщения: ${sText}`);
|
||||||
|
}
|
||||||
//Отправляем ошибку родительскому процессу
|
//Отправляем ошибку родительскому процессу
|
||||||
sendErrorResult({ sMessage: makeErrorText(e) });
|
sendErrorResult({ sMessage: sText });
|
||||||
});
|
});
|
||||||
|
|
||||||
//Приём сообщений
|
//Приём сообщений
|
||||||
|
|||||||
61
index.js
61
index.js
@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
const cfg = require("./config"); //Настройки сервера приложений
|
const cfg = require("./config"); //Настройки сервера приложений
|
||||||
const app = require("./core/app"); //Сервер приложений
|
const app = require("./core/app"); //Сервер приложений
|
||||||
const { makeErrorText, getNowString } = require("./core/utils"); //Вспомогательные функции
|
const { makeErrorText } = require("./core/utils"); //Вспомогательные функции
|
||||||
const { SCONSOLE_LOG_COLOR_PATTERN_ERR, SCONSOLE_LOG_COLOR_PATTERN_WRN } = require("./core/constants"); //Общие константы
|
|
||||||
|
|
||||||
//--------------------------
|
//--------------------------
|
||||||
// Глобальные идентификаторы
|
// Глобальные идентификаторы
|
||||||
@ -31,10 +30,30 @@ process.env.NODE_ORACLE_DB_THIN_MODE = cfg?.dbConnect?.bOraUseThinMode === true
|
|||||||
//Путь к клиентским библиотекам для Oracle
|
//Путь к клиентским библиотекам для Oracle
|
||||||
process.env.ORACLE_CLIENT_LIB_DIR = cfg?.dbConnect?.sOraClient || "";
|
process.env.ORACLE_CLIENT_LIB_DIR = cfg?.dbConnect?.sOraClient || "";
|
||||||
|
|
||||||
|
//Аварийное освобождение файла лога
|
||||||
|
const releaseLogFileEmergency = () => {
|
||||||
|
try {
|
||||||
|
appSrv.logger.closeLogFileSync();
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
//Аварийный выход с протоколированием и освобождением файла лога
|
||||||
|
const emergencyExit = sErrMsg => {
|
||||||
|
if (sErrMsg) appSrv.logger.errorSync(sErrMsg);
|
||||||
|
if (!appSrv.logger.wasStopLogged()) {
|
||||||
|
appSrv.logger.notifyServerStoppedSync(1);
|
||||||
|
}
|
||||||
|
releaseLogFileEmergency();
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
//Обработка события "выход" жизненного цикла процесса
|
//Обработка события "выход" жизненного цикла процесса
|
||||||
process.on("exit", code => {
|
process.on("exit", code => {
|
||||||
//Сообщим о завершении процесса
|
//Если сообщение об останове уже было выдано ранее - не дублируем
|
||||||
console.log(SCONSOLE_LOG_COLOR_PATTERN_WRN, `${getNowString()} ПРЕДУПРЕЖДЕНИЕ: `, `Сервер приложений остановлен (код: ${code})`);
|
if (appSrv.logger.wasStopLogged()) return;
|
||||||
|
//Допишем в console и в файл (если ещё открыт), затем освободим дескриптор
|
||||||
|
appSrv.logger.notifyServerStoppedSync(code);
|
||||||
|
releaseLogFileEmergency();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Обработка событий мягкого останова процесса
|
//Обработка событий мягкого останова процесса
|
||||||
@ -42,30 +61,22 @@ process.on("exit", code => {
|
|||||||
process.once(sSig, async () => {
|
process.once(sSig, async () => {
|
||||||
await appSrv.logger.warn(`Получен сигнал на останов сервера приложений: ${sSig}`);
|
await appSrv.logger.warn(`Получен сигнал на останов сервера приложений: ${sSig}`);
|
||||||
const terminateTimeout = setTimeout(() => {
|
const terminateTimeout = setTimeout(() => {
|
||||||
console.log(
|
emergencyExit("Истекло время ожидания останова сервера приложений. Инициирован аварийный выход из процесса.");
|
||||||
SCONSOLE_LOG_COLOR_PATTERN_ERR,
|
|
||||||
`${getNowString()} ОШИБКА: `,
|
|
||||||
`Истекло время ожидания останова сервера приложений. Инициирован аварийный выход из процесса.`
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}, cfg.common.nTerminateTimeout);
|
}, cfg.common.nTerminateTimeout);
|
||||||
try {
|
try {
|
||||||
await appSrv.stop(terminateTimeout);
|
await appSrv.stop(terminateTimeout);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
|
||||||
await appSrv.logger.error(`При останове сервера приложений: ${makeErrorText(e)}`);
|
|
||||||
clearTimeout(terminateTimeout);
|
clearTimeout(terminateTimeout);
|
||||||
process.exit(1);
|
//Только sync: дальше закрываем файл и выходим
|
||||||
|
appSrv.logger.errorSync(`При останове сервера приложений: ${makeErrorText(e)}`);
|
||||||
|
emergencyExit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//Перехват всех неохваченных ошибок
|
//Перехват всех неохваченных ошибок
|
||||||
process.on("uncaughtException", e => {
|
process.on("uncaughtException", e => {
|
||||||
//Протоколируем ошибку
|
emergencyExit(`НЕПРЕДВИДЕННАЯ ОШИБКА: ${makeErrorText(e)}`);
|
||||||
console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${getNowString()} НЕПРЕДВИДЕННАЯ ОШИБКА: `, makeErrorText(e));
|
|
||||||
//Останов с ошибкой
|
|
||||||
process.exit(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//Запуск сервера приложений
|
//Запуск сервера приложений
|
||||||
@ -78,13 +89,17 @@ const start = async () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
//Если есть ошибки с которыми сервер не справился - ловим их, показываем...
|
//Если есть ошибки с которыми сервер не справился - ловим их, показываем...
|
||||||
await appSrv.logger.error(makeErrorText(e));
|
await appSrv.logger.error(makeErrorText(e));
|
||||||
//...и пытаемся остановить сервер нормально
|
//...и пытаемся остановить сервер нормально (с таймаутом и кодом 1)
|
||||||
|
const terminateTimeout = setTimeout(() => {
|
||||||
|
emergencyExit("Истекло время ожидания останова сервера приложений. Инициирован аварийный выход из процесса.");
|
||||||
|
}, cfg.common.nTerminateTimeout);
|
||||||
try {
|
try {
|
||||||
await appSrv.stop();
|
await appSrv.stop(terminateTimeout, 1);
|
||||||
} catch (e) {
|
} catch (eStop) {
|
||||||
//Могут быть ошибки и при остановке - это аварийный выход
|
clearTimeout(terminateTimeout);
|
||||||
await appSrv.logger.error(makeErrorText(e));
|
//Только sync: дальше закрываем файл и выходим
|
||||||
process.exit(1);
|
appSrv.logger.errorSync(makeErrorText(eStop));
|
||||||
|
emergencyExit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -134,6 +134,14 @@ const common = new Schema({
|
|||||||
type: path => `Признак контроля версии Системы (${path}) имеет некорректный тип данных (ожидалось - Boolean)`,
|
type: path => `Признак контроля версии Системы (${path}) имеет некорректный тип данных (ожидалось - Boolean)`,
|
||||||
required: path => `Не указан признак контроля версии Системы (${path})`
|
required: path => `Не указан признак контроля версии Системы (${path})`
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
//Директория размещения логов
|
||||||
|
sLogPath: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
message: {
|
||||||
|
type: path => `Директория размещения логов (${path}) имеет некорректный тип данных (ожидалось - String)`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
27
models/prms_log_file_writer.js
Normal file
27
models/prms_log_file_writer.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
Сервис интеграции ПП Парус 8 с WEB API
|
||||||
|
Модели данных: описатели параметров функций модуля записи протокола в файл (класс LogFileWriter)
|
||||||
|
*/
|
||||||
|
|
||||||
|
//----------------------
|
||||||
|
// Подключение библиотек
|
||||||
|
//----------------------
|
||||||
|
|
||||||
|
const Schema = require("validate"); //Схемы валидации
|
||||||
|
|
||||||
|
//-----------------
|
||||||
|
// Интерфейс модуля
|
||||||
|
//-----------------
|
||||||
|
|
||||||
|
//Схема валидации параметров функции открытия файла лога
|
||||||
|
exports.open = new Schema({
|
||||||
|
//Директория размещения логов
|
||||||
|
sLogPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
message: {
|
||||||
|
type: path => `Директория размещения логов (${path}) имеет некорректный тип данных (ожидалось - String)`,
|
||||||
|
required: path => `Не указана директория размещения логов (${path})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
const xml2js = require("xml2js"); //Конвертация XML в JSON и JSON в XML
|
const xml2js = require("xml2js"); //Конвертация XML в JSON и JSON в XML
|
||||||
const { deepClone } = require("../core/utils"); //Вспомогательные функции
|
const { deepClone } = require("../core/utils"); //Вспомогательные функции
|
||||||
|
const { isRCMNRequest, prepareRCMNRequestForXml } = require("./rcmn"); //Оперативный учёт (RCMN)
|
||||||
|
|
||||||
//---------------------
|
//---------------------
|
||||||
// Глобальные константы
|
// Глобальные константы
|
||||||
@ -77,6 +78,8 @@ const before = async prms => {
|
|||||||
let requestXML = "";
|
let requestXML = "";
|
||||||
try {
|
try {
|
||||||
let request = JSON.parse(prms.queue.blMsg.toString());
|
let request = JSON.parse(prms.queue.blMsg.toString());
|
||||||
|
//Если запрос относится к расширению "Оперативный учёт" - подготовим его
|
||||||
|
if (isRCMNRequest(request)) request = prepareRCMNRequestForXml(request);
|
||||||
let builder = new xml2js.Builder();
|
let builder = new xml2js.Builder();
|
||||||
requestXML = builder.buildObject(request);
|
requestXML = builder.buildObject(request);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
85
modules/rcmn.js
Normal file
85
modules/rcmn.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
Сервис интеграции ПП Парус 8 с WEB API
|
||||||
|
Дополнительный модуль: Оперативный учёт (RCMN)
|
||||||
|
*/
|
||||||
|
|
||||||
|
//---------------------
|
||||||
|
// Глобальные константы
|
||||||
|
//---------------------
|
||||||
|
|
||||||
|
//Имена узлов/аргументов PWS-запроса
|
||||||
|
const SNODE_XREQUEST = "XREQUEST"; //Корневой узел запроса
|
||||||
|
const SNODE_XPAYLOAD = "XPAYLOAD"; //Полезная нагрузка запроса
|
||||||
|
const SNODE_XARGUMENTS = "XARGUMENTS"; //Коллекция аргументов
|
||||||
|
const SNODE_XARGUMENT = "XARGUMENT"; //Элемент аргумента
|
||||||
|
const SNODE_SNAME = "SNAME"; //Имя аргумента
|
||||||
|
const SNODE_VALUE = "VALUE"; //Значение аргумента
|
||||||
|
const SNODE_SSTORED = "SSTORED"; //Имя вызываемой хранимой процедуры
|
||||||
|
const SARG_SSEARCH = "SSEARCH"; //Аргумент строки поиска
|
||||||
|
|
||||||
|
//Пакет и хранимые процедуры расширения "Оперативный учёт"
|
||||||
|
const SPACKAGE_RCMN = "PKG_EXS_EXT_RCMN"; //Пакет расширения
|
||||||
|
const SSTORED_CLASSES_GET = "PKG_EXS_EXT_RCMN.CLASSES_GET"; //Получение списка классов
|
||||||
|
const SSTORED_OBJECTS_GET = "PKG_EXS_EXT_RCMN.OBJECTS_GET"; //Получение списка объектов
|
||||||
|
const SSTORED_DICT_RECS_GET = "PKG_EXS_EXT_RCMN.DICT_RECS_GET"; //Получение данных словаря
|
||||||
|
const RSTORED_SSEARCH_BASE64 = [SSTORED_CLASSES_GET, SSTORED_OBJECTS_GET, SSTORED_DICT_RECS_GET]; //Процедуры, для которых SSEARCH кодируется в Base64
|
||||||
|
|
||||||
|
//------------
|
||||||
|
// Тело модуля
|
||||||
|
//------------
|
||||||
|
|
||||||
|
//Кодирование строки в Base64 (UTF-8)
|
||||||
|
const encodeBase64Utf8 = sValue => Buffer.from(sValue, "utf8").toString("base64");
|
||||||
|
|
||||||
|
//Получение полезной нагрузки запроса
|
||||||
|
const getRequestPayload = request => {
|
||||||
|
if (!request || typeof request !== "object") return null;
|
||||||
|
const xRequest = request[SNODE_XREQUEST];
|
||||||
|
if (!xRequest || typeof xRequest !== "object") return null;
|
||||||
|
const xPayload = xRequest[SNODE_XPAYLOAD];
|
||||||
|
if (!xPayload || typeof xPayload !== "object") return null;
|
||||||
|
return xPayload;
|
||||||
|
};
|
||||||
|
|
||||||
|
//Получение имени хранимой процедуры из запроса
|
||||||
|
const getRequestStored = request => {
|
||||||
|
const xPayload = getRequestPayload(request);
|
||||||
|
if (!xPayload) return null;
|
||||||
|
const sStored = xPayload[SNODE_SSTORED];
|
||||||
|
return typeof sStored === "string" ? sStored : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
//Проверка, что запрос относится к пакету расширения "Оперативный учёт"
|
||||||
|
const isRCMNRequest = request => {
|
||||||
|
const sStored = getRequestStored(request);
|
||||||
|
if (!sStored) return false;
|
||||||
|
return sStored === SPACKAGE_RCMN || sStored.startsWith(SPACKAGE_RCMN + ".");
|
||||||
|
};
|
||||||
|
|
||||||
|
//Подготовка JSON-запроса расширения "Оперативный учёт" перед конвертацией в XML
|
||||||
|
const prepareRCMNRequestForXml = request => {
|
||||||
|
//Получим полезную нагрузку запроса
|
||||||
|
const xPayload = getRequestPayload(request);
|
||||||
|
if (!xPayload) return request;
|
||||||
|
//Проверим, что указана поддерживаемая хранимая процедура
|
||||||
|
if (RSTORED_SSEARCH_BASE64.indexOf(xPayload[SNODE_SSTORED]) < 0) return request;
|
||||||
|
//Получим коллекцию аргументов
|
||||||
|
const xArguments = xPayload[SNODE_XARGUMENTS];
|
||||||
|
if (!Array.isArray(xArguments)) return request;
|
||||||
|
//Закодируем в Base64 значение аргумента SSEARCH (если есть)
|
||||||
|
for (let i = 0; i < xArguments.length; i++) {
|
||||||
|
const item = xArguments[i];
|
||||||
|
const arg = item && (item[SNODE_XARGUMENT] || item);
|
||||||
|
if (!arg || typeof arg !== "object" || arg[SNODE_SNAME] !== SARG_SSEARCH) continue;
|
||||||
|
if (arg[SNODE_VALUE] === undefined || arg[SNODE_VALUE] === null) continue;
|
||||||
|
arg[SNODE_VALUE] = encodeBase64Utf8(arg[SNODE_VALUE]);
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
|
||||||
|
//-----------------
|
||||||
|
// Интерфейс модуля
|
||||||
|
//-----------------
|
||||||
|
|
||||||
|
exports.isRCMNRequest = isRCMNRequest;
|
||||||
|
exports.prepareRCMNRequestForXml = prepareRCMNRequestForXml;
|
||||||
Loading…
x
Reference in New Issue
Block a user