1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-25 15:29:32 -05:00

fix(ext/node): Implement detached option in child_process (#25218)

Fixes https://github.com/denoland/deno/issues/25193.
This commit is contained in:
Nathan Whitaker 2024-09-12 12:24:58 -07:00 committed by GitHub
parent 3f15e30062
commit 18b89d948d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 136 additions and 20 deletions

View file

@ -56,6 +56,7 @@ import { StringPrototypeSlice } from "ext:deno_node/internal/primordials.mjs";
import { StreamBase } from "ext:deno_node/internal_binding/stream_wrap.ts"; import { StreamBase } from "ext:deno_node/internal_binding/stream_wrap.ts";
import { Pipe, socketType } from "ext:deno_node/internal_binding/pipe_wrap.ts"; import { Pipe, socketType } from "ext:deno_node/internal_binding/pipe_wrap.ts";
import { Socket } from "node:net"; import { Socket } from "node:net";
import { kDetached, kExtraStdio, kIpc } from "ext:runtime/40_process.js";
export function mapValues<T, O>( export function mapValues<T, O>(
record: Readonly<Record<string, T>>, record: Readonly<Record<string, T>>,
@ -109,6 +110,7 @@ export function stdioStringToArray(
const kClosesNeeded = Symbol("_closesNeeded"); const kClosesNeeded = Symbol("_closesNeeded");
const kClosesReceived = Symbol("_closesReceived"); const kClosesReceived = Symbol("_closesReceived");
const kCanDisconnect = Symbol("_canDisconnect");
// We only want to emit a close event for the child process when all of // We only want to emit a close event for the child process when all of
// the writable streams have closed. The value of `child[kClosesNeeded]` should be 1 + // the writable streams have closed. The value of `child[kClosesNeeded]` should be 1 +
@ -222,7 +224,7 @@ export class ChildProcess extends EventEmitter {
#spawned = Promise.withResolvers<void>(); #spawned = Promise.withResolvers<void>();
[kClosesNeeded] = 1; [kClosesNeeded] = 1;
[kClosesReceived] = 0; [kClosesReceived] = 0;
canDisconnect = false; [kCanDisconnect] = false;
constructor( constructor(
command: string, command: string,
@ -238,6 +240,7 @@ export class ChildProcess extends EventEmitter {
shell = false, shell = false,
signal, signal,
windowsVerbatimArguments = false, windowsVerbatimArguments = false,
detached,
} = options || {}; } = options || {};
const normalizedStdio = normalizeStdioOption(stdio); const normalizedStdio = normalizeStdioOption(stdio);
const [ const [
@ -275,8 +278,9 @@ export class ChildProcess extends EventEmitter {
stdout: toDenoStdio(stdout), stdout: toDenoStdio(stdout),
stderr: toDenoStdio(stderr), stderr: toDenoStdio(stderr),
windowsRawArguments: windowsVerbatimArguments, windowsRawArguments: windowsVerbatimArguments,
ipc, // internal [kIpc]: ipc, // internal
extraStdio: extraStdioNormalized, [kExtraStdio]: extraStdioNormalized,
[kDetached]: detached,
}).spawn(); }).spawn();
this.pid = this.#process.pid; this.pid = this.#process.pid;
@ -387,8 +391,8 @@ export class ChildProcess extends EventEmitter {
this.emit("exit", exitCode, signalCode); this.emit("exit", exitCode, signalCode);
await this.#_waitForChildStreamsToClose(); await this.#_waitForChildStreamsToClose();
this.#closePipes(); this.#closePipes();
maybeClose(this);
nextTick(flushStdio, this); nextTick(flushStdio, this);
maybeClose(this);
}); });
})(); })();
} catch (err) { } catch (err) {
@ -421,7 +425,7 @@ export class ChildProcess extends EventEmitter {
} }
/* Cancel any pending IPC I/O */ /* Cancel any pending IPC I/O */
if (this.canDisconnect) { if (this[kCanDisconnect]) {
this.disconnect?.(); this.disconnect?.();
} }
@ -552,7 +556,7 @@ export interface ChildProcessOptions {
stdio?: Array<NodeStdio | number | Stream | null | undefined> | NodeStdio; stdio?: Array<NodeStdio | number | Stream | null | undefined> | NodeStdio;
/** /**
* NOTE: This option is not yet implemented. * Whether to spawn the process in a detached state.
*/ */
detached?: boolean; detached?: boolean;
@ -1416,7 +1420,7 @@ export function setupChannel(target: any, ipc: number) {
} }
target.connected = false; target.connected = false;
target.canDisconnect = false; target[kCanDisconnect] = false;
control[kControlDisconnect](); control[kControlDisconnect]();
process.nextTick(() => { process.nextTick(() => {
target.channel = null; target.channel = null;
@ -1424,7 +1428,7 @@ export function setupChannel(target: any, ipc: number) {
target.emit("disconnect"); target.emit("disconnect");
}); });
}; };
target.canDisconnect = true; target[kCanDisconnect] = true;
// Start reading messages from the channel. // Start reading messages from the channel.
readLoop(); readLoop();

View file

@ -267,9 +267,11 @@ memoryUsage.rss = function (): number {
// Returns a negative error code than can be recognized by errnoException // Returns a negative error code than can be recognized by errnoException
function _kill(pid: number, sig: number): number { function _kill(pid: number, sig: number): number {
const maybeMapErrno = (res: number) =>
res === 0 ? res : uv.mapSysErrnoToUvErrno(res);
// signal 0 does not exist in constants.os.signals, thats why it have to be handled explicitly // signal 0 does not exist in constants.os.signals, thats why it have to be handled explicitly
if (sig === 0) { if (sig === 0) {
return op_node_process_kill(pid, 0); return maybeMapErrno(op_node_process_kill(pid, 0));
} }
const maybeSignal = Object.entries(constants.os.signals).find(( const maybeSignal = Object.entries(constants.os.signals).find((
[_, numericCode], [_, numericCode],
@ -278,7 +280,7 @@ function _kill(pid: number, sig: number): number {
if (!maybeSignal) { if (!maybeSignal) {
return uv.codeMap.get("EINVAL"); return uv.codeMap.get("EINVAL");
} }
return op_node_process_kill(pid, sig); return maybeMapErrno(op_node_process_kill(pid, sig));
} }
export function dlopen(module, filename, _flags) { export function dlopen(module, filename, _flags) {

View file

@ -157,6 +157,10 @@ function run({
return new Process(res); return new Process(res);
} }
export const kExtraStdio = Symbol("extraStdio");
export const kIpc = Symbol("ipc");
export const kDetached = Symbol("detached");
const illegalConstructorKey = Symbol("illegalConstructorKey"); const illegalConstructorKey = Symbol("illegalConstructorKey");
function spawnChildInner(command, apiName, { function spawnChildInner(command, apiName, {
@ -166,13 +170,14 @@ function spawnChildInner(command, apiName, {
env = { __proto__: null }, env = { __proto__: null },
uid = undefined, uid = undefined,
gid = undefined, gid = undefined,
signal = undefined,
stdin = "null", stdin = "null",
stdout = "piped", stdout = "piped",
stderr = "piped", stderr = "piped",
signal = undefined,
windowsRawArguments = false, windowsRawArguments = false,
ipc = -1, [kDetached]: detached = false,
extraStdio = [], [kExtraStdio]: extraStdio = [],
[kIpc]: ipc = -1,
} = { __proto__: null }) { } = { __proto__: null }) {
const child = op_spawn_child({ const child = op_spawn_child({
cmd: pathFromURL(command), cmd: pathFromURL(command),
@ -188,6 +193,7 @@ function spawnChildInner(command, apiName, {
windowsRawArguments, windowsRawArguments,
ipc, ipc,
extraStdio, extraStdio,
detached,
}, apiName); }, apiName);
return new ChildProcess(illegalConstructorKey, { return new ChildProcess(illegalConstructorKey, {
...child, ...child,
@ -414,6 +420,7 @@ function spawnSync(command, {
stderr, stderr,
windowsRawArguments, windowsRawArguments,
extraStdio: [], extraStdio: [],
detached: false,
}); });
return { return {
success: result.status.success, success: result.status.success,

View file

@ -159,6 +159,7 @@ pub struct SpawnArgs {
stdio: ChildStdio, stdio: ChildStdio,
extra_stdio: Vec<Stdio>, extra_stdio: Vec<Stdio>,
detached: bool,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -243,6 +244,14 @@ fn create_command(
let mut command = std::process::Command::new(cmd); let mut command = std::process::Command::new(cmd);
#[cfg(windows)] #[cfg(windows)]
{
if args.detached {
// TODO(nathanwhit): Currently this causes the process to hang
// until the detached process exits (so never). It repros with just the
// rust std library, so it's either a bug or requires more control than we have.
// To be resolved at the same time as additional stdio support.
log::warn!("detached processes are not currently supported on Windows");
}
if args.windows_raw_arguments { if args.windows_raw_arguments {
for arg in args.args.iter() { for arg in args.args.iter() {
command.raw_arg(arg); command.raw_arg(arg);
@ -250,6 +259,7 @@ fn create_command(
} else { } else {
command.args(args.args); command.args(args.args);
} }
}
#[cfg(not(windows))] #[cfg(not(windows))]
command.args(args.args); command.args(args.args);
@ -336,7 +346,11 @@ fn create_command(
} }
} }
let detached = args.detached;
command.pre_exec(move || { command.pre_exec(move || {
if detached {
libc::setsid();
}
for &(src, dst) in &fds_to_dup { for &(src, dst) in &fds_to_dup {
if src >= 0 && dst >= 0 { if src >= 0 && dst >= 0 {
let _fd = libc::dup2(src, dst); let _fd = libc::dup2(src, dst);
@ -402,12 +416,15 @@ fn spawn_child(
command: std::process::Command, command: std::process::Command,
ipc_pipe_rid: Option<ResourceId>, ipc_pipe_rid: Option<ResourceId>,
extra_pipe_rids: Vec<Option<ResourceId>>, extra_pipe_rids: Vec<Option<ResourceId>>,
detached: bool,
) -> Result<Child, AnyError> { ) -> Result<Child, AnyError> {
let mut command = tokio::process::Command::from(command); let mut command = tokio::process::Command::from(command);
// TODO(@crowlkats): allow detaching processes. // TODO(@crowlkats): allow detaching processes.
// currently deno will orphan a process when exiting with an error or Deno.exit() // currently deno will orphan a process when exiting with an error or Deno.exit()
// We want to kill child when it's closed // We want to kill child when it's closed
if !detached {
command.kill_on_drop(true); command.kill_on_drop(true);
}
let mut child = match command.spawn() { let mut child = match command.spawn() {
Ok(child) => child, Ok(child) => child,
@ -647,9 +664,10 @@ fn op_spawn_child(
#[serde] args: SpawnArgs, #[serde] args: SpawnArgs,
#[string] api_name: String, #[string] api_name: String,
) -> Result<Child, AnyError> { ) -> Result<Child, AnyError> {
let detached = args.detached;
let (command, pipe_rid, extra_pipe_rids, handles_to_close) = let (command, pipe_rid, extra_pipe_rids, handles_to_close) =
create_command(state, args, &api_name)?; create_command(state, args, &api_name)?;
let child = spawn_child(state, command, pipe_rid, extra_pipe_rids); let child = spawn_child(state, command, pipe_rid, extra_pipe_rids, detached);
for handle in handles_to_close { for handle in handles_to_close {
close_raw_handle(handle); close_raw_handle(handle);
} }

View file

@ -8,6 +8,7 @@
"elipses.txt", "elipses.txt",
"empty.txt", "empty.txt",
"exit.js", "exit.js",
"parent-process-nonpersistent.js",
"print-chars.js", "print-chars.js",
"x.txt" "x.txt"
], ],
@ -25,6 +26,7 @@
"test-buffer-from.js", "test-buffer-from.js",
"test-buffer-includes.js", "test-buffer-includes.js",
"test-buffer-indexof.js", "test-buffer-indexof.js",
"test-child-process-detached.js",
"test-child-process-exec-abortcontroller-promisified.js", "test-child-process-exec-abortcontroller-promisified.js",
"test-child-process-exec-encoding.js", "test-child-process-exec-encoding.js",
"test-child-process-exec-kill-throws.js", "test-child-process-exec-kill-throws.js",
@ -135,6 +137,7 @@
"fixtures": [ "fixtures": [
"a.js", "a.js",
"child_process_should_emit_error.js", "child_process_should_emit_error.js",
"child-process-persistent.js",
"child-process-spawn-node.js", "child-process-spawn-node.js",
"echo.js", "echo.js",
"elipses.txt", "elipses.txt",
@ -766,6 +769,7 @@
}, },
"windowsIgnore": { "windowsIgnore": {
"parallel": [ "parallel": [
"test-child-process-detached.js",
"test-child-process-exec-abortcontroller-promisified.js", "test-child-process-exec-abortcontroller-promisified.js",
"test-console-log-throw-primitive.js", "test-console-log-throw-primitive.js",
"test-console-no-swallow-stack-overflow.js", "test-console-no-swallow-stack-overflow.js",

View file

@ -0,0 +1,8 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 18.12.1
// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
setInterval(function() {}, 9999);

View file

@ -0,0 +1,21 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 18.12.1
// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
// Modified to add `runner.ts` to inject `require` into subprocess
const spawn = require('child_process').spawn,
path = require('path'),
childPath = path.join(__dirname, 'child-process-persistent.js');
var child = spawn(process.execPath, [ childPath ], {
detached: true,
stdio: 'ignore'
});
console.log(child.pid);
child.unref();

View file

@ -0,0 +1,52 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 18.12.1
// This file is automatically generated by `tests/node_compat/runner/setup.ts`. Do not modify this file manually.
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
// Modified to add `runner.ts` to inject `require` into subprocess
'use strict';
require('../common');
const assert = require('assert');
const fixtures = require('../common/fixtures');
const spawn = require('child_process').spawn;
const childPath = fixtures.path('parent-process-nonpersistent.js');
let persistentPid = -1;
const child = spawn(process.execPath, [ "runner.ts", childPath ]);
child.stdout.on('data', function(data) {
persistentPid = parseInt(data, 10);
});
process.on('exit', function() {
assert.notStrictEqual(persistentPid, -1);
assert.throws(function() {
process.kill(child.pid);
}, /^Error: kill ESRCH$/);
process.kill(persistentPid);
});