Compare commits

..

3 Commits

12 changed files with 730 additions and 60 deletions

View File

@ -18,7 +18,9 @@ let common = {
//Таймаут останова сервера (мс) //Таймаут останова сервера (мс)
nTerminateTimeout: 60000, nTerminateTimeout: 60000,
//Контролировать версию Системы //Контролировать версию Системы
bControlSystemVersion: true bControlSystemVersion: true,
//Директория размещения логов (не указано / пусто - протоколирование в файл не ведётся)
sLogPath: ""
}; };
//Параметры подключения к БД //Параметры подключения к БД

View File

@ -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("Останов сервера приложений...");
//Останов обслуживания очереди исходящих //Останов обслуживания очереди исходящих

View File

@ -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
View 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;

View File

@ -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);
} }
} }

View File

@ -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);
//Перечитывание не требуется, если выполнено успешно //Перечитывание не требуется, если выполнено успешно

View File

@ -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 });
}); });
//Приём сообщений //Приём сообщений

View File

@ -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();
} }
} }
}; };

View File

@ -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)`
}
} }
}); });

View 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})`
}
}
});

View File

@ -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
View 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;