UNPKG

@netlify/content-engine

Version:
329 lines 13.3 kB
"use strict"; 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