Skip to content

Commit 9bc2cec

Browse files
tarrudadanielleadams
authored andcommitted
child_process: add 'overlapped' stdio flag
The 'overlapped' value sets the UV_OVERLAPPED_PIPE libuv flag in the child process stdio. Fixes: #29238 PR-URL: #29412 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 8b43388 commit 9bc2cec

File tree

8 files changed

+255
-8
lines changed

8 files changed

+255
-8
lines changed

doc/api/child_process.md

+16-6
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,9 @@ subprocess.unref();
660660
<!-- YAML
661661
added: v0.7.10
662662
changes:
663+
- version: REPLACEME
664+
pr-url: https://github.com/nodejs/node/pull/29412
665+
description: Added the `overlapped` stdio flag.
663666
- version: v3.3.1
664667
pr-url: https://github.com/nodejs/node/pull/2727
665668
description: The value `0` is now accepted as a file descriptor.
@@ -675,6 +678,7 @@ equal to `['pipe', 'pipe', 'pipe']`.
675678
For convenience, `options.stdio` may be one of the following strings:
676679

677680
* `'pipe'`: equivalent to `['pipe', 'pipe', 'pipe']` (the default)
681+
* `'overlapped'`: equivalent to `['overlapped', 'overlapped', 'overlapped']`
678682
* `'ignore'`: equivalent to `['ignore', 'ignore', 'ignore']`
679683
* `'inherit'`: equivalent to `['inherit', 'inherit', 'inherit']` or `[0, 1, 2]`
680684

@@ -688,7 +692,13 @@ pipes between the parent and child. The value is one of the following:
688692
`child_process` object as [`subprocess.stdio[fd]`][`subprocess.stdio`]. Pipes
689693
created for fds 0, 1, and 2 are also available as [`subprocess.stdin`][],
690694
[`subprocess.stdout`][] and [`subprocess.stderr`][], respectively.
691-
2. `'ipc'`: Create an IPC channel for passing messages/file descriptors
695+
1. `'overlapped'`: Same as `'pipe'` except that the `FILE_FLAG_OVERLAPPED` flag
696+
is set on the handle. This is necessary for overlapped I/O on the child
697+
process's stdio handles. See the
698+
[docs](https://docs.microsoft.com/en-us/windows/win32/fileio/synchronous-and-asynchronous-i-o)
699+
for more details. This is exactly the same as `'pipe'` on non-Windows
700+
systems.
701+
1. `'ipc'`: Create an IPC channel for passing messages/file descriptors
692702
between parent and child. A [`ChildProcess`][] may have at most one IPC
693703
stdio file descriptor. Setting this option enables the
694704
[`subprocess.send()`][] method. If the child is a Node.js process, the
@@ -699,25 +709,25 @@ pipes between the parent and child. The value is one of the following:
699709
Accessing the IPC channel fd in any way other than [`process.send()`][]
700710
or using the IPC channel with a child process that is not a Node.js instance
701711
is not supported.
702-
3. `'ignore'`: Instructs Node.js to ignore the fd in the child. While Node.js
712+
1. `'ignore'`: Instructs Node.js to ignore the fd in the child. While Node.js
703713
will always open fds 0, 1, and 2 for the processes it spawns, setting the fd
704714
to `'ignore'` will cause Node.js to open `/dev/null` and attach it to the
705715
child's fd.
706-
4. `'inherit'`: Pass through the corresponding stdio stream to/from the
716+
1. `'inherit'`: Pass through the corresponding stdio stream to/from the
707717
parent process. In the first three positions, this is equivalent to
708718
`process.stdin`, `process.stdout`, and `process.stderr`, respectively. In
709719
any other position, equivalent to `'ignore'`.
710-
5. {Stream} object: Share a readable or writable stream that refers to a tty,
720+
1. {Stream} object: Share a readable or writable stream that refers to a tty,
711721
file, socket, or a pipe with the child process. The stream's underlying
712722
file descriptor is duplicated in the child process to the fd that
713723
corresponds to the index in the `stdio` array. The stream must have an
714724
underlying descriptor (file streams do not until the `'open'` event has
715725
occurred).
716-
6. Positive integer: The integer value is interpreted as a file descriptor
726+
1. Positive integer: The integer value is interpreted as a file descriptor
717727
that is currently open in the parent process. It is shared with the child
718728
process, similar to how {Stream} objects can be shared. Passing sockets
719729
is not supported on Windows.
720-
7. `null`, `undefined`: Use default value. For stdio fds 0, 1, and 2 (in other
730+
1. `null`, `undefined`: Use default value. For stdio fds 0, 1, and 2 (in other
721731
words, stdin, stdout, and stderr) a pipe is created. For fd 3 and up, the
722732
default is `'ignore'`.
723733

lib/internal/child_process.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ function stdioStringToArray(stdio, channel) {
231231

232232
switch (stdio) {
233233
case 'ignore':
234+
case 'overlapped':
234235
case 'pipe': ArrayPrototypePush(options, stdio, stdio, stdio); break;
235236
case 'inherit': ArrayPrototypePush(options, 0, 1, 2); break;
236237
default:
@@ -976,9 +977,10 @@ function getValidStdio(stdio, sync) {
976977

977978
if (stdio === 'ignore') {
978979
ArrayPrototypePush(acc, { type: 'ignore' });
979-
} else if (stdio === 'pipe' || (typeof stdio === 'number' && stdio < 0)) {
980+
} else if (stdio === 'pipe' || stdio === 'overlapped' ||
981+
(typeof stdio === 'number' && stdio < 0)) {
980982
const a = {
981-
type: 'pipe',
983+
type: stdio === 'overlapped' ? 'overlapped' : 'pipe',
982984
readable: i === 0,
983985
writable: i !== 0
984986
};

node.gyp

+18
Original file line numberDiff line numberDiff line change
@@ -1469,6 +1469,24 @@
14691469
],
14701470
}, # embedtest
14711471

1472+
{
1473+
'target_name': 'overlapped-checker',
1474+
'type': 'executable',
1475+
1476+
'conditions': [
1477+
['OS=="win"', {
1478+
'sources': [
1479+
'test/overlapped-checker/main_win.c'
1480+
],
1481+
}],
1482+
['OS!="win"', {
1483+
'sources': [
1484+
'test/overlapped-checker/main_unix.c'
1485+
],
1486+
}],
1487+
]
1488+
}, # overlapped-checker
1489+
14721490
# TODO(joyeecheung): do not depend on node_lib,
14731491
# instead create a smaller static library node_lib_base that does
14741492
# just enough for node_native_module.cc and the cache builder to

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ constexpr size_t kFsStatsBufferLength =
343343
V(options_string, "options") \
344344
V(order_string, "order") \
345345
V(output_string, "output") \
346+
V(overlapped_string, "overlapped") \
346347
V(parse_error_string, "Parse Error") \
347348
V(password_string, "password") \
348349
V(path_string, "path") \

src/process_wrap.cc

+5
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ class ProcessWrap : public HandleWrap {
120120
options->stdio[i].flags = static_cast<uv_stdio_flags>(
121121
UV_CREATE_PIPE | UV_READABLE_PIPE | UV_WRITABLE_PIPE);
122122
options->stdio[i].data.stream = StreamForWrap(env, stdio);
123+
} else if (type->StrictEquals(env->overlapped_string())) {
124+
options->stdio[i].flags = static_cast<uv_stdio_flags>(
125+
UV_CREATE_PIPE | UV_READABLE_PIPE | UV_WRITABLE_PIPE |
126+
UV_OVERLAPPED_PIPE);
127+
options->stdio[i].data.stream = StreamForWrap(env, stdio);
123128
} else if (type->StrictEquals(env->wrap_string())) {
124129
options->stdio[i].flags = UV_INHERIT_STREAM;
125130
options->stdio[i].data.stream = StreamForWrap(env, stdio);

test/overlapped-checker/main_unix.c

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#include <stdio.h>
2+
#include <string.h>
3+
#include <stdlib.h>
4+
5+
#include <errno.h>
6+
#include <unistd.h>
7+
8+
static size_t r(char* buf, size_t buf_size) {
9+
ssize_t read_count;
10+
do
11+
read_count = read(0, buf, buf_size);
12+
while (read_count < 0 && errno == EINTR);
13+
if (read_count <= 0)
14+
abort();
15+
return (size_t)read_count;
16+
}
17+
18+
static void w(const char* buf, size_t count) {
19+
const char* end = buf + count;
20+
21+
while (buf < end) {
22+
ssize_t write_count;
23+
do
24+
write_count = write(1, buf, count);
25+
while (write_count < 0 && errno == EINTR);
26+
if (write_count <= 0)
27+
abort();
28+
buf += write_count;
29+
}
30+
31+
fprintf(stderr, "%zu", count);
32+
fflush(stderr);
33+
}
34+
35+
int main(void) {
36+
w("0", 1);
37+
38+
while (1) {
39+
char buf[256];
40+
size_t read_count = r(buf, sizeof(buf));
41+
// The JS part (test-child-process-stdio-overlapped.js) only writes the
42+
// "exit" string when the buffer is empty, so the read is guaranteed to be
43+
// atomic due to it being less than PIPE_BUF.
44+
if (!strncmp(buf, "exit", read_count)) {
45+
break;
46+
}
47+
w(buf, read_count);
48+
}
49+
50+
return 0;
51+
}

test/overlapped-checker/main_win.c

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#include <stdlib.h>
2+
#include <stdio.h>
3+
#include <string.h>
4+
5+
#include <windows.h>
6+
7+
static char buf[256];
8+
static DWORD read_count;
9+
static DWORD write_count;
10+
static HANDLE stdin_h;
11+
static OVERLAPPED stdin_o;
12+
13+
static void die(const char* buf) {
14+
fprintf(stderr, "%s\n", buf);
15+
fflush(stderr);
16+
exit(100);
17+
}
18+
19+
static void overlapped_read(void) {
20+
if (ReadFile(stdin_h, buf, sizeof(buf), NULL, &stdin_o)) {
21+
// Since we start the read operation immediately before requesting a write,
22+
// it should never complete synchronously since no data would be available
23+
die("read completed synchronously");
24+
}
25+
if (GetLastError() != ERROR_IO_PENDING) {
26+
die("overlapped read failed");
27+
}
28+
}
29+
30+
static void write(const char* buf, size_t buf_size) {
31+
overlapped_read();
32+
DWORD write_count;
33+
HANDLE stdout_h = GetStdHandle(STD_OUTPUT_HANDLE);
34+
if (!WriteFile(stdout_h, buf, buf_size, &write_count, NULL)) {
35+
die("overlapped write failed");
36+
}
37+
fprintf(stderr, "%d", write_count);
38+
fflush(stderr);
39+
}
40+
41+
int main(void) {
42+
HANDLE event = CreateEvent(NULL, FALSE, FALSE, NULL);
43+
if (event == NULL) {
44+
die("failed to create event handle");
45+
}
46+
47+
stdin_h = GetStdHandle(STD_INPUT_HANDLE);
48+
stdin_o.hEvent = event;
49+
50+
write("0", 1);
51+
52+
while (1) {
53+
DWORD result = WaitForSingleObject(event, INFINITE);
54+
if (result == WAIT_OBJECT_0) {
55+
if (!GetOverlappedResult(stdin_h, &stdin_o, &read_count, FALSE)) {
56+
die("failed to get overlapped read result");
57+
}
58+
if (strncmp(buf, "exit", read_count) == 0) {
59+
break;
60+
}
61+
write(buf, read_count);
62+
} else {
63+
char emsg[0xfff];
64+
int ecode = GetLastError();
65+
DWORD rv = FormatMessage(
66+
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
67+
NULL,
68+
ecode,
69+
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
70+
(LPSTR)emsg,
71+
sizeof(emsg),
72+
NULL);
73+
if (rv > 0) {
74+
snprintf(emsg, sizeof(emsg),
75+
"WaitForSingleObject failed. Error %d (%s)", ecode, emsg);
76+
} else {
77+
snprintf(emsg, sizeof(emsg),
78+
"WaitForSingleObject failed. Error %d", ecode);
79+
}
80+
die(emsg);
81+
}
82+
}
83+
84+
return 0;
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Test for "overlapped" stdio option. This test uses the "overlapped-checker"
2+
// helper program which basically a specialized echo program.
3+
//
4+
// The test has two goals:
5+
//
6+
// - Verify that overlapped I/O works on windows. The test program will deadlock
7+
// if stdin doesn't have the FILE_FLAG_OVERLAPPED flag set on startup (see
8+
// test/overlapped-checker/main_win.c for more details).
9+
// - Verify that "overlapped" stdio option works transparently as a pipe (on
10+
// unix/windows)
11+
//
12+
// This is how the test works:
13+
//
14+
// - This script assumes only numeric strings are written to the test program
15+
// stdout.
16+
// - The test program will be spawned with "overlapped" set on stdin and "pipe"
17+
// set on stdout/stderr and at startup writes a number to its stdout
18+
// - When this script receives some data, it will parse the number, add 50 and
19+
// write to the test program's stdin.
20+
// - The test program will then echo the number back to us which will repeat the
21+
// cycle until the number reaches 200, at which point we send the "exit"
22+
// string, which causes the test program to exit.
23+
// - Extra assertion: Every time the test program writes a string to its stdout,
24+
// it will write the number of bytes written to stderr.
25+
// - If overlapped I/O is not setup correctly, this test is going to hang.
26+
'use strict';
27+
const common = require('../common');
28+
const assert = require('assert');
29+
const path = require('path');
30+
const child_process = require('child_process');
31+
32+
const exeExtension = process.platform === 'win32' ? '.exe' : '';
33+
const exe = 'overlapped-checker' + exeExtension;
34+
const exePath = path.join(path.dirname(process.execPath), exe);
35+
36+
const child = child_process.spawn(exePath, [], {
37+
stdio: ['overlapped', 'pipe', 'pipe']
38+
});
39+
40+
child.stdin.setEncoding('utf8');
41+
child.stdout.setEncoding('utf8');
42+
child.stderr.setEncoding('utf8');
43+
44+
function writeNext(n) {
45+
child.stdin.write((n + 50).toString());
46+
}
47+
48+
child.stdout.on('data', (s) => {
49+
const n = Number(s);
50+
if (n >= 200) {
51+
child.stdin.write('exit');
52+
return;
53+
}
54+
writeNext(n);
55+
});
56+
57+
let stderr = '';
58+
child.stderr.on('data', (s) => {
59+
stderr += s;
60+
});
61+
62+
child.stderr.on('end', common.mustCall(() => {
63+
// This is the sequence of numbers sent to us:
64+
// - 0 (1 byte written)
65+
// - 50 (2 bytes written)
66+
// - 100 (3 bytes written)
67+
// - 150 (3 bytes written)
68+
// - 200 (3 bytes written)
69+
assert.strictEqual(stderr, '12333');
70+
}));
71+
72+
child.on('exit', common.mustCall((status) => {
73+
// The test program will return the number of writes as status code.
74+
assert.strictEqual(status, 0);
75+
}));

0 commit comments

Comments
 (0)