Skip to content

Commit 755ad76

Browse files
authored
Add a subpackage for running child processes synchronously (#81)
0 parents  commit 755ad76

File tree

4 files changed

+488
-0
lines changed

4 files changed

+488
-0
lines changed

lib/event.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2021 Google LLC. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
/** An event emitted by the child process. */
6+
export type Event = StdoutEvent | StderrEvent | ExitEvent;
7+
8+
/** An event sent from the worker to the host. */
9+
export type InternalEvent =
10+
| InternalStdoutEvent
11+
| InternalStderrEvent
12+
| ExitEvent
13+
| ErrorEvent;
14+
15+
/** An event indicating that data has been emitted over stdout. */
16+
export interface StdoutEvent {
17+
type: 'stdout';
18+
data: Buffer;
19+
}
20+
21+
/** An event indicating that data has been emitted over stderr. */
22+
export interface StderrEvent {
23+
type: 'stderr';
24+
data: Buffer;
25+
}
26+
27+
/** An event indicating that process has exited. */
28+
export interface ExitEvent {
29+
type: 'exit';
30+
31+
/**
32+
* The exit code. This will be `undefined` if the subprocess was killed via
33+
* signal.
34+
*/
35+
code?: number;
36+
37+
/**
38+
* The signal that caused this process to exit. This will be `undefined` if
39+
* the subprocess exited normally.
40+
*/
41+
signal?: NodeJS.Signals;
42+
}
43+
44+
/**
45+
* The stdout event sent from the worker to the host. The structured clone
46+
* algorithm automatically converts `Buffer`s sent through `MessagePort`s to
47+
* `Uint8Array`s.
48+
*/
49+
export interface InternalStdoutEvent {
50+
type: 'stdout';
51+
data: Buffer | Uint8Array;
52+
}
53+
54+
/**
55+
* The stderr event sent from the worker to the host. The structured clone
56+
* algorithm automatically converts `Buffer`s sent through `MessagePort`s to
57+
* `Uint8Array`s.
58+
*/
59+
export interface InternalStderrEvent {
60+
type: 'stderr';
61+
data: Buffer | Uint8Array;
62+
}
63+
64+
/**
65+
* An error occurred when starting or closing the child process. This is only
66+
* used internally; the host will throw the error rather than returning it to
67+
* the caller.
68+
*/
69+
export interface ErrorEvent {
70+
type: 'error';
71+
error: Error;
72+
}

lib/index.test.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright 2021 Google LLC. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import * as fs from 'fs';
6+
import * as p from 'path';
7+
import * as del from 'del';
8+
9+
import {Event, StderrEvent, StdoutEvent, SyncProcess} from './index';
10+
11+
describe('SyncProcess', () => {
12+
describe('stdio', () => {
13+
it('emits stdout', () => {
14+
withJSProcess('console.log("hello, world!");', node => {
15+
expectStdout(node.yield(), 'hello, world!\n');
16+
});
17+
});
18+
19+
it('emits stderr', () => {
20+
withJSProcess('console.error("hello, world!");', node => {
21+
expectStderr(node.yield(), 'hello, world!\n');
22+
});
23+
});
24+
25+
it('receives stdin', () => {
26+
withJSProcess(
27+
'process.stdin.on("data", (data) => process.stdout.write(data));',
28+
node => {
29+
node.stdin.write('hi there!\n');
30+
expectStdout(node.yield(), 'hi there!\n');
31+
node.stdin.write('fblthp\n');
32+
expectStdout(node.yield(), 'fblthp\n');
33+
}
34+
);
35+
});
36+
37+
it('closes stdin', () => {
38+
withJSProcess(
39+
`
40+
process.stdin.on("data", () => {});
41+
process.stdin.on("end", () => console.log("closed!"));
42+
`,
43+
node => {
44+
node.stdin.end();
45+
expectStdout(node.yield(), 'closed!\n');
46+
}
47+
);
48+
});
49+
});
50+
51+
describe('emits exit', () => {
52+
it('with code 0 by default', () => {
53+
withJSProcess('', node => {
54+
expectExit(node.yield(), 0);
55+
});
56+
});
57+
58+
it('with a non-0 code', () => {
59+
withJSProcess('process.exit(123);', node => {
60+
expectExit(node.yield(), 123);
61+
});
62+
});
63+
64+
it('with a signal code', () => {
65+
withJSProcess('for (;;) {}', node => {
66+
node.kill('SIGINT');
67+
expectExit(node.yield(), 'SIGINT');
68+
});
69+
});
70+
});
71+
72+
it('passes options to the subprocess', () => {
73+
withJSFile('console.log(process.env.SYNC_PROCESS_TEST);', file => {
74+
const node = new SyncProcess(process.argv0, [file], {
75+
env: {...process.env, SYNC_PROCESS_TEST: 'abcdef'},
76+
});
77+
expectStdout(node.yield(), 'abcdef\n');
78+
node.kill();
79+
});
80+
});
81+
});
82+
83+
/** Asserts that `event` is a `StdoutEvent` with text `text`. */
84+
function expectStdout(event: Event, text: string): void {
85+
if (event.type === 'stderr') {
86+
throw `Expected stdout event, was stderr event: ${event.data.toString()}`;
87+
}
88+
89+
expect(event.type).toEqual('stdout');
90+
expect((event as StdoutEvent).data.toString()).toEqual(text);
91+
}
92+
93+
/** Asserts that `event` is a `StderrEvent` with text `text`. */
94+
function expectStderr(event: Event, text: string): void {
95+
if (event.type === 'stdout') {
96+
throw `Expected stderr event, was stdout event: ${event.data.toString()}`;
97+
}
98+
99+
expect(event.type).toEqual('stderr');
100+
expect((event as StderrEvent).data.toString()).toEqual(text);
101+
}
102+
103+
/**
104+
* Asserts that `event` is an `ExitEvent` with either the given exit code (if
105+
* `codeOrSignal` is a number) or signal (if `codeOrSignal` is a string).
106+
*/
107+
function expectExit(event: Event, codeOrSignal: number | NodeJS.Signals): void {
108+
if (event.type !== 'exit') {
109+
throw (
110+
`Expected exit event, was ${event.type} event: ` + event.data.toString()
111+
);
112+
}
113+
114+
expect(event).toEqual(
115+
typeof codeOrSignal === 'number'
116+
? {type: 'exit', code: codeOrSignal}
117+
: {type: 'exit', signal: codeOrSignal}
118+
);
119+
}
120+
121+
/**
122+
* Starts a `SyncProcess` running a JS file with the given `contents` and passes
123+
* it to `callback`.
124+
*/
125+
function withJSProcess(
126+
contents: string,
127+
callback: (process: SyncProcess) => void
128+
): void {
129+
return withJSFile(contents, file => {
130+
const node = new SyncProcess(process.argv0, [file]);
131+
132+
try {
133+
callback(node);
134+
} finally {
135+
node.kill();
136+
}
137+
});
138+
}
139+
140+
/**
141+
* Creates a JS file with the given `contents` for the duration of `callback`.
142+
*
143+
* The `callback` is passed the name of the created file.
144+
*/
145+
function withJSFile(contents: string, callback: (file: string) => void): void {
146+
const testDir = p.join('spec', 'sandbox', `${Math.random()}`.slice(2));
147+
fs.mkdirSync(testDir, {recursive: true});
148+
const file = p.join(testDir, 'script.js');
149+
fs.writeFileSync(file, contents);
150+
151+
try {
152+
callback(file);
153+
} finally {
154+
// TODO(awjin): Change this to rmSync once we drop support for Node 12.
155+
del.sync(testDir, {force: true});
156+
}
157+
}

0 commit comments

Comments
 (0)