Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit eed4d8c

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

File tree

Expand file treeCollapse file tree

6 files changed

+221
-14
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

6 files changed

+221
-14
lines changed
Open diff view settings
Collapse file

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

Copy file name to clipboardExpand all lines: src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js
+69Lines 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
/******/ ])));
Collapse file

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

Copy file name to clipboardExpand all lines: src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js
+82-13Lines 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");
Collapse file

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

Copy file name to clipboardExpand all lines: src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs
+2-1Lines 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,
Collapse file

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

Copy file name to clipboardExpand all lines: src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts
+3Lines 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
Collapse file

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

Copy file name to clipboardExpand all lines: src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts
+3Lines 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;
Collapse file
+62Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.