This commit is contained in:
parent
0cdf85541c
commit
3750ec109f
|
@ -2,7 +2,7 @@ debug: false
|
|||
log: true
|
||||
dryPrint: false
|
||||
keepFile: false
|
||||
serverId: 1
|
||||
serverId: null
|
||||
concurrency: 4
|
||||
reconnectTimeout: 10
|
||||
refreshRate: 1000
|
||||
|
|
597
print-server.js
597
print-server.js
|
@ -14,332 +14,335 @@ const errorQuery = fs.readFileSync(`sql/updateError.sql`).toString();
|
|||
const printedQuery = fs.readFileSync(`sql/updatePrinted.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;
|
||||
this.jobs = [];
|
||||
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;
|
||||
this.jobs = [];
|
||||
|
||||
console.clear();
|
||||
const decoration = '△▽'.repeat(10);
|
||||
const printnatura = colors.bgBlack.bold(' Print'.white + 'Natura '.green);
|
||||
console.log(`${decoration} ${printnatura} ${decoration}`);
|
||||
if (this.conf.debug)
|
||||
this.serverLog('log', 'Debug mode enabled'.yellow);
|
||||
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);
|
||||
console.clear();
|
||||
const decoration = '△▽'.repeat(10);
|
||||
const printnatura = colors.bgBlack.bold(' Print'.white + 'Natura '.green);
|
||||
console.log(`${decoration} ${printnatura} ${decoration}`);
|
||||
if (this.conf.debug)
|
||||
this.serverLog('log', 'Debug mode enabled'.yellow);
|
||||
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);
|
||||
|
||||
await this.init();
|
||||
this.rejectionHandler = (err, p) => this.onRejection(err, p);
|
||||
process.on('unhandledRejection', this.rejectionHandler);
|
||||
await this.init();
|
||||
this.rejectionHandler = (err, p) => this.onRejection(err, p);
|
||||
process.on('unhandledRejection', this.rejectionHandler);
|
||||
|
||||
this.serverLog('log', 'Ready to print'.green);
|
||||
await this.poll();
|
||||
}
|
||||
async stop() {
|
||||
this.serverLog('log', 'Bye ( ◕ ‿ ◕ )っ'.green);
|
||||
process.off('unhandledRejection', this.rejectionHandler);
|
||||
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.serverLog('log', 'Ready to print'.green);
|
||||
await this.poll();
|
||||
}
|
||||
async stop() {
|
||||
this.serverLog('log', 'Bye ( ◕ ‿ ◕ )っ'.green);
|
||||
process.off('unhandledRejection', this.rejectionHandler);
|
||||
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);
|
||||
}
|
||||
async end() {
|
||||
if (this.token) {
|
||||
await this.api.post(`Accounts/logout`);
|
||||
this.token = null;
|
||||
}
|
||||
if (this.pollTimeout) {
|
||||
clearTimeout(this.pollTimeout);
|
||||
this.pollTimeout = null;
|
||||
}
|
||||
await this.pool.end();
|
||||
}
|
||||
async getToken() {
|
||||
const salix = this.conf.salix;
|
||||
let response = await this.api.post(`Accounts/login`, {
|
||||
user: salix.user,
|
||||
password: salix.password
|
||||
});
|
||||
this.token = response.data.token;
|
||||
}
|
||||
serverLog(realm, message) {
|
||||
this.log(`Server`, realm, message);
|
||||
}
|
||||
log(classMsg, realm, message) {
|
||||
classMsg = `${classMsg}:`;
|
||||
this.pool = await mysql.createPool(conf.db);
|
||||
}
|
||||
async end() {
|
||||
if (this.token) {
|
||||
await this.api.post(`Accounts/logout`);
|
||||
this.token = null;
|
||||
}
|
||||
if (this.pollTimeout) {
|
||||
clearTimeout(this.pollTimeout);
|
||||
this.pollTimeout = null;
|
||||
}
|
||||
await this.pool.end();
|
||||
}
|
||||
async getToken() {
|
||||
const salix = this.conf.salix;
|
||||
let response = await this.api.post(`Accounts/login`, {
|
||||
user: salix.user,
|
||||
password: salix.password
|
||||
});
|
||||
this.token = response.data.token;
|
||||
}
|
||||
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);
|
||||
}
|
||||
async poll() {
|
||||
this.pollTimeout = null;
|
||||
let delay = this.conf.refreshRate;
|
||||
await this.getJobs();
|
||||
if (this.dbDown) delay = this.conf.reconnectTimeout * 1000;
|
||||
this.pollTimeout = setTimeout(() => this.poll(), delay);
|
||||
}
|
||||
async getJobs() {
|
||||
if (this.polling) return;
|
||||
this.polling = true;
|
||||
const conf = this.conf;
|
||||
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);
|
||||
}
|
||||
async poll() {
|
||||
this.pollTimeout = null;
|
||||
let delay = this.conf.refreshRate;
|
||||
await this.getJobs();
|
||||
if (this.dbDown) delay = this.conf.reconnectTimeout * 1000;
|
||||
this.pollTimeout = setTimeout(() => this.poll(), delay);
|
||||
}
|
||||
async getJobs() {
|
||||
if (this.polling) return;
|
||||
this.polling = true;
|
||||
const conf = this.conf;
|
||||
|
||||
if (this.dbDown) {
|
||||
try {
|
||||
const conn = await this.pool.getConnection();
|
||||
try {
|
||||
await conn.ping();
|
||||
this.dbDown = false;
|
||||
this.serverLog('log', 'DB connection recovered'.green);
|
||||
} catch (err) {
|
||||
conn.release();
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
if (this.dbDown) {
|
||||
try {
|
||||
const conn = await this.pool.getConnection();
|
||||
try {
|
||||
await conn.ping();
|
||||
this.dbDown = false;
|
||||
this.serverLog('log', 'DB connection recovered'.green);
|
||||
} catch (err) {
|
||||
conn.release();
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
if (!this.dbDown) {
|
||||
try {
|
||||
let nJobs = 0;
|
||||
while (this.jobs.length < conf.concurrency) {
|
||||
const jobId = await this.getJob();
|
||||
if (jobId) {
|
||||
nJobs++;
|
||||
this.jobs.push(jobId);
|
||||
const jobP = this.printJob(jobId);
|
||||
// XXX: Workaround for Promise unhandledRejection
|
||||
// https://stackoverflow.com/questions/67789309/why-do-i-get-an-unhandled-promise-rejection-with-await-promise-all
|
||||
jobP.catch(err => this.errorHandler(err));
|
||||
} else
|
||||
break;
|
||||
}
|
||||
if (nJobs > 0)
|
||||
this.serverLog('debug', `${nJobs} jobs buffered`);
|
||||
} catch (err) {
|
||||
this.errorHandler(err);
|
||||
}
|
||||
}
|
||||
if (!this.dbDown) {
|
||||
try {
|
||||
let nJobs = 0;
|
||||
while (this.jobs.length < conf.concurrency) {
|
||||
const jobId = await this.getJob();
|
||||
if (jobId) {
|
||||
nJobs++;
|
||||
this.jobs.push(jobId);
|
||||
const jobP = this.printJob(jobId);
|
||||
// XXX: Workaround for Promise unhandledRejection
|
||||
// https://stackoverflow.com/questions/67789309/why-do-i-get-an-unhandled-promise-rejection-with-await-promise-all
|
||||
jobP.catch(err => this.errorHandler(err));
|
||||
} else
|
||||
break;
|
||||
}
|
||||
if (nJobs > 0)
|
||||
this.serverLog('debug', `${nJobs} jobs buffered`);
|
||||
} catch (err) {
|
||||
this.errorHandler(err);
|
||||
}
|
||||
}
|
||||
|
||||
this.polling = false;
|
||||
}
|
||||
async getJob() {
|
||||
let jobId;
|
||||
this.polling = false;
|
||||
}
|
||||
async getJob() {
|
||||
let jobId;
|
||||
|
||||
const conn = await this.pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
try {
|
||||
const [[printJob]] = await conn.query(selectQuery);
|
||||
if (printJob) {
|
||||
jobId = printJob.id;
|
||||
await conn.query(printingQuery, [
|
||||
'printing',
|
||||
now(),
|
||||
null,
|
||||
null,
|
||||
this.conf.serverId,
|
||||
jobId
|
||||
]);
|
||||
this.jobLog(jobId, 'debug', 'get: printing');
|
||||
await conn.commit();
|
||||
} else
|
||||
await conn.rollback();
|
||||
} catch (err) {
|
||||
try {
|
||||
await conn.rollback();
|
||||
} catch (e) {}
|
||||
if (jobId)
|
||||
this.jobLog(jobId, 'error', err.message);
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
const conn = await this.pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
try {
|
||||
const [[printJob]] = await conn.query(selectQuery);
|
||||
if (printJob) {
|
||||
jobId = printJob.id;
|
||||
await conn.query(printingQuery, [
|
||||
'printing',
|
||||
now(),
|
||||
null,
|
||||
null,
|
||||
this.conf.serverId,
|
||||
jobId
|
||||
]);
|
||||
this.jobLog(jobId, 'debug', 'get: printing');
|
||||
await conn.commit();
|
||||
} else
|
||||
await conn.rollback();
|
||||
} catch (err) {
|
||||
try {
|
||||
await conn.rollback();
|
||||
} catch (e) {}
|
||||
if (jobId)
|
||||
this.jobLog(jobId, 'error', err.message);
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
|
||||
return jobId;
|
||||
}
|
||||
async printJob(jobId) {
|
||||
const conf = this.conf;
|
||||
let jobData;
|
||||
const args = {};
|
||||
let tmpFilePath;
|
||||
let tmpFileCreated = false;
|
||||
return jobId;
|
||||
}
|
||||
async printJob(jobId) {
|
||||
const conf = this.conf;
|
||||
let jobData;
|
||||
const args = {};
|
||||
let tmpFilePath;
|
||||
let tmpFileCreated = false;
|
||||
|
||||
let conn;
|
||||
try {
|
||||
conn = await this.pool.getConnection();
|
||||
let conn;
|
||||
try {
|
||||
conn = await this.pool.getConnection();
|
||||
|
||||
// Job data
|
||||
await conn.beginTransaction();
|
||||
try {
|
||||
const [[data]] = await conn.query(jobDataQuery, jobId);
|
||||
jobData = data;
|
||||
// Job data
|
||||
await conn.beginTransaction();
|
||||
try {
|
||||
const [[data]] = await conn.query(jobDataQuery, jobId);
|
||||
jobData = data;
|
||||
|
||||
const [res] = await conn.query(jobArgsQuery, jobId);
|
||||
for (const row of res)
|
||||
args[row.name] = row.value;
|
||||
} finally {
|
||||
await conn.rollback();
|
||||
}
|
||||
const [res] = await conn.query(jobArgsQuery, jobId);
|
||||
for (const row of res)
|
||||
args[row.name] = row.value;
|
||||
} finally {
|
||||
await conn.rollback();
|
||||
}
|
||||
|
||||
// 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;
|
||||
});
|
||||
// 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;
|
||||
});
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
|
||||
const url = `${methodPath}?${urlParams.toString()}`;
|
||||
this.jobLog(jobId, 'debug', `api: ${url}`);
|
||||
const url = `${methodPath}?${urlParams.toString()}`;
|
||||
this.jobLog(jobId, 'debug', `api: ${url}`);
|
||||
|
||||
// Request
|
||||
let pdfData;
|
||||
for (let attempts = 0; !pdfData && attempts < this.conf.retryAttempts; attempts++) {
|
||||
try {
|
||||
const res = await this.api({
|
||||
method: 'get',
|
||||
url,
|
||||
responseType: 'arraybuffer',
|
||||
headers: {'Accept': 'application/pdf'}
|
||||
});
|
||||
pdfData = res.data;
|
||||
}
|
||||
catch (err) {
|
||||
if (err.name === 'AxiosError' && attempts < this.conf.retryAttempts - 1) {
|
||||
const res = err.response;
|
||||
switch(res.status) {
|
||||
case 401: // Unauthorized
|
||||
await this.getToken();
|
||||
break;
|
||||
case 502 || 504: // Bad Gateway & Gateway Timeout
|
||||
await new Promise(
|
||||
resolve => setTimeout(resolve, this.conf.retryTimeout));
|
||||
break;
|
||||
default:
|
||||
try {
|
||||
const resMessage = JSON.parse(res.data).error.message;
|
||||
const resErr = new Error(`${err.message}: ${resMessage}`);
|
||||
resErr.stack = err.stack;
|
||||
throw resErr;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
// Request
|
||||
let pdfData;
|
||||
for (let attempts = 0; !pdfData && attempts < this.conf.retryAttempts; attempts++) {
|
||||
try {
|
||||
const res = await this.api({
|
||||
method: 'get',
|
||||
url,
|
||||
responseType: 'arraybuffer',
|
||||
headers: {'Accept': 'application/pdf'}
|
||||
});
|
||||
pdfData = res.data;
|
||||
}
|
||||
catch (err) {
|
||||
if (err.name === 'AxiosError' && attempts < this.conf.retryAttempts - 1) {
|
||||
const res = err.response;
|
||||
switch(res.status) {
|
||||
case 401: // Unauthorized
|
||||
await this.getToken();
|
||||
break;
|
||||
case 502 || 504: // Bad Gateway & Gateway Timeout
|
||||
await new Promise(
|
||||
resolve => setTimeout(resolve, this.conf.retryTimeout));
|
||||
break;
|
||||
default:
|
||||
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
|
||||
const printer = jobData.printer;
|
||||
const tmpPath = conf.tmpDir;
|
||||
if (!fs.existsSync(tmpPath))
|
||||
fs.mkdirSync(tmpPath)
|
||||
tmpFilePath = path.join(tmpPath, `job-${jobId}.pdf`);
|
||||
await fs.writeFile(tmpFilePath, pdfData, 'binary');
|
||||
tmpFileCreated = true;
|
||||
// Save PDF to disk
|
||||
const printer = jobData.printer;
|
||||
const tmpPath = conf.tmpDir;
|
||||
if (!fs.existsSync(tmpPath))
|
||||
fs.mkdirSync(tmpPath)
|
||||
tmpFilePath = path.join(tmpPath, `job-${jobId}.pdf`);
|
||||
await fs.writeFile(tmpFilePath, pdfData, 'binary');
|
||||
tmpFileCreated = true;
|
||||
|
||||
// Print PDF
|
||||
const printCommand = `lp -d "${printer}" "${tmpFilePath}"`;
|
||||
this.jobLog(jobId, 'debug', `print: ${printCommand}`);
|
||||
try {
|
||||
if (!conf.dryPrint) await pExec(printCommand);
|
||||
} catch(err) {
|
||||
throw new Error(`Print error: ${err.message}`);
|
||||
}
|
||||
// Print PDF
|
||||
const printCommand = `lp -d "${printer}" "${tmpFilePath}"`;
|
||||
this.jobLog(jobId, 'debug', `print: ${printCommand}`);
|
||||
try {
|
||||
if (!conf.dryPrint) await pExec(printCommand);
|
||||
} catch(err) {
|
||||
throw new Error(`Print error: ${err.message}`);
|
||||
}
|
||||
|
||||
await conn.query(printedQuery, ['printed', now(), jobId]);
|
||||
this.jobLog(jobId, 'log', `${jobData.report}: '${printCommand}': GET ${url}`);
|
||||
} catch (err) {
|
||||
try {
|
||||
await conn.query(errorQuery, ['error', now(), err.message, jobId]);
|
||||
} catch (e) {
|
||||
this.jobLog(jobId, 'error', e.message);
|
||||
}
|
||||
await conn.query(printedQuery, ['printed', now(), jobId]);
|
||||
this.jobLog(jobId, 'log', `${jobData.report}: '${printCommand}': GET ${url}`);
|
||||
} catch (err) {
|
||||
try {
|
||||
await conn.query(errorQuery, ['error', now(), err.message, jobId]);
|
||||
} catch (e) {
|
||||
this.jobLog(jobId, 'error', e.message);
|
||||
}
|
||||
|
||||
this.jobLog(jobId, 'error', err.message);
|
||||
const jobErr = new Error(`(${jobId}) ${err.message}`);
|
||||
jobErr.stack = err.stack;
|
||||
throw jobErr;
|
||||
} finally {
|
||||
if (conn) conn.release();
|
||||
this.jobLog(jobId, 'error', err.message);
|
||||
const jobErr = new Error(`(${jobId}) ${err.message}`);
|
||||
jobErr.stack = err.stack;
|
||||
throw jobErr;
|
||||
} finally {
|
||||
if (conn) 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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const index = this.jobs.indexOf(jobId);
|
||||
if (index !== -1) this.jobs.splice(index, 1);
|
||||
setTimeout(() => this.getJobs());
|
||||
}
|
||||
}
|
||||
jobLog(jobId, realm, message) {
|
||||
this.log(`Job[${colors.yellow(jobId)}]`, realm, message);
|
||||
}
|
||||
const index = this.jobs.indexOf(jobId);
|
||||
if (index !== -1) this.jobs.splice(index, 1);
|
||||
setTimeout(() => this.getJobs());
|
||||
}
|
||||
}
|
||||
jobLog(jobId, realm, message) {
|
||||
this.log(`Job[${colors.yellow(jobId)}]`, realm, message);
|
||||
}
|
||||
}
|
||||
|
||||
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})
|
||||
});
|
||||
});
|
||||
return new Promise(function(resolve, reject) {
|
||||
exec(command, function(err, stdout, stderr) {
|
||||
if (err) return reject(err);
|
||||
resolve({stdout, stderr})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function now() {
|
||||
return new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||||
return new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
Loading…
Reference in New Issue