Skip to content

Fix duplicate server handlers on concurrent restarts#259

Merged
edvilme merged 9 commits intomainfrom
copilot/fix-lsp-server-registrations
Feb 25, 2026
Merged

Fix duplicate server handlers on concurrent restarts#259
edvilme merged 9 commits intomainfrom
copilot/fix-lsp-server-registrations

Conversation

Copy link
Contributor

Copilot AI commented Feb 25, 2026

The previous change replaced the inline client-stop logic in runServer() with a call to deactivate(), introducing a subtle behavioural difference: deactivate() does not null out lsClient, requiring an extra lsClient = undefined after the call and splitting the cleanup across two statements. The revert restores the original self-contained inline block, which is clearer and keeps the lsClient lifecycle management in one place.

Changes

  • src/extension.ts — Restored the original inline lsClient.stop() block inside the !getServerEnabled branch of runServer(), replacing the deactivate() delegation introduced in the prior commit.
Original prompt

Problem

Fix multiple LSP server handlers getting registered due to concurrent server restarts and lack of proper cleanup, as reported in downstream microsoft/vscode-isort#463. This is a template-originated issue — the fix should follow the same approach as microsoft/vscode-isort#541.

What vscode-isort PR #541 Does

PR #541 in microsoft/vscode-isort fixes the issue by modifying only src/extension.ts with the following changes:

  1. Concurrency guard with debounce on runServer(): Wraps runServer() in an isRestarting flag with a debounce timer (restartTimer). If runServer() is called while already restarting, it schedules a retry after LS_SERVER_RESTART_DELAY ms instead of spawning a duplicate. This prevents concurrent calls from racing and creating multiple servers.

  2. serverEnabled setting support with proper mode switching: Checks getServerEnabled(serverId) to determine whether to use the LSP server or a server-less fallback. When switching modes, it properly tears down the previous mode first:

    • When switching to server mode: calls unRegisterSortImportFeatures() before starting the LSP server
    • When switching to server-less mode: stops the LSP client, sets it to undefined, calls unRegisterSortImportFeatures(), then registers new sort features
  3. Module-level sortFeaturesDisposable tracking: Tracks the disposable returned by registerSortImportFeatures() at module level so it can be properly disposed before re-registration, preventing subscription accumulation.

  4. Unified cleanup: unRegisterSortImportFeatures() is called in both branches (server mode and server-less mode) for symmetric cleanup before setting up new handlers.

Required Changes for the Template

Since the template doesn't have isort-specific concepts (sortImports.ts, configWatcher.ts, status.ts, serverEnabled setting), the template PR should adapt the applicable patterns from #541:

1. src/extension.ts — Add concurrency guard with debounce

Following the exact pattern from vscode-isort PR #541, add an isRestarting mutex and restartTimer debounce to runServer():

let isRestarting = false;
let restartTimer: NodeJS.Timeout | undefined;
const runServer = async () => {
    if (isRestarting) {
        if (restartTimer) {
            clearTimeout(restartTimer);
        }
        restartTimer = setTimeout(runServer, 1000); // debounce delay
        return;
    }
    isRestarting = true;
    try {
        // ... existing runServer logic (interpreter check, restartServer call) ...
    } finally {
        isRestarting = false;
    }
};

This is the key fix that prevents the N concurrent runServer() calls from spawning N duplicate servers.

2. src/extension.ts — Wrap existing logic in try/finally

The existing runServer() body should be wrapped in a try/finally block so isRestarting is always reset, even if restartServer() throws.

3. src/common/server.ts — Add timeout + force-kill to server stop

When stopping the old server in restartServer(), add a timeout so that if lsClient.stop() hangs (as can happen when the server process is orphaned), the restart isn't blocked forever. This follows the defensive pattern from vscode-isort #541 where the server stop is wrapped in try/catch.

if (lsClient) {
    traceInfo(`Server: Stop requested`);
    try {
        await lsClient.stop();
    } catch (ex) {
        traceError(`Server: Stop failed: ${ex}`);
    }
    _disposables.forEach((d) => d.dispose());
    _disposables = [];
}

Files to Modify

  • src/extension.ts — Add isRestarting + restartTimer concurrency guard to runServer()
  • src/common/server.ts — Add try/catch around lsClient.stop() in restartServer()

Reference

This pull request was created from Copilot chat.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

… handling

Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix multiple LSP server handlers registration issues Fix multiple LSP server handlers from concurrent restarts Feb 25, 2026
…cel timer and handle stop errors

Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
Copilot AI changed the title Fix multiple LSP server handlers from concurrent restarts Fix duplicate LSP server handlers from concurrent and post-deactivate restarts Feb 25, 2026
…PR pattern

Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
Copilot AI changed the title Fix duplicate LSP server handlers from concurrent and post-deactivate restarts Fix concurrent restart race and unsafe shutdown in extension lifecycle Feb 25, 2026
…bling the server

Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
Copilot AI changed the title Fix concurrent restart race and unsafe shutdown in extension lifecycle Add getServerEnabled support and remove unnecessary restart timer Feb 25, 2026
…opped concurrent restarts

Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
Copilot AI changed the title Add getServerEnabled support and remove unnecessary restart timer Add getServerEnabled setting and debounce concurrent server restarts Feb 25, 2026
… restartServer regardless of lsClient

Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
Copilot AI changed the title Add getServerEnabled setting and debounce concurrent server restarts Prevent duplicate server handlers and dropped restart requests Feb 25, 2026
Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
Copilot AI changed the title Prevent duplicate server handlers and dropped restart requests Fix duplicate server handlers on concurrent restarts; deduplicate stop logic via deactivate() Feb 25, 2026
@edvilme edvilme changed the title Fix duplicate server handlers on concurrent restarts; deduplicate stop logic via deactivate() Fix duplicate server handlers on concurrent restarts Feb 25, 2026
Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
Copilot AI changed the title Fix duplicate server handlers on concurrent restarts Revert: restore original inline stop block in runServer() Feb 25, 2026
@edvilme edvilme changed the title Revert: restore original inline stop block in runServer() Fix duplicate server handlers on concurrent restarts Feb 25, 2026
@edvilme edvilme marked this pull request as ready for review February 25, 2026 21:11
@vs-code-engineering vs-code-engineering bot added this to the February 2026 milestone Feb 25, 2026
@edvilme edvilme enabled auto-merge (squash) February 25, 2026 21:16
@edvilme edvilme merged commit c1c4f80 into main Feb 25, 2026
2 checks passed
@edvilme edvilme deleted the copilot/fix-lsp-server-registrations branch February 25, 2026 21:24
github-actions bot pushed a commit to microsoft/vscode-flake8 that referenced this pull request Feb 25, 2026
- Move _disposables cleanup outside lsClient guard in restartServer() so
  disposables are always released, even when no previous client existed.
- Wrap lsClient.stop() in deactivate() with try/catch to prevent unhandled
  rejections during extension deactivation.

Source: microsoft/vscode-python-tools-extension-template#259

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit to microsoft/vscode-black-formatter that referenced this pull request Feb 25, 2026
- Move _disposables cleanup outside the if(oldLsClient) block in restartServer()
  so disposables are always cleared even when no prior client exists
- Wrap lsClient.stop() in deactivate() with try/catch to handle errors gracefully

Upstream: microsoft/vscode-python-tools-extension-template#259

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit to microsoft/vscode-mypy that referenced this pull request Feb 26, 2026
- Move _disposables cleanup outside the if(oldLsClient) block in restartServer()
  so disposables are always cleaned up even if the old client is undefined
- Add try/catch around lsClient.stop() in deactivate() to prevent unhandled
  exceptions during extension deactivation

Source: microsoft/vscode-python-tools-extension-template#259

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
edvilme added a commit to microsoft/vscode-mypy that referenced this pull request Feb 26, 2026
…rts (#442)

- Move _disposables cleanup outside the if(oldLsClient) block in restartServer()
  so disposables are always cleaned up even if the old client is undefined
- Add try/catch around lsClient.stop() in deactivate() to prevent unhandled
  exceptions during extension deactivation

Source: microsoft/vscode-python-tools-extension-template#259

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
github-actions bot pushed a commit to microsoft/vscode-isort that referenced this pull request Feb 26, 2026
- Move _disposables cleanup outside the if(oldLsClient) block in
  restartServer() so disposables are always cleared, not just when
  a previous client exists.
- Add try/catch around lsClient.stop() in deactivate() to prevent
  unhandled rejections if the server stop fails during extension
  deactivation.

Source: microsoft/vscode-python-tools-extension-template#259

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants