printnatura/print-server.js

310 lines
10 KiB
JavaScript
Raw Normal View History

2022-11-04 13:41:36 +00:00
const mysql = require('mysql2/promise');
const exec = require('child_process').exec;
const fs = require('fs-extra');
const path = require('path');
const colors = require('colors');
const axios = require('axios');
const yml = require('require-yml');
const selectQuery = fs.readFileSync(`sql/selectQueued.sql`).toString();
2022-12-15 14:04:36 +00:00
const jobDataQuery = fs.readFileSync(`sql/jobData.sql`).toString();
2022-11-04 13:41:36 +00:00
const jobArgsQuery = fs.readFileSync(`sql/jobArgs.sql`).toString();
const updateQuery = fs.readFileSync(`sql/updateState.sql`).toString();
class PrintServer {
async start() {
let conf = yml(path.join(__dirname, 'config.yml'));
const localConfFile = path.join(__dirname, 'config.local.yml');
if (fs.existsSync(localConfFile))
conf = Object.assign({}, conf, yml(localConfFile));
this.conf = conf;
console.clear();
const decoration = '△▽'.repeat(10);
const printnatura = colors.bgBlack.bold(' Print'.white + 'Natura '.green);
console.log(`${decoration} ${printnatura} ${decoration}`);
if (this.conf.dryPrint)
this.serverLog('log', 'Running in dry print mode, documents won\'t be printed'.yellow);
if (this.conf.keepFile)
this.serverLog('log', 'Keep file enabled, documents won\'t be deleted from disk'.yellow);
2022-11-04 13:41:36 +00:00
await this.init();
this.rejectionHandler = (err, p) => this.onRejection(err, p);
process.on('unhandledRejection', this.rejectionHandler);
this.serverLog('log', 'Ready to print'.green);
setTimeout(() => this.poll());
2022-11-04 13:41:36 +00:00
}
async stop() {
this.serverLog('log', 'Bye ( ◕ ‿ ◕ )っ'.green);
process.off('unhandledRejection', this.rejectionHandler);
2022-11-04 13:41:36 +00:00
await this.end();
}
async init() {
const conf = this.conf;
const api = this.api = axios.create({
baseURL: `${conf.salix.url}/api/`
});
api.interceptors.request.use(config => {
if (this.token)
config.headers['Authorization'] = this.token
return config;
});
await this.getToken();
this.pool = await mysql.createPool(conf.db);
2022-11-04 13:41:36 +00:00
}
async end() {
await this.api.post(`Accounts/logout`);
this.token = null;
2022-12-19 13:28:27 +00:00
if (this.pollTimeout) {
clearTimeout(this.pollTimeout);
this.pollTimeout = null;
}
await this.pool.end();
2022-11-04 13:41:36 +00:00
}
async getToken() {
2022-12-12 19:04:51 +00:00
const salix = this.conf.salix;
let response = await this.api.post(`Accounts/login`, {
2022-11-10 18:21:13 +00:00
user: salix.user,
password: salix.password
2022-11-04 13:41:36 +00:00
});
2022-12-12 19:04:51 +00:00
this.token = response.data.token;
2022-11-04 13:41:36 +00:00
}
serverLog(realm, message) {
this.log(`Server`, realm, message);
}
log(classMsg, realm, message) {
classMsg = `${classMsg}:`;
switch(realm) {
case 'debug':
if (this.conf.debug) console.debug(classMsg, message.magenta);
break;
case 'error':
console.error(classMsg, message.red);
break;
default:
if (this.conf.log) console.log(classMsg, message);
}
}
onRejection(err, p) {
console.debug('unhandledRejection');
this.errorHandler(err);
}
errorHandler(err) {
if (err.code === 'ETIMEDOUT') {
if (!this.dbDown) {
this.dbDown = true;
this.serverLog('error', `DB connection lost: ${err.message}`);
}
} else
console.error(err);
}
2022-11-04 13:41:36 +00:00
async poll() {
const conf = this.conf;
2022-12-19 13:28:27 +00:00
this.pollTimeout = null;
if (this.dbDown) {
try {
let conn;
try {
conn = await this.pool.getConnection();
await conn.ping();
this.dbDown = false;
this.serverLog('log', 'DB connection recovered'.green);
} catch (err) {
conn.release();
}
} catch(e) {}
}
if (!this.dbDown) {
try {
let jobs;
let nJobs = 0;
do {
jobs = [];
for (let i = 0; i < conf.concurrency; i++) {
const jobId = await this.getJob();
2022-12-23 20:28:02 +00:00
if (jobId) {
const job = this.printJob(jobId);
// XXX: Workaround for Promise.all() unhandledRejection
// https://stackoverflow.com/questions/67789309/why-do-i-get-an-unhandled-promise-rejection-with-await-promise-all
job.catch(() => {});
jobs.push(job);
}
else
break;
}
nJobs += jobs.length;
await Promise.all(jobs);
} while (jobs.length);
if (nJobs > 0)
this.serverLog('debug', `${nJobs} jobs printed`);
} catch (err) {
this.errorHandler(err);
}
2022-11-04 13:41:36 +00:00
}
let delay = this.conf.refreshRate;
if (this.dbDown) delay = this.conf.reconnectTimeout;
this.pollTimeout = setTimeout(() => this.poll(), delay);
2022-11-04 13:41:36 +00:00
}
async getJob() {
2022-11-04 13:41:36 +00:00
let jobId;
const conn = await this.pool.getConnection();
2022-11-04 13:41:36 +00:00
try {
await conn.beginTransaction();
try {
const [[printJob]] = await conn.query(selectQuery);
if (printJob) {
jobId = printJob.id;
await conn.query(updateQuery, ['printing', null, jobId]);
await conn.commit();
this.jobLog(jobId, 'debug', 'get: printing');
}
} catch (err) {
await conn.rollback();
if (jobId)
this.jobLog(jobId, 'error', err.message);
throw err;
}
} finally {
conn.release();
}
2022-11-10 17:25:21 +00:00
return jobId;
}
async printJob(jobId) {
const conf = this.conf;
let jobData;
const args = {};
let tmpFilePath;
let tmpFileCreated = false;
const conn = await this.pool.getConnection();
try {
await conn.beginTransaction();
try {
// Job data
const [[data]] = await conn.query(jobDataQuery, jobId);
jobData = data;
// Job arguments
const [res] = await conn.query(jobArgsQuery, jobId);
for (const row of res)
args[row.name] = row.value;
await conn.commit();
} catch (err) {
await conn.rollback();
throw err;
}
2022-11-04 13:41:36 +00:00
2022-12-15 14:04:36 +00:00
// Path params
const usedParams = new Set();
const methodPath = jobData.method.replace(/{\w+}/g, function(match) {
const key = match.substr(1, match.length - 2);
const value = args[key];
usedParams.add(key);
return value !== undefined ? value : match;
});
let pdfData;
let url;
for (let attempts = 0; !pdfData && attempts < 2; attempts++) {
2022-12-12 19:04:51 +00:00
// URL params
const params = {userFk: jobData.userFk};
2022-12-12 19:04:51 +00:00
for (const key in args) {
if (!usedParams.has(key))
params[key] = args[key];
}
const urlParams = new URLSearchParams(params);
url = `${methodPath}?${urlParams.toString()}`;
this.jobLog(jobId, 'debug', `api: ${url}`);
2022-12-12 19:04:51 +00:00
// Request
2022-12-15 14:04:36 +00:00
try {
const response = await this.api({
2022-12-15 14:04:36 +00:00
method: 'get',
url,
2022-12-15 14:04:36 +00:00
responseType: 'arraybuffer',
headers: {'Accept': 'application/pdf'}
2022-12-15 14:04:36 +00:00
});
pdfData = response.data;
}
catch (err) {
if (err.response?.statusText === 'Unauthorized') {
await this.getToken();
} else
throw err;
2022-11-04 13:41:36 +00:00
}
2022-11-09 17:34:27 +00:00
}
2022-12-15 14:04:36 +00:00
// Save PDF to disk
const printer = jobData.printer;
const tmpPath = conf.tmpDir;
2022-11-10 18:21:13 +00:00
if (!fs.existsSync(tmpPath))
fs.mkdirSync(tmpPath)
tmpFilePath = path.join(tmpPath, `job-${jobId}.pdf`);
2022-12-15 14:04:36 +00:00
await fs.writeFile(tmpFilePath, pdfData, 'binary');
tmpFileCreated = true;
2022-11-04 13:41:36 +00:00
2022-12-15 14:04:36 +00:00
// Print PDF
const printCommand = `lp -d "${printer}" "${tmpFilePath}"`;
this.jobLog(jobId, 'debug', `print: ${printCommand}`);
2022-11-04 13:41:36 +00:00
try {
if (!conf.dryPrint) await pExec(printCommand);
2022-11-04 13:41:36 +00:00
} catch(err) {
2022-12-19 13:36:51 +00:00
throw new Error(`Print error: ${err.message}`);
2022-11-04 13:41:36 +00:00
}
await conn.query(updateQuery, ['printed', null, jobId]);
this.jobLog(jobId, 'log', `${jobData.report}: '${printCommand}': GET ${url}`);
2022-11-04 13:41:36 +00:00
} catch (err) {
2022-12-19 13:28:27 +00:00
let message = err.message;
if (err.name === 'AxiosError' && err.code === 'ERR_BAD_REQUEST') {
const resMessage = JSON.parse(err.response.data).error.message;
message = `${message}: ${resMessage}`;
}
await conn.query(updateQuery, ['error', message, jobId]);
this.jobLog(jobId, 'error', message);
const jobErr = new Error(`(${jobId}) ${message}`);
jobErr.stack = err.stack;
throw jobErr;
} finally {
conn.release();
}
if (!conf.keepFile) {
try {
const shouldDelete = tmpFileCreated
|| (tmpFilePath && await fs.pathExists(tmpFilePath));
if (shouldDelete) await fs.unlink(tmpFilePath);
} catch (err) {
this.jobLog(jobId, 'error', err.message);
}
}
}
jobLog(jobId, realm, message) {
this.log(`Job[${colors.yellow(jobId)}]`, realm, message);
2022-11-04 13:41:36 +00:00
}
}
module.exports = PrintServer;
function pExec(command) {
return new Promise(function(resolve, reject) {
exec(command, function(err, stdout, stderr) {
if (err) return reject(err);
resolve({stdout, stderr})
});
});
2022-11-04 13:41:36 +00:00
}