Compare commits
No commits in common. "master" and "master" have entirely different histories.
@ -18,9 +18,7 @@ let common = {
|
|||||||
//Таймаут останова сервера (мс)
|
//Таймаут останова сервера (мс)
|
||||||
nTerminateTimeout: 60000,
|
nTerminateTimeout: 60000,
|
||||||
//Контролировать версию Системы
|
//Контролировать версию Системы
|
||||||
bControlSystemVersion: true,
|
bControlSystemVersion: true
|
||||||
//Директория размещения логов (не указано / пусто - протоколирование в файл не ведётся)
|
|
||||||
sLogPath: ""
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//Параметры подключения к БД
|
//Параметры подключения к БД
|
||||||
|
|||||||
28
core/app.js
28
core/app.js
@ -42,8 +42,6 @@ class ParusAppServer {
|
|||||||
this.bStopping = false;
|
this.bStopping = false;
|
||||||
//Таймаут останова сервера
|
//Таймаут останова сервера
|
||||||
this.terminateTimeout = null;
|
this.terminateTimeout = null;
|
||||||
//Код завершения процесса при завершении каскада останова (0 - естественное завершение)
|
|
||||||
this.nStopExitCode = 0;
|
|
||||||
//Список обслуживаемых сервисов
|
//Список обслуживаемых сервисов
|
||||||
this.services = [];
|
this.services = [];
|
||||||
//Привяжем методы к указателю на себя для использования в обработчиках событий
|
//Привяжем методы к указателю на себя для использования в обработчиках событий
|
||||||
@ -182,32 +180,20 @@ 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}`);
|
||||||
//Создаём подключение к БД
|
//Создаём подключение к БД
|
||||||
@ -292,14 +278,12 @@ class ParusAppServer {
|
|||||||
await this.dbConn.connect();
|
await this.dbConn.connect();
|
||||||
}
|
}
|
||||||
//Останов сервера
|
//Останов сервера
|
||||||
async stop(terminateTimeout, nExitCode) {
|
async stop(terminateTimeout) {
|
||||||
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,9 +52,3 @@ 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 = "ОШИБКА ПРОТОКОЛИРОВАНИЯ";
|
|
||||||
|
|||||||
@ -1,364 +0,0 @@
|
|||||||
/*
|
|
||||||
Сервис интеграции ПП Парус 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,76 +7,22 @@
|
|||||||
// Подключение библиотек
|
// Подключение библиотек
|
||||||
//----------------------
|
//----------------------
|
||||||
|
|
||||||
const { validateObject, getNowString, deepClone, makeErrorText } = require("./utils"); //Вспомогательные функции
|
const { validateObject, getNowString, deepClone } = require("./utils"); //Вспомогательные функции
|
||||||
const db = require("./db_connector"); //Модуль взаимодействия с БД
|
const db = require("./db_connector"); //Модуль взаимодействия с БД
|
||||||
const { LogFileWriter } = require("./log_file_writer"); //Запись протокола в файл
|
const { SCONSOLE_LOG_COLOR_PATTERN_ERR, SCONSOLE_LOG_COLOR_PATTERN_WRN, SCONSOLE_LOG_COLOR_PATTERN_INF } = require("./constants"); //Общие константы
|
||||||
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) {
|
||||||
@ -95,109 +41,6 @@ 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) {
|
||||||
//Фиксируем время
|
//Фиксируем время
|
||||||
@ -206,7 +49,30 @@ 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 {
|
||||||
@ -215,15 +81,15 @@ class Logger {
|
|||||||
await this.dbConnector.putLog(prms);
|
await this.dbConnector.putLog(prms);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.emitLogLine(NLOG_STATE_ERR, e.sMessage, sNow, SLOG_PREFIX_PROTOCOL_ERR);
|
console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${sNow} ОШИБКА ПРОТОКОЛИРОВАНИЯ: `, e.sMessage);
|
||||||
try {
|
try {
|
||||||
if (this.dbConnector && this.dbConnector.bConnected)
|
if (this.dbConnector && this.dbConnector.bConnected)
|
||||||
await this.dbConnector.putLog({ nLogState: NLOG_STATE_ERR, sMsg: `${SLOG_PREFIX_PROTOCOL_ERR}: ${e.sMessage}` });
|
await this.dbConnector.putLog({ nLogState: NLOG_STATE_ERR, sMsg: `ОШИБКА ПРОТОКОЛИРОВАНИЯ: ${e.sMessage}` });
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.emitLogLine(NLOG_STATE_ERR, sCheckResult, sNow, SLOG_PREFIX_PROTOCOL_ERR);
|
console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${sNow} ОШИБКА ПРОТОКОЛИРОВАНИЯ: `, sCheckResult);
|
||||||
console.log(prms);
|
console.log(prms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -271,11 +271,6 @@ 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,13 +753,8 @@ process.on("SIGTERM", () => {
|
|||||||
|
|
||||||
//Перехват ошибок
|
//Перехват ошибок
|
||||||
process.on("uncaughtException", e => {
|
process.on("uncaughtException", e => {
|
||||||
const sText = makeErrorText(e);
|
|
||||||
//Синхронно в console и (при необходимости) IPC родителю - до возможной гибели процесса
|
|
||||||
if (logger) {
|
|
||||||
logger.errorSync(`Непредвиденная ошибка обработчика исходящего сообщения: ${sText}`);
|
|
||||||
}
|
|
||||||
//Отправляем ошибку родительскому процессу
|
//Отправляем ошибку родительскому процессу
|
||||||
sendErrorResult({ sMessage: sText });
|
sendErrorResult({ sMessage: makeErrorText(e) });
|
||||||
});
|
});
|
||||||
|
|
||||||
//Приём сообщений
|
//Приём сообщений
|
||||||
|
|||||||
61
index.js
61
index.js
@ -9,7 +9,8 @@
|
|||||||
|
|
||||||
const cfg = require("./config"); //Настройки сервера приложений
|
const cfg = require("./config"); //Настройки сервера приложений
|
||||||
const app = require("./core/app"); //Сервер приложений
|
const app = require("./core/app"); //Сервер приложений
|
||||||
const { makeErrorText } = require("./core/utils"); //Вспомогательные функции
|
const { makeErrorText, getNowString } = require("./core/utils"); //Вспомогательные функции
|
||||||
|
const { SCONSOLE_LOG_COLOR_PATTERN_ERR, SCONSOLE_LOG_COLOR_PATTERN_WRN } = require("./core/constants"); //Общие константы
|
||||||
|
|
||||||
//--------------------------
|
//--------------------------
|
||||||
// Глобальные идентификаторы
|
// Глобальные идентификаторы
|
||||||
@ -30,30 +31,10 @@ 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 => {
|
||||||
//Если сообщение об останове уже было выдано ранее - не дублируем
|
//Сообщим о завершении процесса
|
||||||
if (appSrv.logger.wasStopLogged()) return;
|
console.log(SCONSOLE_LOG_COLOR_PATTERN_WRN, `${getNowString()} ПРЕДУПРЕЖДЕНИЕ: `, `Сервер приложений остановлен (код: ${code})`);
|
||||||
//Допишем в console и в файл (если ещё открыт), затем освободим дескриптор
|
|
||||||
appSrv.logger.notifyServerStoppedSync(code);
|
|
||||||
releaseLogFileEmergency();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//Обработка событий мягкого останова процесса
|
//Обработка событий мягкого останова процесса
|
||||||
@ -61,22 +42,30 @@ 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(() => {
|
||||||
emergencyExit("Истекло время ожидания останова сервера приложений. Инициирован аварийный выход из процесса.");
|
console.log(
|
||||||
|
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);
|
||||||
//Только sync: дальше закрываем файл и выходим
|
process.exit(1);
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
//Запуск сервера приложений
|
//Запуск сервера приложений
|
||||||
@ -89,17 +78,13 @@ 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(terminateTimeout, 1);
|
await appSrv.stop();
|
||||||
} catch (eStop) {
|
} catch (e) {
|
||||||
clearTimeout(terminateTimeout);
|
//Могут быть ошибки и при остановке - это аварийный выход
|
||||||
//Только sync: дальше закрываем файл и выходим
|
await appSrv.logger.error(makeErrorText(e));
|
||||||
appSrv.logger.errorSync(makeErrorText(eStop));
|
process.exit(1);
|
||||||
emergencyExit();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -134,14 +134,6 @@ 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)`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
Сервис интеграции ПП Парус 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,7 +9,6 @@
|
|||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
//---------------------
|
//---------------------
|
||||||
// Глобальные константы
|
// Глобальные константы
|
||||||
@ -28,6 +27,15 @@ const SQUERY_RESP_CT = "SRESP_CT"; //Параметр URL-запроса для
|
|||||||
//Коды функций-обработчиков (согласно настройкам "Сервисов обмена")
|
//Коды функций-обработчиков (согласно настройкам "Сервисов обмена")
|
||||||
const SFNC_UPLOAD = "Upload"; //Функция загрузки
|
const SFNC_UPLOAD = "Upload"; //Функция загрузки
|
||||||
|
|
||||||
|
//Имена узлов/аргументов 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 SARG_SSEARCH = "SSEARCH"; //Аргумент строки поиска
|
||||||
|
|
||||||
//------------
|
//------------
|
||||||
// Тело модуля
|
// Тело модуля
|
||||||
//------------
|
//------------
|
||||||
@ -70,6 +78,38 @@ const converXMLArraysToJSON = (obj, arrayKey) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Кодирование строки в 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
//Подготовка JSON-запроса перед конвертацией в XML
|
||||||
|
const prepareRequestForXml = request => {
|
||||||
|
//Получим полезную нагрузку запроса
|
||||||
|
const xPayload = getRequestPayload(request);
|
||||||
|
if (!xPayload) return request;
|
||||||
|
//Получим коллекцию аргументов
|
||||||
|
const xArguments = xPayload[SNODE_XARGUMENTS];
|
||||||
|
if (!Array.isArray(xArguments)) return request;
|
||||||
|
//Обработаем аргументы
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
//Обработчик "До" для полученного сообщения
|
//Обработчик "До" для полученного сообщения
|
||||||
const before = async prms => {
|
const before = async prms => {
|
||||||
//Если пришел запрос в JSON
|
//Если пришел запрос в JSON
|
||||||
@ -78,8 +118,7 @@ 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());
|
||||||
//Если запрос относится к расширению "Оперативный учёт" - подготовим его
|
request = prepareRequestForXml(request);
|
||||||
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) {
|
||||||
|
|||||||
@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
Сервис интеграции ПП Парус 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