Skip to content

Commit eed4d8c

Browse files
Child Node processes poll and exit when parent has exited. Fixes aspnet#270
1 parent 1ce8a22 commit eed4d8c

File tree

6 files changed

+221
-14
lines changed

6 files changed

+221
-14
lines changed

src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
var http = __webpack_require__(3);
5959
var path = __webpack_require__(4);
6060
var ArgsUtil_1 = __webpack_require__(5);
61+
var ExitWhenParentExits_1 = __webpack_require__(6);
6162
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
6263
// reference to Node's runtime 'require' function.
6364
var dynamicRequire = eval('require');
@@ -121,6 +122,7 @@
121122
// Signal to the NodeServices base class that we're ready to accept invocations
122123
console.log('[Microsoft.AspNetCore.NodeServices:Listening]');
123124
});
125+
ExitWhenParentExits_1.exitWhenParentExits(parseInt(parsedArgs.parentPid));
124126
function readRequestBodyAsJson(request, callback) {
125127
var requestBodyAsString = '';
126128
request
@@ -208,5 +210,72 @@
208210
exports.parseArgs = parseArgs;
209211

210212

213+
/***/ },
214+
/* 6 */
215+
/***/ function(module, exports) {
216+
217+
/*
218+
In general, we want the Node child processes to be terminated as soon as the parent .NET processes exit,
219+
because we have no further use for them. If the .NET process shuts down gracefully, it will run its
220+
finalizers, one of which (in OutOfProcessNodeInstance.cs) will kill its associated Node process immediately.
221+
222+
But if the .NET process is terminated forcefully (e.g., on Linux/OSX with 'kill -9'), then it won't have
223+
any opportunity to shut down its child processes, and by default they will keep running. In this case, it's
224+
up to the child process to detect this has happened and terminate itself.
225+
226+
There are many possible approaches to detecting when a parent process has exited, most of which behave
227+
differently between Windows and Linux/OS X:
228+
229+
- On Windows, the parent process can mark its child as being a 'job' that should auto-terminate when
230+
the parent does (http://stackoverflow.com/a/4657392). Not cross-platform.
231+
- The child Node process can get a callback when the parent disconnects (process.on('disconnect', ...)).
232+
But despite http://stackoverflow.com/a/16487966, no callback fires in any case I've tested (Windows / OS X).
233+
- The child Node process can get a callback when its stdin/stdout are disconnected, as described at
234+
http://stackoverflow.com/a/15693934. This works well on OS X, but calling stdout.resume() on Windows
235+
causes the process to terminate prematurely.
236+
- I don't know why, but on Windows, it's enough to invoke process.stdin.resume(). For some reason this causes
237+
the child Node process to exit as soon as the parent one does, but I don't see this documented anywhere.
238+
- You can poll to see if the parent process, or your stdin/stdout connection to it, is gone
239+
- You can directly pass a parent process PID to the child, and then have the child poll to see if it's
240+
still running (e.g., using process.kill(pid, 0), which doesn't kill it but just tests whether it exists,
241+
as per https://nodejs.org/api/process.html#process_process_kill_pid_signal)
242+
- Or, on each poll, you can try writing to process.stdout. If the parent has died, then this will throw.
243+
However I don't see this documented anywhere. It would be nice if you could just poll for whether or not
244+
process.stdout is still connected (without actually writing to it) but I haven't found any property whose
245+
value changes until you actually try to write to it.
246+
247+
Of these, the only cross-platform approach that is actually documented as a valid strategy is simply polling
248+
to check whether the parent PID is still running. So that's what we do here.
249+
*/
250+
"use strict";
251+
var pollIntervalMs = 1000;
252+
function exitWhenParentExits(parentPid) {
253+
setInterval(function () {
254+
if (!processExists(parentPid)) {
255+
// Can't log anything at this point, because out stdout was connected to the parent,
256+
// but the parent is gone.
257+
process.exit();
258+
}
259+
}, pollIntervalMs);
260+
}
261+
exports.exitWhenParentExits = exitWhenParentExits;
262+
function processExists(pid) {
263+
try {
264+
// Sending signal 0 - on all platforms - tests whether the process exists. As long as it doesn't
265+
// throw, that means it does exist.
266+
process.kill(pid, 0);
267+
return true;
268+
}
269+
catch (ex) {
270+
// If the reason for the error is that we don't have permission to ask about this process,
271+
// report that as a separate problem.
272+
if (ex.code === 'EPERM') {
273+
throw new Error("Attempted to check whether process " + pid + " was running, but got a permissions error.");
274+
}
275+
return false;
276+
}
277+
}
278+
279+
211280
/***/ }
212281
/******/ ])));

src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
/* 0 */
4545
/***/ function(module, exports, __webpack_require__) {
4646

47-
module.exports = __webpack_require__(6);
47+
module.exports = __webpack_require__(7);
4848

4949

5050
/***/ },
@@ -124,17 +124,85 @@
124124

125125
/***/ },
126126
/* 6 */
127+
/***/ function(module, exports) {
128+
129+
/*
130+
In general, we want the Node child processes to be terminated as soon as the parent .NET processes exit,
131+
because we have no further use for them. If the .NET process shuts down gracefully, it will run its
132+
finalizers, one of which (in OutOfProcessNodeInstance.cs) will kill its associated Node process immediately.
133+
134+
But if the .NET process is terminated forcefully (e.g., on Linux/OSX with 'kill -9'), then it won't have
135+
any opportunity to shut down its child processes, and by default they will keep running. In this case, it's
136+
up to the child process to detect this has happened and terminate itself.
137+
138+
There are many possible approaches to detecting when a parent process has exited, most of which behave
139+
differently between Windows and Linux/OS X:
140+
141+
- On Windows, the parent process can mark its child as being a 'job' that should auto-terminate when
142+
the parent does (http://stackoverflow.com/a/4657392). Not cross-platform.
143+
- The child Node process can get a callback when the parent disconnects (process.on('disconnect', ...)).
144+
But despite http://stackoverflow.com/a/16487966, no callback fires in any case I've tested (Windows / OS X).
145+
- The child Node process can get a callback when its stdin/stdout are disconnected, as described at
146+
http://stackoverflow.com/a/15693934. This works well on OS X, but calling stdout.resume() on Windows
147+
causes the process to terminate prematurely.
148+
- I don't know why, but on Windows, it's enough to invoke process.stdin.resume(). For some reason this causes
149+
the child Node process to exit as soon as the parent one does, but I don't see this documented anywhere.
150+
- You can poll to see if the parent process, or your stdin/stdout connection to it, is gone
151+
- You can directly pass a parent process PID to the child, and then have the child poll to see if it's
152+
still running (e.g., using process.kill(pid, 0), which doesn't kill it but just tests whether it exists,
153+
as per https://nodejs.org/api/process.html#process_process_kill_pid_signal)
154+
- Or, on each poll, you can try writing to process.stdout. If the parent has died, then this will throw.
155+
However I don't see this documented anywhere. It would be nice if you could just poll for whether or not
156+
process.stdout is still connected (without actually writing to it) but I haven't found any property whose
157+
value changes until you actually try to write to it.
158+
159+
Of these, the only cross-platform approach that is actually documented as a valid strategy is simply polling
160+
to check whether the parent PID is still running. So that's what we do here.
161+
*/
162+
"use strict";
163+
var pollIntervalMs = 1000;
164+
function exitWhenParentExits(parentPid) {
165+
setInterval(function () {
166+
if (!processExists(parentPid)) {
167+
// Can't log anything at this point, because out stdout was connected to the parent,
168+
// but the parent is gone.
169+
process.exit();
170+
}
171+
}, pollIntervalMs);
172+
}
173+
exports.exitWhenParentExits = exitWhenParentExits;
174+
function processExists(pid) {
175+
try {
176+
// Sending signal 0 - on all platforms - tests whether the process exists. As long as it doesn't
177+
// throw, that means it does exist.
178+
process.kill(pid, 0);
179+
return true;
180+
}
181+
catch (ex) {
182+
// If the reason for the error is that we don't have permission to ask about this process,
183+
// report that as a separate problem.
184+
if (ex.code === 'EPERM') {
185+
throw new Error("Attempted to check whether process " + pid + " was running, but got a permissions error.");
186+
}
187+
return false;
188+
}
189+
}
190+
191+
192+
/***/ },
193+
/* 7 */
127194
/***/ function(module, exports, __webpack_require__) {
128195

129196
"use strict";
130197
// Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive,
131198
// but simplifies things for the consumer of this module.
132199
__webpack_require__(2);
133-
var net = __webpack_require__(7);
200+
var net = __webpack_require__(8);
134201
var path = __webpack_require__(4);
135-
var readline = __webpack_require__(8);
202+
var readline = __webpack_require__(9);
136203
var ArgsUtil_1 = __webpack_require__(5);
137-
var virtualConnectionServer = __webpack_require__(9);
204+
var ExitWhenParentExits_1 = __webpack_require__(6);
205+
var virtualConnectionServer = __webpack_require__(10);
138206
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
139207
// reference to Node's runtime 'require' function.
140208
var dynamicRequire = eval('require');
@@ -189,27 +257,28 @@
189257
var parsedArgs = ArgsUtil_1.parseArgs(process.argv);
190258
var listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress;
191259
server.listen(listenAddress);
260+
ExitWhenParentExits_1.exitWhenParentExits(parseInt(parsedArgs.parentPid));
192261

193262

194263
/***/ },
195-
/* 7 */
264+
/* 8 */
196265
/***/ function(module, exports) {
197266

198267
module.exports = require("net");
199268

200269
/***/ },
201-
/* 8 */
270+
/* 9 */
202271
/***/ function(module, exports) {
203272

204273
module.exports = require("readline");
205274

206275
/***/ },
207-
/* 9 */
276+
/* 10 */
208277
/***/ function(module, exports, __webpack_require__) {
209278

210279
"use strict";
211-
var events_1 = __webpack_require__(10);
212-
var VirtualConnection_1 = __webpack_require__(11);
280+
var events_1 = __webpack_require__(11);
281+
var VirtualConnection_1 = __webpack_require__(12);
213282
// Keep this in sync with the equivalent constant in the .NET code. Both sides split up their transmissions into frames with this max length,
214283
// and both will reject longer frames.
215284
var MaxFrameBodyLength = 16 * 1024;
@@ -390,13 +459,13 @@
390459

391460

392461
/***/ },
393-
/* 10 */
462+
/* 11 */
394463
/***/ function(module, exports) {
395464

396465
module.exports = require("events");
397466

398467
/***/ },
399-
/* 11 */
468+
/* 12 */
400469
/***/ function(module, exports, __webpack_require__) {
401470

402471
"use strict";
@@ -405,7 +474,7 @@
405474
function __() { this.constructor = d; }
406475
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
407476
};
408-
var stream_1 = __webpack_require__(12);
477+
var stream_1 = __webpack_require__(13);
409478
/**
410479
* Represents a virtual connection. Multiple virtual connections may be multiplexed over a single physical socket connection.
411480
*/
@@ -446,7 +515,7 @@
446515

447516

448517
/***/ },
449-
/* 12 */
518+
/* 13 */
450519
/***/ function(module, exports) {
451520

452521
module.exports = require("stream");

src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,10 @@ protected virtual ProcessStartInfo PrepareNodeProcessStartInfo(
114114
debuggingArgs = string.Empty;
115115
}
116116

117+
var thisProcessPid = Process.GetCurrentProcess().Id;
117118
var startInfo = new ProcessStartInfo("node")
118119
{
119-
Arguments = debuggingArgs + "\"" + entryPointFilename + "\" " + (commandLineArguments ?? string.Empty),
120+
Arguments = $"{debuggingArgs}\"{entryPointFilename}\" --parentPid {thisProcessPid} {commandLineArguments ?? string.Empty}",
120121
UseShellExecute = false,
121122
RedirectStandardInput = true,
122123
RedirectStandardOutput = true,

src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import './Util/OverrideStdOutputs';
44
import * as http from 'http';
55
import * as path from 'path';
66
import { parseArgs } from './Util/ArgsUtil';
7+
import { exitWhenParentExits } from './Util/ExitWhenParentExits';
78

89
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
910
// reference to Node's runtime 'require' function.
@@ -73,6 +74,8 @@ server.listen(requestedPortOrZero, 'localhost', function () {
7374
console.log('[Microsoft.AspNetCore.NodeServices:Listening]');
7475
});
7576

77+
exitWhenParentExits(parseInt(parsedArgs.parentPid));
78+
7679
function readRequestBodyAsJson(request, callback) {
7780
let requestBodyAsString = '';
7881
request

src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as path from 'path';
66
import * as readline from 'readline';
77
import { Duplex } from 'stream';
88
import { parseArgs } from './Util/ArgsUtil';
9+
import { exitWhenParentExits } from './Util/ExitWhenParentExits';
910
import * as virtualConnectionServer from './VirtualConnections/VirtualConnectionServer';
1011

1112
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
@@ -69,6 +70,8 @@ const parsedArgs = parseArgs(process.argv);
6970
const listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress;
7071
server.listen(listenAddress);
7172

73+
exitWhenParentExits(parseInt(parsedArgs.parentPid));
74+
7275
interface RpcInvocation {
7376
moduleName: string;
7477
exportedFunctionName: string;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
In general, we want the Node child processes to be terminated as soon as the parent .NET processes exit,
3+
because we have no further use for them. If the .NET process shuts down gracefully, it will run its
4+
finalizers, one of which (in OutOfProcessNodeInstance.cs) will kill its associated Node process immediately.
5+
6+
But if the .NET process is terminated forcefully (e.g., on Linux/OSX with 'kill -9'), then it won't have
7+
any opportunity to shut down its child processes, and by default they will keep running. In this case, it's
8+
up to the child process to detect this has happened and terminate itself.
9+
10+
There are many possible approaches to detecting when a parent process has exited, most of which behave
11+
differently between Windows and Linux/OS X:
12+
13+
- On Windows, the parent process can mark its child as being a 'job' that should auto-terminate when
14+
the parent does (http://stackoverflow.com/a/4657392). Not cross-platform.
15+
- The child Node process can get a callback when the parent disconnects (process.on('disconnect', ...)).
16+
But despite http://stackoverflow.com/a/16487966, no callback fires in any case I've tested (Windows / OS X).
17+
- The child Node process can get a callback when its stdin/stdout are disconnected, as described at
18+
http://stackoverflow.com/a/15693934. This works well on OS X, but calling stdout.resume() on Windows
19+
causes the process to terminate prematurely.
20+
- I don't know why, but on Windows, it's enough to invoke process.stdin.resume(). For some reason this causes
21+
the child Node process to exit as soon as the parent one does, but I don't see this documented anywhere.
22+
- You can poll to see if the parent process, or your stdin/stdout connection to it, is gone
23+
- You can directly pass a parent process PID to the child, and then have the child poll to see if it's
24+
still running (e.g., using process.kill(pid, 0), which doesn't kill it but just tests whether it exists,
25+
as per https://nodejs.org/api/process.html#process_process_kill_pid_signal)
26+
- Or, on each poll, you can try writing to process.stdout. If the parent has died, then this will throw.
27+
However I don't see this documented anywhere. It would be nice if you could just poll for whether or not
28+
process.stdout is still connected (without actually writing to it) but I haven't found any property whose
29+
value changes until you actually try to write to it.
30+
31+
Of these, the only cross-platform approach that is actually documented as a valid strategy is simply polling
32+
to check whether the parent PID is still running. So that's what we do here.
33+
*/
34+
35+
const pollIntervalMs = 1000;
36+
37+
export function exitWhenParentExits(parentPid: number) {
38+
setInterval(() => {
39+
if (!processExists(parentPid)) {
40+
// Can't log anything at this point, because out stdout was connected to the parent,
41+
// but the parent is gone.
42+
process.exit();
43+
}
44+
}, pollIntervalMs);
45+
}
46+
47+
function processExists(pid: number) {
48+
try {
49+
// Sending signal 0 - on all platforms - tests whether the process exists. As long as it doesn't
50+
// throw, that means it does exist.
51+
process.kill(pid, 0);
52+
return true;
53+
} catch (ex) {
54+
// If the reason for the error is that we don't have permission to ask about this process,
55+
// report that as a separate problem.
56+
if (ex.code === 'EPERM') {
57+
throw new Error(`Attempted to check whether process ${pid} was running, but got a permissions error.`);
58+
}
59+
60+
return false;
61+
}
62+
}

0 commit comments

Comments
 (0)