Merge branch '5368-tryErrors'
gitea/printnatura/pipeline/head This commit looks good
Details
gitea/printnatura/pipeline/head This commit looks good
Details
This commit is contained in:
commit
c6bd546fb9
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
587
print-server.js
587
print-server.js
|
@ -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', ' ');
|
||||||
}
|
}
|
Loading…
Reference in New Issue