Skip to content

Commit

Permalink
feat(node): add child_process.execSync() (#2689)
Browse files Browse the repository at this point in the history
Refs: #2631
  • Loading branch information
cjihrig committed Sep 24, 2022
1 parent 5fb2569 commit 2fb18b3
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 8 deletions.
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 @@ -479,6 +482,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 @@ -705,6 +709,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

0 comments on commit 2fb18b3

Please sign in to comment.