Skip to content

Commit dd87d6c

Browse files
server: Disable exit/watchdog timers if detached
Background Info: - In Unix-like environments, the child process would sometimes not exit even when the parent did. - The exit/watchdog timers were created due to address this behavior. See the docstring of setupExitTimer(). - The 'detached' flag would not be honored in Unix-like environments, and looks to stay open regardless in some cases. Problem: If a language server was set as detached in the ServerOptions, and we wanted it to not exit on parent exit, it would still exit due to the setupExitTimer() Solution: - A server will know that it is detached by the cli process argument: '--detached' - The client side code to inject this is in a following commit - The exit/watchdog timers will not run if the detached flag is true - When the input stream to the server ends due to parent terminating, the server would also. This PR changes it to skip this if detached. Signed-off-by: nkomonen-amazon <nkomonen@amazon.com>
1 parent dadd73f commit dd87d6c

File tree

1 file changed

+58
-3
lines changed

1 file changed

+58
-3
lines changed

server/src/node/main.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,41 @@ export namespace Files {
2626
export const resolveModulePath = fm.resolveModulePath;
2727
}
2828

29+
let _isDetached: boolean | undefined = undefined;
30+
31+
/**
32+
* @returns {boolean} True if the process is detached from its parent, false otherwise.
33+
*
34+
* @description
35+
* When a process is detached, it will continue running even if it's parent process
36+
* terminates. **But EXIT notifications from the client will be honored regardless.**
37+
*
38+
* @remarks
39+
* This function assumes the parent process has been properly configured with
40+
* necessary settings (e.g., using `child_process.unref()`).
41+
*/
42+
function isDetached() {
43+
// cached result
44+
if (_isDetached !== undefined) {
45+
return _isDetached;
46+
}
47+
48+
if(process.argv.includes('--detached')) {
49+
setDetached();
50+
} else {
51+
_isDetached = false;
52+
}
53+
return _isDetached;
54+
55+
function setDetached() {
56+
_isDetached = true;
57+
58+
// Ignores the default behavior when then parent/child communication
59+
// channel (e.g. IPC) disconnects, the child terminates.
60+
process.on('disconnect', () => {});
61+
}
62+
}
63+
2964
let _protocolConnection: ProtocolConnection | undefined;
3065
function endProtocolConnection(): void {
3166
if (_protocolConnection === undefined) {
@@ -38,10 +73,23 @@ function endProtocolConnection(): void {
3873
// did and we can't send an end into the connection.
3974
}
4075
}
76+
4177
let _shutdownReceived: boolean = false;
4278
let exitTimer: NodeJS.Timer | undefined = undefined;
4379

80+
81+
/**
82+
* May auto-exit the server if the parent process does not exist.
83+
*
84+
* We need this due to different behaviors between OS's when the parent terminates:
85+
* - Windows terminates the child
86+
* - Unix-like may not terminate the child
87+
*/
4488
function setupExitTimer(): void {
89+
if (isDetached()) {
90+
return;
91+
}
92+
4593
const argName = '--clientProcessId';
4694
function runTimer(value: string): void {
4795
try {
@@ -79,6 +127,10 @@ setupExitTimer();
79127

80128
const watchDog: WatchDog = {
81129
initialize: (params: InitializeParams): void => {
130+
if (isDetached()) {
131+
return;
132+
}
133+
82134
const processId = params.processId;
83135
if (Is.number(processId) && exitTimer === undefined) {
84136
// We received a parent process id. Set up a timer to periodically check
@@ -105,7 +157,6 @@ const watchDog: WatchDog = {
105157
}
106158
};
107159

108-
109160
/**
110161
* Creates a new connection based on the processes command line arguments:
111162
*
@@ -249,11 +300,15 @@ function _createConnection<PConsole = _, PTracer = _, PTelemetry = _, PClient =
249300
const inputStream = <NodeJS.ReadableStream>input;
250301
inputStream.on('end', () => {
251302
endProtocolConnection();
252-
process.exit(_shutdownReceived ? 0 : 1);
303+
if (!isDetached()) {
304+
process.exit(_shutdownReceived ? 0 : 1);
305+
}
253306
});
254307
inputStream.on('close', () => {
255308
endProtocolConnection();
256-
process.exit(_shutdownReceived ? 0 : 1);
309+
if (!isDetached()) {
310+
process.exit(_shutdownReceived ? 0 : 1);
311+
}
257312
});
258313
}
259314

0 commit comments

Comments
 (0)