Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b6515795d | |||
| 5410aa4fb4 |
@ -18,7 +18,9 @@ let common = {
|
||||
//Таймаут останова сервера (мс)
|
||||
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.terminateTimeout = null;
|
||||
//Код завершения процесса при завершении каскада останова (0 - естественное завершение)
|
||||
this.nStopExitCode = 0;
|
||||
//Список обслуживаемых сервисов
|
||||
this.services = [];
|
||||
//Привяжем методы к указателю на себя для использования в обработчиках событий
|
||||
@ -180,20 +182,32 @@ class ParusAppServer {
|
||||
} catch (e) {
|
||||
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) {
|
||||
await this.logger.info("Инициализация сервера приложений...");
|
||||
//Проверяем структуру переданного объекта конфигурации
|
||||
let sCheckResult = validateObject(prms, prmsAppSchema.init, "Параметры инициализации");
|
||||
//Если настройки верны - будем стартовать
|
||||
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}`);
|
||||
//Создаём подключение к БД
|
||||
@ -278,12 +292,14 @@ class ParusAppServer {
|
||||
await this.dbConn.connect();
|
||||
}
|
||||
//Останов сервера
|
||||
async stop(terminateTimeout) {
|
||||
async stop(terminateTimeout, nExitCode) {
|
||||
if (!this.bStopping) {
|
||||
//Установим флаг - остановка в процессе
|
||||
this.bStopping = true;
|
||||
//Запомним таймер аварийного останова
|
||||
this.terminateTimeout = terminateTimeout;
|
||||
//Код завершения после каскада останова (по умолчанию - естественное завершение)
|
||||
this.nStopExitCode = typeof nExitCode === "number" ? nExitCode : 0;
|
||||
//Сообщаем, что начался останов сервера
|
||||
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_WRN = "\x1b[33m%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 { 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 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 {
|
||||
//Конструктор класса
|
||||
constructor() {
|
||||
this.dbConnector = null;
|
||||
this.bLogDB = false;
|
||||
this.fileWriter = null;
|
||||
this.bStopLogged = false;
|
||||
this.nLastIpcErrorAt = 0;
|
||||
}
|
||||
//Включение/выключение записи протоколов в БД
|
||||
setLogDB(bLogDB) {
|
||||
@ -41,6 +95,109 @@ class Logger {
|
||||
this.dbConnector = null;
|
||||
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) {
|
||||
//Фиксируем время
|
||||
@ -49,30 +206,7 @@ class Logger {
|
||||
let sCheckResult = validateObject(prms, prmsLoggerSchema.log, "Параметры функции протоколирования");
|
||||
//Если структура объекта в норме
|
||||
if (!sCheckResult) {
|
||||
//Определим оформление сообщения
|
||||
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);
|
||||
this.emitLogLine(prms.nLogState, prms.sMsg, sNow);
|
||||
//Протоколируем в БД, если это необходимо
|
||||
if (this.bLogDB) {
|
||||
try {
|
||||
@ -81,15 +215,15 @@ class Logger {
|
||||
await this.dbConnector.putLog(prms);
|
||||
}
|
||||
} 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 {
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${sNow} ОШИБКА ПРОТОКОЛИРОВАНИЯ: `, sCheckResult);
|
||||
this.emitLogLine(NLOG_STATE_ERR, sCheckResult, sNow, SLOG_PREFIX_PROTOCOL_ERR);
|
||||
console.log(prms);
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,6 +271,11 @@ class OutQueue extends EventEmitter {
|
||||
const proc = ChildProcess.fork("core/out_queue_processor", { silent: false });
|
||||
//Перехват сообщений обработчика
|
||||
proc.on("message", async result => {
|
||||
//Строка лога от воркера - файл и console родителя (без завершения задачи)
|
||||
if (result && result.bLogLine === true) {
|
||||
self.logger.acceptWorkerLogLine(result);
|
||||
return;
|
||||
}
|
||||
//Сбросим таймер принудительного останова
|
||||
self.clearWorkerTerminateTimeout(proc);
|
||||
//Перечитывание не требуется, если выполнено успешно
|
||||
|
||||
@ -753,8 +753,13 @@ process.on("SIGTERM", () => {
|
||||
|
||||
//Перехват ошибок
|
||||
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 app = require("./core/app"); //Сервер приложений
|
||||
const { makeErrorText, getNowString } = require("./core/utils"); //Вспомогательные функции
|
||||
const { SCONSOLE_LOG_COLOR_PATTERN_ERR, SCONSOLE_LOG_COLOR_PATTERN_WRN } = require("./core/constants"); //Общие константы
|
||||
const { makeErrorText } = require("./core/utils"); //Вспомогательные функции
|
||||
|
||||
//--------------------------
|
||||
// Глобальные идентификаторы
|
||||
@ -31,10 +30,30 @@ process.env.NODE_ORACLE_DB_THIN_MODE = cfg?.dbConnect?.bOraUseThinMode === true
|
||||
//Путь к клиентским библиотекам для Oracle
|
||||
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 => {
|
||||
//Сообщим о завершении процесса
|
||||
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 () => {
|
||||
await appSrv.logger.warn(`Получен сигнал на останов сервера приложений: ${sSig}`);
|
||||
const terminateTimeout = setTimeout(() => {
|
||||
console.log(
|
||||
SCONSOLE_LOG_COLOR_PATTERN_ERR,
|
||||
`${getNowString()} ОШИБКА: `,
|
||||
`Истекло время ожидания останова сервера приложений. Инициирован аварийный выход из процесса.`
|
||||
);
|
||||
process.exit(1);
|
||||
emergencyExit("Истекло время ожидания останова сервера приложений. Инициирован аварийный выход из процесса.");
|
||||
}, cfg.common.nTerminateTimeout);
|
||||
try {
|
||||
await appSrv.stop(terminateTimeout);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
await appSrv.logger.error(`При останове сервера приложений: ${makeErrorText(e)}`);
|
||||
clearTimeout(terminateTimeout);
|
||||
process.exit(1);
|
||||
//Только sync: дальше закрываем файл и выходим
|
||||
appSrv.logger.errorSync(`При останове сервера приложений: ${makeErrorText(e)}`);
|
||||
emergencyExit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//Перехват всех неохваченных ошибок
|
||||
process.on("uncaughtException", e => {
|
||||
//Протоколируем ошибку
|
||||
console.log(SCONSOLE_LOG_COLOR_PATTERN_ERR, `${getNowString()} НЕПРЕДВИДЕННАЯ ОШИБКА: `, makeErrorText(e));
|
||||
//Останов с ошибкой
|
||||
process.exit(1);
|
||||
emergencyExit(`НЕПРЕДВИДЕННАЯ ОШИБКА: ${makeErrorText(e)}`);
|
||||
});
|
||||
|
||||
//Запуск сервера приложений
|
||||
@ -78,13 +89,17 @@ const start = async () => {
|
||||
} catch (e) {
|
||||
//Если есть ошибки с которыми сервер не справился - ловим их, показываем...
|
||||
await appSrv.logger.error(makeErrorText(e));
|
||||
//...и пытаемся остановить сервер нормально
|
||||
//...и пытаемся остановить сервер нормально (с таймаутом и кодом 1)
|
||||
const terminateTimeout = setTimeout(() => {
|
||||
emergencyExit("Истекло время ожидания останова сервера приложений. Инициирован аварийный выход из процесса.");
|
||||
}, cfg.common.nTerminateTimeout);
|
||||
try {
|
||||
await appSrv.stop();
|
||||
} catch (e) {
|
||||
//Могут быть ошибки и при остановке - это аварийный выход
|
||||
await appSrv.logger.error(makeErrorText(e));
|
||||
process.exit(1);
|
||||
await appSrv.stop(terminateTimeout, 1);
|
||||
} catch (eStop) {
|
||||
clearTimeout(terminateTimeout);
|
||||
//Только sync: дальше закрываем файл и выходим
|
||||
appSrv.logger.errorSync(makeErrorText(eStop));
|
||||
emergencyExit();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -134,6 +134,14 @@ const common = new Schema({
|
||||
type: path => `Признак контроля версии Системы (${path}) имеет некорректный тип данных (ожидалось - Boolean)`,
|
||||
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 { deepClone } = require("../core/utils"); //Вспомогательные функции
|
||||
const { isRCMNRequest, prepareRCMNRequestForXml } = require("./rcmn"); //Оперативный учёт (RCMN)
|
||||
|
||||
//---------------------
|
||||
// Глобальные константы
|
||||
@ -27,15 +28,6 @@ const SQUERY_RESP_CT = "SRESP_CT"; //Параметр URL-запроса для
|
||||
//Коды функций-обработчиков (согласно настройкам "Сервисов обмена")
|
||||
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"; //Аргумент строки поиска
|
||||
|
||||
//------------
|
||||
// Тело модуля
|
||||
//------------
|
||||
@ -78,38 +70,6 @@ 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 => {
|
||||
//Если пришел запрос в JSON
|
||||
@ -118,7 +78,8 @@ const before = async prms => {
|
||||
let requestXML = "";
|
||||
try {
|
||||
let request = JSON.parse(prms.queue.blMsg.toString());
|
||||
request = prepareRequestForXml(request);
|
||||
//Если запрос относится к расширению "Оперативный учёт" - подготовим его
|
||||
if (isRCMNRequest(request)) request = prepareRCMNRequestForXml(request);
|
||||
let builder = new xml2js.Builder();
|
||||
requestXML = builder.buildObject(request);
|
||||
} 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