Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(node): add child_process.execSync() #2689

Merged
merged 4 commits into from
Sep 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions node/_tools/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"test-child-process-exec-kill-throws.js",
"test-child-process-exec-maxbuf.js",
"test-child-process-exec-std-encoding.js",
"test-child-process-execsync-maxbuf.js",
"test-child-process-spawnsync-env.js",
"test-child-process-spawnsync-maxbuf.js",
"test-console-instance.js",
Expand Down Expand Up @@ -64,6 +65,7 @@
"test-path.js",
"test-querystring.js",
"test-readline-interface.js",
"test-stdin-from-file-spawn.js",
"test-stream-writable-change-default-encoding.js",
"test-url-urltooptions.js",
"test-util-format.js",
Expand Down Expand Up @@ -180,6 +182,7 @@
"test-child-process-exec-maxbuf.js",
"test-child-process-exec-std-encoding.js",
"test-child-process-exec-stdout-stderr-data-string.js",
"test-child-process-execsync-maxbuf.js",
"test-child-process-kill.js",
"test-child-process-spawnsync-args.js",
"test-child-process-spawnsync-env.js",
Expand Down Expand Up @@ -478,6 +481,7 @@
"test-readline-set-raw-mode.js",
"test-readline-undefined-columns.js",
"test-readline.js",
"test-stdin-from-file-spawn.js",
"test-stream-add-abort-signal.js",
"test-stream-aliases-legacy.js",
"test-stream-auto-destroy.js",
Expand Down Expand Up @@ -704,6 +708,7 @@
"test-child-process-exec-maxbuf.js",
"test-child-process-exec-std-encoding.js",
"test-child-process-exec-stdout-stderr-data-string.js",
"test-child-process-execsync-maxbuf.js",
"test-child-process-kill.js",
"test-child-process-spawnsync-args.js",
"test-console-log-throw-primitive.js",
Expand Down
76 changes: 76 additions & 0 deletions node/_tools/test/parallel/test-child-process-execsync-maxbuf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file

// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 18.8.0
// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually

// TODO(cjihrig): This should use Node's -e instead of Deno's eval CLI arg.

'use strict';
require('../common');

// This test checks that the maxBuffer option for child_process.spawnSync()
// works as expected.

const assert = require('assert');
const { getSystemErrorName } = require('util');
const { execSync } = require('child_process');
const msgOut = 'this is stdout';
const msgOutBuf = Buffer.from(`${msgOut}\n`);

const args = [
'eval',
`"console.log('${msgOut}')";`,
];

// Verify that an error is returned if maxBuffer is surpassed.
{
assert.throws(() => {
execSync(`"${process.execPath}" ${args.join(' ')}`, { maxBuffer: 1 });
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(e.errno), 'ENOBUFS');
// We can have buffers larger than maxBuffer because underneath we alloc 64k
// that matches our read sizes.
assert.deepStrictEqual(e.stdout, msgOutBuf);
return true;
});
}

// Verify that a maxBuffer size of Infinity works.
{
const ret = execSync(
`"${process.execPath}" ${args.join(' ')}`,
{ maxBuffer: Infinity }
);

assert.deepStrictEqual(ret, msgOutBuf);
}

// Default maxBuffer size is 1024 * 1024.
{
assert.throws(() => {
execSync(
`"${process.execPath}" eval "console.log('a'.repeat(1024 * 1024))"`
);
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(e.errno), 'ENOBUFS');
return true;
});
}

// Default maxBuffer size is 1024 * 1024.
{
const ret = execSync(
`"${process.execPath}" eval "console.log('a'.repeat(1024 * 1024 - 1))"`
);

assert.deepStrictEqual(
ret.toString().trim(),
'a'.repeat(1024 * 1024 - 1)
);
}
52 changes: 52 additions & 0 deletions node/_tools/test/parallel/test-stdin-from-file-spawn.js
Original file line number Diff line number Diff line change
@@ -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.8.0
// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually

// TODO(cjihrig): 'run -A --unstable require.ts' should not be needed in
// execSync() call at the bottom of this test.

'use strict';
const common = require('../common');
const process = require('process');

let defaultShell;
if (process.platform === 'linux' || process.platform === 'darwin') {
defaultShell = '/bin/sh';
} else if (process.platform === 'win32') {
defaultShell = 'cmd.exe';
} else {
common.skip('This is test exists only on Linux/Win32/OSX');
}

const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');

const tmpDir = tmpdir.path;
tmpdir.refresh();
const tmpCmdFile = path.join(tmpDir, 'test-stdin-from-file-spawn-cmd');
const tmpJsFile = path.join(tmpDir, 'test-stdin-from-file-spawn.js');
fs.writeFileSync(tmpCmdFile, 'echo hello');
fs.writeFileSync(tmpJsFile, `
'use strict';
const { spawn } = require('child_process');
// Reference the object to invoke the getter
process.stdin;
setTimeout(() => {
let ok = false;
const child = spawn(process.env.SHELL || '${defaultShell}',
[], { stdio: ['inherit', 'pipe'] });
child.stdout.on('data', () => {
ok = true;
});
child.on('close', () => {
process.exit(ok ? 0 : -1);
});
}, 100);
`);

execSync(`${process.argv[0]} run -A --unstable require.ts ${tmpJsFile} < ${tmpCmdFile}`);
49 changes: 41 additions & 8 deletions node/child_process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ import {
ERR_CHILD_PROCESS_STDIO_MAXBUFFER,
ERR_INVALID_ARG_VALUE,
ERR_OUT_OF_RANGE,
genericNodeError,
} from "./internal/errors.ts";
import {
ArrayPrototypeJoin,
ArrayPrototypePush,
ObjectAssign,
StringPrototypeSlice,
} from "./internal/primordials.mjs";
import { getSystemErrorName, promisify } from "./util.ts";
Expand Down Expand Up @@ -195,13 +198,13 @@ export function spawnSync(
interface ExecOptions extends
Pick<
ChildProcessOptions,
| "cwd"
| "env"
| "signal"
| "uid"
| "gid"
| "windowsHide"
> {
cwd?: string | URL;
encoding?: string;
/**
* Shell to execute the command with.
Expand All @@ -220,13 +223,12 @@ type ExecCallback = (
stdout?: string | Buffer,
stderr?: string | Buffer,
) => void;

type ExecSyncOptions = SpawnSyncOptions;
function normalizeExecArgs(
command: string,
optionsOrCallback?: ExecOptions | ExecCallback,
optionsOrCallback?: ExecOptions | ExecSyncOptions | ExecCallback,
maybeCallback?: ExecCallback,
) {
let options: ExecFileOptions | undefined = undefined;
let callback: ExecFileCallback | undefined = maybeCallback;

if (typeof optionsOrCallback === "function") {
Expand All @@ -235,7 +237,7 @@ function normalizeExecArgs(
}

// Make a shallow copy so we don't clobber the user's options object.
options = { ...optionsOrCallback };
const options: ExecOptions | ExecSyncOptions = { ...optionsOrCallback };
options.shell = typeof options.shell === "string" ? options.shell : true;

return {
Expand All @@ -262,7 +264,7 @@ export function exec(
maybeCallback?: ExecCallback,
): ChildProcess {
const opts = normalizeExecArgs(command, optionsOrCallback, maybeCallback);
return execFile(opts.file, opts.options, opts.callback);
return execFile(opts.file, opts.options as ExecFileOptions, opts.callback);
}

interface PromiseWithChild<T> extends Promise<T> {
Expand Down Expand Up @@ -603,8 +605,39 @@ export function execFile(
return child;
}

export function execSync() {
throw new Error("execSync is currently not supported");
function checkExecSyncError(ret: SpawnSyncResult, args: string[], cmd: string) {
let err;
if (ret.error) {
err = ret.error;
ObjectAssign(err, ret);
} else if (ret.status !== 0) {
let msg = "Command failed: ";
msg += cmd || ArrayPrototypeJoin(args, " ");
if (ret.stderr && ret.stderr.length > 0) {
msg += `\n${ret.stderr.toString()}`;
}
err = genericNodeError(msg, ret);
}
return err;
}

export function execSync(command: string, options: ExecSyncOptions) {
const opts = normalizeExecArgs(command, options);
const inheritStderr = !(opts.options as ExecSyncOptions).stdio;

const ret = spawnSync(opts.file, opts.options as SpawnSyncOptions);

if (inheritStderr && ret.stderr) {
process.stderr.write(ret.stderr);
}

const err = checkExecSyncError(ret, [], command);

if (err) {
throw err;
}

return ret.stdout;
}

export default {
Expand Down