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(); const jobDataQuery = fs.readFileSync(`sql/jobData.sql`).toString(); const jobArgsQuery = fs.readFileSync(`sql/jobArgs.sql`).toString(); const updateQuery = fs.readFileSync(`sql/updateState.sql`).toString(); const appDir = path.dirname(require.main.filename); 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; const decoration = '△▽'.repeat(10) console.clear(); console.log(decoration, `${colors.bgBlack.white.bold(' Print')}${colors.bgBlack.green.bold('Natura ')}`, decoration, '\n') await this.getToken(); await this.init(); } async init() { this.pool = await mysql.createPool(this.conf.db); console.log('Connected to DB successfully.'.green); await this.poll(); } async stop() { await this.end(); await axios.post(`${this.conf.salix.url}/api/Accounts/logout?access_token=${this.token}`); } async end() { if (this.pollTimeout) { clearTimeout(this.pollTimeout); this.pollTimeout = null; } await this.pool.end(); } async getToken() { const salix = this.conf.salix; let response = await axios.post(`${salix.url}/api/Accounts/login`, { user: salix.user, password: salix.password }); this.token = response.data.token; } async poll() { let delay = this.conf.refreshRate; this.pollTimeout = null; try { const conn = await this.pool.getConnection(); try { if (this.dbDisconnected) { await conn.ping(); this.dbDisconnected = false; console.log('DB connection recovered.'.green); } if (await this.printJob(conn)) delay = 0; } finally { await conn.release(); } } catch (err) { if (err.code === 'ETIMEDOUT') { delay = this.conf.reconnectTimeout; if (!this.dbDisconnected) { this.dbDisconnected = true; console.log(`DB connection lost: ${err.message}`.red); } } else console.error(err); } this.pollTimeout = setTimeout(() => this.poll(), delay); } async printJob(conn) { const conf = this.conf; let jobId; try { let jobData; const args = {}; try { await conn.beginTransaction(); const [[printJob]] = await conn.query(selectQuery); if (!printJob) { await conn.rollback(); return; } jobId = printJob.id; // 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.query(updateQuery, ['printing', null, jobId]); await conn.commit(); } catch (err) { await conn.rollback(); throw err; } // 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; for (let attempts = 0; !pdfData && attempts < 2; attempts++) { // URL params const params = {userFk: jobData.userFk}; for (const key in args) { if (!usedParams.has(key)) params[key] = args[key]; } const urlParams = new URLSearchParams(params); // Request try { const response = await axios({ method: 'get', url: `${conf.salix.url}/api/${methodPath}?${urlParams.toString()}`, responseType: 'arraybuffer', headers: { 'Accept': 'application/pdf', 'Authorization': this.token } }); pdfData = response.data; } catch (err) { if (err.response?.statusText === 'Unauthorized') { await this.getToken(); } else throw err; } } // Save PDF to disk const printer = jobData.printer; const tmpPath = path.join(appDir, 'tmp') if (!fs.existsSync(tmpPath)) fs.mkdirSync(tmpPath) const tmpFilePath = path.join(tmpPath, `${Math.random().toString(36).substring(7)}.pdf`); await fs.writeFile(tmpFilePath, pdfData, 'binary'); // Print PDF try { await pExec(`lp -d "${printer}" "${tmpFilePath}"`); } catch(err) { await fs.unlink(tmpFilePath); throw new Error(`Print error: ${err.message}`); } await conn.query(updateQuery, ['printed', null, jobId]); if (conf.debug) console.debug(`(${colors.yellow(jobId)}) Document has been printed`, `[${args.collectionFk}, ${jobData.report}, ${printer}]`.green); await fs.unlink(tmpFilePath); } catch (err) { 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]); throw new Error(`(${jobId}) ${message}`); } return jobId; } } 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}) }); }); }