Merge branch '5368-tryErrors'
gitea/printnatura/pipeline/head This commit looks good Details

This commit is contained in:
Guillermo Bonet 2023-04-26 13:49:51 +02:00
commit c6bd546fb9
3 changed files with 304 additions and 288 deletions

View File

@ -28,6 +28,8 @@ concurrency: 4
reconnectTimeout: 10 reconnectTimeout: 10
refreshRate: 1000 refreshRate: 1000
tmpDir: /dev/shm/printnatura tmpDir: /dev/shm/printnatura
retryAttempts: 3
retryTimeout: 1000
db: db:
host: localhost host: localhost
port: 3306 port: 3306
@ -44,7 +46,6 @@ salix:
Exec Exec
``` ```
> docker run --name printnatura -it --rm -v $PWD/config.local.yml:/printnatura/config.local.yml:ro -v $PWD/cupsd.conf:/etc/cups/cupsd.conf:ro -p 80:631 printnatura > docker run --name printnatura -it --rm -v $PWD/config.local.yml:/printnatura/config.local.yml:ro -v $PWD/cupsd.conf:/etc/cups/cupsd.conf:ro -p 80:631 printnatura
``` ```
Bash Bash

View File

@ -7,6 +7,8 @@ concurrency: 4
reconnectTimeout: 10 reconnectTimeout: 10
refreshRate: 1000 refreshRate: 1000
tmpDir: /dev/shm/printnatura tmpDir: /dev/shm/printnatura
retryAttempts: 3
retryTimeout: 1500
db: db:
host: localhost host: localhost
port: 3306 port: 3306

View File

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