@netlify/content-engine
Version:
329 lines • 13.3 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.exampleImpl = exports.ControllableScript = exports.getDebugInfo = void 0;
const path_1 = __importDefault(require("path"));
const execa_1 = __importDefault(require("execa"));
const detect_port_in_use_and_prompt_1 = require("../utils/detect-port-in-use-and-prompt");
const fs_extra_1 = __importDefault(require("fs-extra"));
const signal_exit_1 = __importDefault(require("signal-exit"));
const uuid_1 = require("../core-utils/uuid");
const core_utils_1 = require("../core-utils");
const reporter_1 = __importDefault(require("../reporter"));
const get_ssl_cert_1 = require("../utils/get-ssl-cert");
// Return a user-supplied port otherwise the default Node.js debugging port
const getDebugPort = (port) => port ?? 9229;
const getDebugInfo = (program) => {
if (Object.prototype.hasOwnProperty.call(program, `inspect`)) {
return {
port: getDebugPort(program.inspect),
break: false,
};
}
else if (Object.prototype.hasOwnProperty.call(program, `inspectBrk`)) {
return {
port: getDebugPort(program.inspectBrk),
break: true,
};
}
else {
return null;
}
};
exports.getDebugInfo = getDebugInfo;
class ControllableScript {
process;
script;
debugInfo;
shouldPrintLogs = true;
isRunning;
constructor(script, debugInfo, options) {
this.script = script;
this.debugInfo = debugInfo;
if (typeof options?.printLogs !== `undefined` &&
options.printLogs === false) {
this.shouldPrintLogs = false;
}
}
// todo this is kinda naive. if multiple processes start with the same name, we possibly wont get them all due to a race condition. this only works when starting multiple subprocesses in the same process without calling .stop() in between. We'd need to use a lockfile or something to make this work reliably across processes.
maybeKillZombie(processName, directory) {
const dirname = path_1.default.join(directory, `.cache`);
const pidFileName = dirname + `/pids/${processName}.pid`;
if (fs_extra_1.default.existsSync(pidFileName)) {
const pid = fs_extra_1.default.readFileSync(pidFileName, `utf-8`);
if (pid) {
setTimeout(() => {
try {
process.kill(Number(pid), `SIGKILL`);
console.info(`[content-engine] killing zombie process ${pid} for ${processName} in directory ${directory}.\nYou may need to ensure you're calling engine.stop() when you're done.`);
}
catch (e) {
// ignore if kill fails, maybe it's already dead, maybe we don't own the process for some reason
}
// wait 100ms to give the process a chance to die on its own incase we just restarted it
}, 100);
}
}
}
start(options) {
if (!options) {
options = {};
}
if (!options.env) {
options.env = {};
}
if (!options.directory) {
options.directory = process.cwd();
}
const args = [];
const dirname = path_1.default.join(options.directory, `.cache`);
const processName = `content-engine-process-${(0, core_utils_1.md5)(dirname)}`;
const tmpFileName = path_1.default.join(dirname, `${processName}.cjs`);
this.maybeKillZombie(processName, options.directory);
fs_extra_1.default.outputFileSync(tmpFileName, this.script);
this.isRunning = true;
// Passing --inspect isn't necessary for the child process to launch a port but it allows some editors to automatically attach
if (this.debugInfo) {
if (this.debugInfo.break) {
args.push(`--inspect-brk=${this.debugInfo.port}`);
}
else {
args.push(`--inspect=${this.debugInfo.port}`);
}
}
const defaultExecArgv = process.execArgv.filter((arg) => !arg.startsWith(`--inspect`));
this.process = execa_1.default.node(tmpFileName, args, {
env: {
FORCE_COLOR: `1`,
GATSBY_NODE_GLOBALS: JSON.stringify(global.__GATSBY ?? {}),
NODE_ENV: process.env.NODE_ENV,
...options.env,
},
stdio: [`ignore`, `pipe`, `pipe`, `ipc`],
cwd: options.directory,
nodeOptions: [`--enable-source-maps`, ...defaultExecArgv],
});
if (!this.process.stdout || !this.process.stderr) {
// to make TS happy
throw new Error(`Somehow the process is undefined immediately after starting it`);
}
if (this.shouldPrintLogs) {
// @ts-ignore FIXME(ndhoule): Not sure why this is angry, the type should be fine
this.process.stdout.pipe(process.stdout);
// @ts-ignore FIXME(ndhoule): Not sure why this is angry, the type should be fine
this.process.stderr.pipe(process.stderr);
}
// Write the pid to a file so we can kill it later if it becomes a zombie process
if (this.process?.pid) {
const pidDir = path_1.default.join(dirname, `pids`);
fs_extra_1.default.ensureDirSync(pidDir);
fs_extra_1.default.writeFileSync(path_1.default.join(pidDir + `/${processName}.pid`), this.process.pid.toString());
}
}
stop(signal = null, code) {
if (!this.process) {
throw new Error(`Trying to stop the process before starting it`);
}
let killTimeout;
try {
if (signal) {
this.process.kill(signal);
}
else {
// If the process doesn't exit within 1 second, kill it
killTimeout = setTimeout(() => {
if (this.process) {
const killWith = typeof code === `undefined` ? `SIGKILL` : code;
this.process.kill(killWith);
}
}, 1000);
this.process.send({
type: `COMMAND`,
action: {
type: `EXIT`,
payload: code,
},
}, () => {
// The try/catch won't suffice for this process.send
// So use the callback to manually catch the Error, otherwise it'll be thrown
// Ref: https://nodejs.org/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback
});
}
}
catch (err) {
// Ignore error if process has crashed or already quit.
// Ref: https://github.com/gatsbyjs/gatsby/issues/28011#issuecomment-877302917
}
return new Promise((resolve) => {
if (!this.process) {
throw new Error(`Trying to stop the process before starting it`);
}
this.process.on(`exit`, () => {
if (this.process) {
this.process.removeAllListeners();
}
if (killTimeout)
clearTimeout(killTimeout);
this.process = undefined;
this.isRunning = false;
resolve();
});
});
}
onMessage(callback) {
if (!this.process) {
throw new Error(`Trying to attach message handler before process started`);
}
this.process.on(`message`, callback);
}
offMessage(callback) {
if (!this.process) {
throw new Error(`Trying to detach message handler before process started`);
}
this.process.off(`message`, callback);
}
onExit(callback) {
if (!this.process) {
throw new Error(`Trying to attach exit handler before process started`);
}
this.process.on(`exit`, callback);
}
offExit(callback) {
if (!this.process) {
throw new Error(`Trying to detach exit handler before process started`);
}
this.process.off(`exit`, callback);
}
send(msg) {
if (!this.process) {
throw new Error(`Trying to send a message before process started`);
}
this.process.send(msg);
}
}
exports.ControllableScript = ControllableScript;
let isRestarting;
const exampleImpl = async (program) => {
global.__GATSBY = {
buildId: (0, uuid_1.v4)(),
root: program.directory,
};
// In some cases, port can actually be a string. But our codebase is expecting it to be a number.
// So we want to early just force it to a number to ensure we always act on a correct type.
program.port = parseInt(program.port + ``, 10);
const developProcessPath = (0, core_utils_1.slash)(require.resolve(`./develop-process`));
try {
program.port = await (0, detect_port_in_use_and_prompt_1.detectPortInUseAndPrompt)(program.port, program.host);
}
catch (e) {
if (e.message === `USER_REJECTED`) {
process.exit(0);
}
throw e;
}
// Run the actual develop server on a random port, and the proxy on the program port
// which users will access
const debugInfo = (0, exports.getDebugInfo)(program);
const developPort = program.port;
// In order to enable custom ssl, --cert-file --key-file and -https flags must all be
// used together
if ((program[`cert-file`] || program[`key-file`]) && !program.https) {
reporter_1.default.panic(`for custom ssl --https, --cert-file, and --key-file must be used together`);
}
// Check if https is enabled, then create or get SSL cert.
// Certs are named 'devcert' and issued to the host.
// NOTE(@mxstbr): We mutate program.ssl _after_ passing it
// to the develop process controllable script above because
// that would mean we double SSL browser => proxy => server
if (program.https) {
const sslHost = program.host === `0.0.0.0` || program.host === `::`
? `localhost`
: program.host;
const ssl = await (0, get_ssl_cert_1.getSslCert)({
name: sslHost,
caFile: program[`ca-file`],
certFile: program[`cert-file`],
keyFile: program[`key-file`],
directory: program.directory,
});
if (ssl) {
program.ssl = ssl;
}
}
const developProcess = new ControllableScript(`
const cmd = require(${JSON.stringify(developProcessPath)});
const args = ${JSON.stringify({
...program,
port: developPort,
// TODO(v5): remove
proxyPort: developPort,
debugInfo,
})};
cmd(args);
`, debugInfo);
const handleChildProcessIPC = (msg) => {
if (msg.type === `HEARTBEAT`)
return;
if (process.send) {
// Forward IPC
process.send(msg);
}
};
developProcess.start();
developProcess.onMessage(handleChildProcessIPC);
// Plugins can call `process.exit` which would be sent to `develop-process` (child process)
// This needs to be propagated back to the parent process
developProcess.onExit((code, signal) => {
if (isRestarting)
return;
if (signal !== null) {
process.kill(process.pid, signal);
return;
}
if (code !== null) {
process.exit(code);
}
// This should not happen:
// https://nodejs.org/api/child_process.html#child_process_event_exit
// The 'exit' event is emitted after the child process ends. If the process
// exited, code is the final exit code of the process, otherwise null.
// If the process terminated due to receipt of a signal, signal is the
// string name of the signal, otherwise null. One of the two will always be
// non - null.
//
// but just in case let do non-zero exit, because we are in situation
// we don't expect to be possible
process.exit(1);
});
// route ipc messaging to the original develop process
process.on(`message`, (msg) => {
developProcess.send(msg);
});
process.on(`SIGINT`, async () => {
await shutdownServices({
developProcess,
}, `SIGINT`);
process.exit(0);
});
process.on(`SIGTERM`, async () => {
await shutdownServices({
developProcess,
}, `SIGTERM`);
process.exit(0);
});
(0, signal_exit_1.default)((_code, signal) => {
shutdownServices({
developProcess,
}, signal);
});
};
exports.exampleImpl = exampleImpl;
function shutdownServices({ developProcess }, signal) {
const services = [developProcess.stop(signal)];
return Promise.all(services)
.catch(() => { })
.then(() => { });
}
//# sourceMappingURL=controllable-script.js.map
;