Skip to content

Commit c1c4f80

Browse files
Copilotedvilme
andauthored
Fix duplicate server handlers on concurrent restarts (#259)
* Initial plan * Fix multiple LSP server handlers via concurrency guard and stop error handling Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com> * Move isRestarting/restartTimer to module scope; fix deactivate to cancel timer and handle stop errors Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com> * Remove timer debounce from runServer; simplify to match vscode-isort PR pattern Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com> * Add getServerEnabled from settings; wire into extension to allow disabling the server Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com> * Add LS_SERVER_RESTART_DELAY constant and debounce timer to prevent dropped concurrent restarts Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com> * Fix multiple server handlers registered: always clear _disposables in restartServer regardless of lsClient Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com> * Deduplicate server-stop logic in runServer() by calling deactivate() Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com> * Revert: restore original inline stop block in runServer() Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
1 parent a0dd1f0 commit c1c4f80

4 files changed

Lines changed: 64 additions & 22 deletions

File tree

src/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export const EXTENSION_ROOT_DIR =
99
export const BUNDLED_PYTHON_SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, 'bundled');
1010
export const SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `lsp_server.py`);
1111
export const DEBUG_SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, 'tool', `_debug_server.py`);
12+
export const LS_SERVER_RESTART_DELAY = 1000; // Delay before restarting the LS in case of multiple configuration change events.

src/common/server.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,14 @@ export async function restartServer(
8686
): Promise<LanguageClient | undefined> {
8787
if (lsClient) {
8888
traceInfo(`Server: Stop requested`);
89-
await lsClient.stop();
90-
_disposables.forEach((d) => d.dispose());
91-
_disposables = [];
89+
try {
90+
await lsClient.stop();
91+
} catch (ex) {
92+
traceError(`Server: Stop failed: ${ex}`);
93+
}
9294
}
95+
_disposables.forEach((d) => d.dispose());
96+
_disposables = [];
9397
const projectRoot = await getProjectRoot();
9498
const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true);
9599

src/common/settings.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,13 @@ export function checkIfConfigurationChanged(e: ConfigurationChangeEvent, namespa
108108
`${namespace}.interpreter`,
109109
`${namespace}.importStrategy`,
110110
`${namespace}.showNotifications`,
111+
`${namespace}.serverEnabled`,
111112
];
112113
const changed = settings.map((s) => e.affectsConfiguration(s));
113114
return changed.includes(true);
114115
}
116+
117+
export function getServerEnabled(namespace: string): boolean {
118+
const config = getConfiguration(namespace);
119+
return config.get<boolean>('serverEnabled', true);
120+
}

src/extension.ts

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import {
1212
resolveInterpreter,
1313
} from './common/python';
1414
import { restartServer } from './common/server';
15-
import { checkIfConfigurationChanged, getInterpreterFromSetting } from './common/settings';
15+
import { checkIfConfigurationChanged, getInterpreterFromSetting, getServerEnabled } from './common/settings';
1616
import { loadServerDefaults } from './common/setup';
17+
import { LS_SERVER_RESTART_DELAY } from './common/constants';
1718
import { getLSClientTraceLevel } from './common/utilities';
1819
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from './common/vscodeapi';
1920

2021
let lsClient: LanguageClient | undefined;
22+
let isRestarting = false;
23+
let restartTimer: NodeJS.Timeout | undefined;
2124
export async function activate(context: vscode.ExtensionContext): Promise<void> {
2225
// This is required to get server name and module. This should be
2326
// the first thing that we do in this extension.
@@ -49,28 +52,52 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
4952
traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`);
5053

5154
const runServer = async () => {
52-
const interpreter = getInterpreterFromSetting(serverId);
53-
if (interpreter && interpreter.length > 0) {
54-
if (checkVersion(await resolveInterpreter(interpreter))) {
55-
traceVerbose(`Using interpreter from ${serverInfo.module}.interpreter: ${interpreter.join(' ')}`);
56-
lsClient = await restartServer(serverId, serverName, outputChannel, lsClient);
55+
if (isRestarting) {
56+
if (restartTimer) {
57+
clearTimeout(restartTimer);
5758
}
59+
restartTimer = setTimeout(runServer, LS_SERVER_RESTART_DELAY);
5860
return;
5961
}
62+
isRestarting = true;
63+
try {
64+
if (!getServerEnabled(serverId)) {
65+
if (lsClient) {
66+
try {
67+
await lsClient.stop();
68+
} catch (ex) {
69+
traceError(`Server: Stop failed: ${ex}`);
70+
}
71+
lsClient = undefined;
72+
}
73+
return;
74+
}
6075

61-
const interpreterDetails = await getInterpreterDetails();
62-
if (interpreterDetails.path) {
63-
traceVerbose(`Using interpreter from Python extension: ${interpreterDetails.path.join(' ')}`);
64-
lsClient = await restartServer(serverId, serverName, outputChannel, lsClient);
65-
return;
66-
}
76+
const interpreter = getInterpreterFromSetting(serverId);
77+
if (interpreter && interpreter.length > 0) {
78+
if (checkVersion(await resolveInterpreter(interpreter))) {
79+
traceVerbose(`Using interpreter from ${serverInfo.module}.interpreter: ${interpreter.join(' ')}`);
80+
lsClient = await restartServer(serverId, serverName, outputChannel, lsClient);
81+
}
82+
return;
83+
}
6784

68-
traceError(
69-
'Python interpreter missing:\r\n' +
70-
'[Option 1] Select python interpreter using the ms-python.python.\r\n' +
71-
`[Option 2] Set an interpreter using "${serverId}.interpreter" setting.\r\n` +
72-
'Please use Python 3.8 or greater.',
73-
);
85+
const interpreterDetails = await getInterpreterDetails();
86+
if (interpreterDetails.path) {
87+
traceVerbose(`Using interpreter from Python extension: ${interpreterDetails.path.join(' ')}`);
88+
lsClient = await restartServer(serverId, serverName, outputChannel, lsClient);
89+
return;
90+
}
91+
92+
traceError(
93+
'Python interpreter missing:\r\n' +
94+
'[Option 1] Select python interpreter using the ms-python.python.\r\n' +
95+
`[Option 2] Set an interpreter using "${serverId}.interpreter" setting.\r\n` +
96+
'Please use Python 3.8 or greater.',
97+
);
98+
} finally {
99+
isRestarting = false;
100+
}
74101
};
75102

76103
context.subscriptions.push(
@@ -101,6 +128,10 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
101128

102129
export async function deactivate(): Promise<void> {
103130
if (lsClient) {
104-
await lsClient.stop();
131+
try {
132+
await lsClient.stop();
133+
} catch (ex) {
134+
traceError(`Server: Stop failed: ${ex}`);
135+
}
105136
}
106137
}

0 commit comments

Comments
 (0)