butlerd
Version:
Node.js library for butlerd, the butler daemon
169 lines (168 loc) • 5.94 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const split2 = require("split2");
const fs = require("fs");
const child_process_1 = require("child_process");
const debug = require("debug")("butlerd:instance");
const DEFAULT_TIMEOUT = 7000; // ms
class Instance {
constructor(butlerOpts) {
this.exiting = false;
this.cancelled = false;
this.gracefullyExited = false;
let log = (msg) => {
debug(msg);
if (butlerOpts.log) {
butlerOpts.log(msg);
}
};
let onExit = () => {
this.exiting = true;
this.cancel();
};
process.on("exit", onExit);
let resolveEndpoint;
let rejectEndpoint;
let endpointTimeout = butlerOpts.endpointTimeout || DEFAULT_TIMEOUT;
let beforeEndpoint = Date.now();
this._endpointPromise = new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
if (this.process) {
log(`one timeout`);
}
else {
log(`spawned butler, PID ${this.process.pid}`);
}
reject(new Error("timed out waiting for butlerd to listen"));
}, endpointTimeout);
rejectEndpoint = reject;
resolveEndpoint = (endpoint) => {
clearTimeout(timeout);
resolve(endpoint);
};
});
this._promise = new Promise((resolve, reject) => {
let butlerArgs = [
"--json",
"daemon",
"--transport",
"tcp",
"--keep-alive",
];
if (debug.enabled) {
butlerArgs = [...butlerArgs, "--verbose"];
}
if (butlerOpts.args) {
butlerArgs = [...butlerArgs, ...butlerOpts.args];
}
log(`spawning butler with args ${butlerArgs.join(" ")}...`);
let { butlerExecutable } = butlerOpts;
let exists = false;
try {
exists = fs.existsSync(butlerExecutable);
}
catch (_) {
// ignore
}
log(`using executable ${butlerExecutable} (${exists ? "exists on disk" : "does not exist on disk"})`);
this.process = child_process_1.spawn(butlerExecutable, butlerArgs, {
stdio: ["ignore", "pipe", "pipe"],
});
log(`spawned butler, PID ${this.process.pid}`);
let errLines = [];
const onClose = (code, signal) => {
process.removeListener("exit", onExit);
log(`butler closed, signal ${signal}, code ${code}`);
if (this.cancelled) {
resolve();
return;
}
if (signal) {
reject(new Error(`Killed by signal ${signal}`));
return;
}
if (code === 0) {
this.gracefullyExited = true;
resolve();
return;
}
reject(new Error(`butler exit code ${code}, error log:\n${errLines.join("\n")}`));
};
this.process.on("close", (code, signal) => {
try {
onClose(code, signal);
}
catch (e) {
reject(e);
}
});
this.process.on("error", err => {
if (this.exiting) {
// swallow error if process is quitting anyway
return;
}
log(`butler had error: ${err.stack ? err.stack : String(err)}`);
reject(err);
});
const processStdoutLine = (line) => {
let data;
try {
data = JSON.parse(line);
}
catch (e) {
log(`[out] ${line}`);
return;
}
switch (data.type) {
case "butlerd/listen-notification": {
let elapsedMs = Date.now() - beforeEndpoint;
log(`got endpoint, took ${elapsedMs.toFixed()}ms`);
resolveEndpoint(data);
break;
}
case "log": {
log(`[${data.level}] ${data.message}`);
break;
}
}
};
this.process.stdout.pipe(split2()).on("data", (line) => {
try {
processStdoutLine(line);
}
catch (e) {
reject(e);
}
});
const processStderrLine = (line) => {
log(`[err] ${line}`);
errLines.push(line);
};
this.process.stderr.pipe(split2()).on("data", (line) => {
try {
processStderrLine(line);
}
catch (e) {
reject(e);
}
});
});
this._promise.catch(e => rejectEndpoint(e));
}
async getEndpoint() {
return await this._endpointPromise;
}
cancel() {
if (!this.cancelled) {
this.cancelled = true;
if (this.process) {
this.process.kill("SIGINT");
}
}
return this._promise;
}
async promise() {
await this._promise;
}
}
exports.Instance = Instance;