Skip to content

Commit b174106

Browse files
authored
fix: kill entire process tree when shutting down nxls & vscode (#2288)
1 parent 990e2df commit b174106

File tree

13 files changed

+210
-59
lines changed

13 files changed

+210
-59
lines changed

apps/nxls-e2e/src/nxls-wrapper.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
StreamMessageWriter,
1212
} from 'vscode-languageserver/node';
1313

14-
import treeKill from 'tree-kill';
14+
import { killTree } from '@nx-console/shared/utils';
1515

1616
export class NxlsWrapper {
1717
private messageReader?: StreamMessageReader;
@@ -134,13 +134,9 @@ export class NxlsWrapper {
134134

135135
this.process?.removeListener('exit', this.earlyExitListener);
136136

137-
await new Promise<void>((resolve) => {
138-
if (this.process?.pid) {
139-
treeKill(this.process.pid, 'SIGKILL', () => resolve());
140-
} else {
141-
resolve();
142-
}
143-
});
137+
if (this.process?.pid) {
138+
await killTree(this.process.pid, 'SIGKILL');
139+
}
144140
}
145141

146142
async sendRequest(

apps/nxls-e2e/tsconfig.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
"forceConsistentCasingInFileNames": true,
66
"strict": true,
77
"noImplicitOverride": true,
8-
"noPropertyAccessFromIndexSignature": true,
9-
"noImplicitReturns": true,
108
"noFallthroughCasesInSwitch": true,
119
"esModuleInterop": true
1210
},

apps/nxls/src/main.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getDocumentLinks } from '@nx-console/language-server/capabilities/docum
1010
import { getHover } from '@nx-console/language-server/capabilities/hover';
1111
import {
1212
NxChangeWorkspace,
13+
NxCloudOnboardingInfoRequest,
1314
NxCloudStatusRequest,
1415
NxCreateProjectGraphRequest,
1516
NxGeneratorContextFromPathRequest,
@@ -35,7 +36,6 @@ import {
3536
NxWorkspaceRefreshNotification,
3637
NxWorkspaceRefreshStartedNotification,
3738
NxWorkspaceRequest,
38-
NxCloudOnboardingInfoRequest,
3939
NxWorkspaceSerializedRequest,
4040
} from '@nx-console/language-server/types';
4141
import {
@@ -79,9 +79,8 @@ import {
7979
import { GeneratorSchema } from '@nx-console/shared/generate-ui-types';
8080
import { TaskExecutionSchema } from '@nx-console/shared/schema';
8181
import { NxWorkspace } from '@nx-console/shared/types';
82-
import { formatError } from '@nx-console/shared/utils';
82+
import { formatError, killTree } from '@nx-console/shared/utils';
8383
import { dirname, relative } from 'node:path';
84-
import treeKill from 'tree-kill';
8584
import {
8685
ClientCapabilities,
8786
CompletionList,
@@ -111,7 +110,6 @@ process.on('uncaughtException', (e) => {
111110
});
112111

113112
let WORKING_PATH: string | undefined = undefined;
114-
let PID: number | null = null;
115113
let CLIENT_CAPABILITIES: ClientCapabilities | undefined = undefined;
116114
let unregisterFileWatcher: () => void = () => {
117115
//noop
@@ -139,8 +137,6 @@ connection.onInitialize(async (params) => {
139137
setLspLogger(connection);
140138
lspLogger.log('Initializing Nx Language Server');
141139

142-
PID = params.processId;
143-
144140
const { workspacePath } = params.initializationOptions ?? {};
145141
try {
146142
WORKING_PATH =
@@ -210,6 +206,7 @@ connection.onInitialize(async (params) => {
210206
},
211207
},
212208
},
209+
pid: process.pid,
213210
};
214211

215212
return result;
@@ -372,7 +369,7 @@ connection.onShutdown(async () => {
372369

373370
connection.onExit(() => {
374371
connection.dispose();
375-
treeKill(process.pid, 'SIGTERM');
372+
killTree(process.pid);
376373
});
377374

378375
connection.onRequest(NxStopDaemonRequest, async () => {

apps/vscode/src/main.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import {
1111
workspace,
1212
} from 'vscode';
1313

14-
import { checkIsNxWorkspace } from '@nx-console/shared/utils';
14+
import {
15+
checkIsNxWorkspace,
16+
killTree,
17+
withTimeout,
18+
} from '@nx-console/shared/utils';
1519
import {
1620
GlobalConfigurationStore,
1721
WorkspaceConfigurationStore,
@@ -34,7 +38,6 @@ import { createNxlsClient, getNxlsClient } from '@nx-console/vscode/lsp-client';
3438
import { initNxConfigDecoration } from '@nx-console/vscode/nx-config-decoration';
3539
import { initNxConversion } from '@nx-console/vscode/nx-conversion';
3640
import { initHelpAndFeedbackView } from '@nx-console/vscode/nx-help-and-feedback-view';
37-
import { stopDaemon } from '@nx-console/vscode/nx-workspace';
3841
import { initVscodeProjectGraph } from '@nx-console/vscode/project-graph';
3942
import { enableTypeScriptPlugin } from '@nx-console/vscode/typescript-plugin';
4043

@@ -45,15 +48,16 @@ import {
4548
} from '@nx-console/language-server/types';
4649
import { initErrorDiagnostics } from '@nx-console/vscode/error-diagnostics';
4750
import { initNvmTip } from '@nx-console/vscode/nvm-tip';
51+
import { initNxCloudView } from '@nx-console/vscode/nx-cloud-view';
4852
import {
4953
getOutputChannel,
5054
initOutputChannels,
5155
} from '@nx-console/vscode/output-channels';
5256
import { initVscodeProjectDetails } from '@nx-console/vscode/project-details';
57+
import { getTelemetry, initTelemetry } from '@nx-console/vscode/telemetry';
58+
import { RequestType } from 'vscode-languageserver';
5359
import { initNxInit } from './nx-init';
5460
import { registerRefreshWorkspace } from './refresh-workspace';
55-
import { initNxCloudView } from '@nx-console/vscode/nx-cloud-view';
56-
import { initTelemetry, getTelemetry } from '@nx-console/vscode/telemetry';
5761

5862
let nxProjectsTreeProvider: NxProjectTreeProvider;
5963

@@ -114,10 +118,30 @@ export async function activate(c: ExtensionContext) {
114118
}
115119

116120
export async function deactivate() {
117-
await stopDaemon();
118-
await getNxlsClient()?.stop();
121+
try {
122+
await withTimeout(
123+
async () =>
124+
await getNxlsClient()?.sendRequest(
125+
new RequestType('shutdown'),
126+
undefined
127+
),
128+
129+
2000
130+
);
131+
} catch (e) {
132+
// do nothing, we have to deactivate before the process is killed
133+
}
134+
119135
workspaceFileWatcher?.dispose();
136+
137+
const nxlsPid = getNxlsClient()?.getNxlsPid();
138+
if (nxlsPid) {
139+
killTree(nxlsPid, 'SIGINT');
140+
}
141+
120142
getTelemetry().logUsage('extension-deactivate');
143+
144+
killTree(process.pid);
121145
}
122146

123147
// -----------------------------------------------------------------------------

libs/shared/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export { findConfig } from './lib/find-config';
44
export { checkIsNxWorkspace } from './lib/check-is-nx-workspace';
55
export { getNxExecutionCommand } from './lib/get-nx-execution-command';
66
export * from './lib/utils';
7+
export * from './lib/kill-tree';

libs/shared/utils/src/lib/build-project-path.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export async function buildProjectPath(
2020
} else if (await fileExists(packageJsonPath)) {
2121
return packageJsonPath;
2222
}
23+
return;
2324
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Adapted from https://raw.githubusercontent.com/pkrumins/node-tree-kill/deee138/index.jss
2+
import { spawn, exec, ExecException, ChildProcess } from 'child_process';
3+
4+
type ProcessTree = Record<number, number[]>;
5+
type ProcessMap = Record<number, number>;
6+
type SpawnFunction = (parentPid: number) => ChildProcess;
7+
type CallbackFunction = (error?: ExecException | null) => void;
8+
9+
export async function killTree(
10+
pid: number,
11+
signal: NodeJS.Signals = 'SIGTERM'
12+
): Promise<void> {
13+
const tree: ProcessTree = {};
14+
const pidsToProcess: ProcessMap = {};
15+
tree[pid] = [];
16+
pidsToProcess[pid] = 1;
17+
18+
return new Promise<void>((resolve, reject) => {
19+
const callback: CallbackFunction = (error) => {
20+
if (error) {
21+
reject(error);
22+
} else {
23+
resolve();
24+
}
25+
};
26+
27+
switch (process.platform) {
28+
case 'win32':
29+
exec(
30+
'taskkill /pid ' + pid + ' /T /F',
31+
{
32+
windowsHide: true,
33+
},
34+
(error) => {
35+
// Ignore Fatal errors (128) because it might be due to the process already being killed.
36+
// On Linux/Mac we can check ESRCH (no such process), but on Windows we can't.
37+
callback(error?.code !== 128 ? error : null);
38+
}
39+
);
40+
break;
41+
case 'darwin':
42+
buildProcessTree(
43+
pid,
44+
tree,
45+
pidsToProcess,
46+
function (parentPid: number): ChildProcess {
47+
return spawn('pgrep', ['-P', `${parentPid}`], {
48+
windowsHide: false,
49+
});
50+
},
51+
function (): void {
52+
killAll(tree, signal, callback);
53+
}
54+
);
55+
break;
56+
default: // Linux
57+
buildProcessTree(
58+
pid,
59+
tree,
60+
pidsToProcess,
61+
function (parentPid: number): ChildProcess {
62+
return spawn(
63+
'ps',
64+
['-o', 'pid', '--no-headers', '--ppid', `${parentPid}`],
65+
{
66+
windowsHide: false,
67+
}
68+
);
69+
},
70+
function (): void {
71+
killAll(tree, signal, callback);
72+
}
73+
);
74+
break;
75+
}
76+
});
77+
}
78+
79+
function killAll(
80+
tree: ProcessTree,
81+
signal: NodeJS.Signals,
82+
callback?: CallbackFunction
83+
): void {
84+
const killed: ProcessMap = {};
85+
try {
86+
Object.keys(tree).forEach(function (pid) {
87+
tree[parseInt(pid, 10)].forEach(function (pidpid) {
88+
if (!killed[pidpid]) {
89+
killPid(pidpid, signal);
90+
killed[pidpid] = 1;
91+
}
92+
});
93+
if (!killed[parseInt(pid, 10)]) {
94+
killPid(parseInt(pid, 10), signal);
95+
killed[parseInt(pid, 10)] = 1;
96+
}
97+
});
98+
} catch (err) {
99+
if (callback) {
100+
return callback(err as ExecException);
101+
} else {
102+
throw err;
103+
}
104+
}
105+
if (callback) {
106+
return callback();
107+
}
108+
}
109+
110+
function killPid(pid: number, signal: NodeJS.Signals): void {
111+
try {
112+
process.kill(pid, signal);
113+
} catch (err) {
114+
const error = err as NodeJS.ErrnoException;
115+
if (error.code !== 'ESRCH') throw err;
116+
}
117+
}
118+
119+
function buildProcessTree(
120+
parentPid: number,
121+
tree: ProcessTree,
122+
pidsToProcess: ProcessMap,
123+
spawnChildProcessesList: SpawnFunction,
124+
cb: () => void
125+
): void {
126+
const ps = spawnChildProcessesList(parentPid);
127+
let allData = '';
128+
ps.stdout?.on('data', (data: Buffer | string) => {
129+
const strData = data.toString('ascii');
130+
allData += strData;
131+
});
132+
133+
const onClose = function (code: number): void {
134+
delete pidsToProcess[parentPid];
135+
136+
if (code != 0) {
137+
// no more parent processes
138+
if (Object.keys(pidsToProcess).length == 0) {
139+
cb();
140+
}
141+
return;
142+
}
143+
144+
const pids = allData.match(/\d+/g);
145+
if (pids) {
146+
pids.forEach((_pid) => {
147+
const pid = parseInt(_pid, 10);
148+
tree[parentPid].push(pid);
149+
tree[pid] = [];
150+
pidsToProcess[pid] = 1;
151+
buildProcessTree(pid, tree, pidsToProcess, spawnChildProcessesList, cb);
152+
});
153+
}
154+
};
155+
156+
ps.on('close', onClose);
157+
}

libs/shared/utils/tsconfig.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
{
22
"extends": "../../../tsconfig.base.json",
3-
"compilerOptions": {
4-
"types": ["jest", "node"]
5-
},
63
"files": [],
74
"include": [],
85
"references": [

libs/vscode/lsp-client/src/lib/nxls-client.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
NxWorkspaceRefreshNotification,
44
NxWorkspaceRefreshStartedNotification,
55
} from '@nx-console/language-server/types';
6+
import { killTree } from '@nx-console/shared/utils';
67
import {
78
getNxlsOutputChannel,
89
getOutputChannel,
@@ -178,7 +179,14 @@ class NxlsClient {
178179
this.state = 'idle';
179180
return;
180181
}
181-
await this.client.stop(2000);
182+
try {
183+
await this.client.stop(2000);
184+
} catch (e) {
185+
const nxlsPid = this.getNxlsPid();
186+
if (nxlsPid) {
187+
killTree(nxlsPid);
188+
}
189+
}
182190
this.onRefreshNotificationDisposable?.dispose();
183191
this.onRefreshStartedNotificationDisposable?.dispose();
184192
this.disposables.forEach((d) => d.dispose());
@@ -229,4 +237,8 @@ class NxlsClient {
229237
});
230238
this.disposables.push(disposable);
231239
}
240+
241+
public getNxlsPid(): number | undefined {
242+
return this.client?.initializeResult?.['pid'];
243+
}
232244
}

libs/vscode/nx-workspace/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export * from './lib/stop-daemon';
21
export * from './lib/get-nx-workspace';
32
export * from './lib/get-nx-workspace-path';
43
export * from './lib/get-project-by-path';

0 commit comments

Comments
 (0)