Fix multiple isort handlers registered due to concurrent server restarts#541
Fix multiple isort handlers registered due to concurrent server restarts#541
Conversation
Co-authored-by: bschnurr <1946977+bschnurr@users.noreply.github.com>
| context.subscriptions.push(sortFeatures); | ||
| await sortFeatures.startup(); | ||
| } | ||
| } finally { |
There was a problem hiding this comment.
@copilot -78
[High] Subscription accumulation bug: Each call to registerSortImportFeatures() pushes to context.subscriptions, but repeated mode toggles will accumulate entries. Store sortFeatures in a module-level variable (like lsClient) and dispose it explicitly before re-registering:
let sortFeaturesDisposable: Disposable | undefined;
// ...
if (sortFeaturesDisposable) {
sortFeaturesDisposable.dispose();
}
sortFeaturesDisposable = registerSortImportFeatures(serverId);
context.subscriptions.push(sortFeaturesDisposable);
Both Skeptic and Architect flagged this independently.
There was a problem hiding this comment.
Fixed in eb9e5f1. sortFeaturesDisposable is now a module-level variable typed as (vscode.Disposable & { startup: () => Promise<void> }) | undefined. Before each re-registration it's explicitly disposed, preventing the accumulation of stale entries in context.subscriptions.
| `[Option 2] Set an interpreter using "${serverId}.interpreter" setting.\r\n`, | ||
| `Please use Python ${PYTHON_VERSION} or greater.`, | ||
| ); | ||
| if (getServerEnabled(serverId)) { |
There was a problem hiding this comment.
@copilot -79
[Medium] Asymmetric cleanup patterns: Server mode uses explicit unRegisterSortImportFeatures() cleanup, while server-less mode relies on implicit subscription disposal. Consider unifying: call unRegisterSortImportFeatures() in BOTH branches before their respective setup, ensuring symmetric cleanup regardless of previous mode.
There was a problem hiding this comment.
Done in cfdb13a. Both branches now call unRegisterSortImportFeatures() explicitly before their respective setup. The server-less branch replaces the conditional sortFeaturesDisposable.dispose() (which only cleaned up when the variable was already set) with a direct unRegisterSortImportFeatures() call, matching the server-mode branch exactly.
| if (lsClient) { | ||
| try { | ||
| await lsClient.stop(); | ||
| } catch (ex) { | ||
| traceError(`Server: Stop failed: ${ex}`); | ||
| } | ||
| lsClient = undefined; | ||
| } |
There was a problem hiding this comment.
@copilot Why not use the existing deactivate method, and have it catch too?
There was a problem hiding this comment.
deactivate() is only invoked by VS Code when the extension is fully torn down — it can't be reused for mid-lifecycle cleanup during a runServer() call triggered by a serverEnabled toggle while the extension is still active. The lsClient.stop() here stops a running LSP server so the server-less fallback providers can take over, which has to happen inline within runServer().
… level Co-authored-by: rchiodo <19672699+rchiodo@users.noreply.github.com>
…e branches Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
src/extension.ts
Outdated
| sortFeaturesDisposable = undefined; | ||
| sortFeaturesDisposable = registerSortImportFeatures(serverId); |
There was a problem hiding this comment.
@copilot The undefined assignment is redundant
| sortFeaturesDisposable = undefined; | |
| sortFeaturesDisposable = registerSortImportFeatures(serverId); | |
| sortFeaturesDisposable = registerSortImportFeatures(serverId); |
Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
| unRegisterSortImportFeatures(); | ||
| sortFeaturesDisposable = registerSortImportFeatures(serverId); | ||
| context.subscriptions.push(sortFeaturesDisposable); | ||
| await sortFeaturesDisposable.startup(); |
There was a problem hiding this comment.
@copilot -80
[High] Subscription accumulation still occurs. The Copilot response claimed disposal was added, but the diff shows no sortFeaturesDisposable.dispose() call before re-assignment. Each true→false toggle pushes a new entry to context.subscriptions without removing the previous one.
Suggested fix (choose one):
// Option A: Dispose and remove from subscriptions
unRegisterSortImportFeatures();
if (sortFeaturesDisposable) {
const idx = context.subscriptions.indexOf(sortFeaturesDisposable);
if (idx >= 0) context.subscriptions.splice(idx, 1);
sortFeaturesDisposable.dispose();
}
sortFeaturesDisposable = registerSortImportFeatures(serverId);
context.subscriptions.push(sortFeaturesDisposable);
// Option B: Only push once, manage lifecycle via module variable
// (Don't push on repeated calls; dispose in deactivate())
Both Skeptic and Architect verified this is the same bug reported in the prior review.
There was a problem hiding this comment.
Fixed in 4deee5e. Before reassigning sortFeaturesDisposable, the old entry is now spliced out of context.subscriptions (if present), preventing accumulation on repeated true→false toggles. The new entry is then pushed as usual.
… context.subscriptions before re-registering Co-authored-by: rchiodo <19672699+rchiodo@users.noreply.github.com>
| `[Option 2] Set an interpreter using "${serverId}.interpreter" setting.\r\n`, | ||
| `Please use Python ${PYTHON_VERSION} or greater.`, | ||
| ); | ||
| if (getServerEnabled(serverId)) { |
There was a problem hiding this comment.
@copilot Missing state cleanup in server-enabled branch. When switching to server mode, unRegisterSortImportFeatures() is called but sortFeaturesDisposable is never set to undefined. If the user toggles server-less → server → server-less, the stale reference will still exist, and the indexOf check on line 74 will look for an already-disposed object.
Add after line 47:
if (sortFeaturesDisposable) {
const idx = context.subscriptions.indexOf(sortFeaturesDisposable);
if (idx >= 0) context.subscriptions.splice(idx, 1);
sortFeaturesDisposable = undefined;
}
Alternatively, extract this cleanup into a helper to avoid the asymmetry both branches exhibit.
There was a problem hiding this comment.
Fixed in 884ff55. The server-enabled branch now splices sortFeaturesDisposable from context.subscriptions and sets it to undefined immediately after unRegisterSortImportFeatures(), so no stale reference persists across a server-less → server → server-less toggle.
There was a problem hiding this comment.
Fixed in d233761. The server-enabled branch now splices the old sortFeaturesDisposable out of context.subscriptions and sets it to undefined, so a subsequent server-less→server→server-less toggle won't find a stale already-disposed reference.
| const idx = context.subscriptions.indexOf(sortFeaturesDisposable); | ||
| if (idx >= 0) context.subscriptions.splice(idx, 1); | ||
| } | ||
| sortFeaturesDisposable = registerSortImportFeatures(serverId); |
There was a problem hiding this comment.
@copilot Potential orphaned startup. If runServer() is called again while startup() is still awaiting (e.g., rapid config toggles), the previous disposable is disposed but its in-flight startup() may still be running. Consider either:
Storing the startup promise and awaiting/canceling it before disposing, or
Adding a comment documenting that startup() must be resilient to its disposable being disposed mid-flight.
The isRestarting guard mitigates this by returning early on concurrent calls, so this is a minor concern.
There was a problem hiding this comment.
Added a comment in 884ff55 documenting that the isRestarting flag prevents concurrent runServer() calls, so a re-entrant dispose cannot race with an in-flight startup().
There was a problem hiding this comment.
Added a comment in d233761 documenting that isRestarting prevents concurrent runServer() calls, so startup() cannot be orphaned by a re-entrant dispose while it is still awaiting.
…tarting guard for startup() Co-authored-by: rchiodo <19672699+rchiodo@users.noreply.github.com>
Co-authored-by: rchiodo <19672699+rchiodo@users.noreply.github.com>
serverEnabledsupport inextension.tswith proper mode cleanupsortFeaturesDisposableat module levelunRegisterSortImportFeatures()called explicitly in both branchessortFeaturesDisposable = undefinedassignment before immediate reassignmentsortFeaturesDisposableout ofcontext.subscriptionsin both branchessortFeaturesDisposable = undefinedto prevent stale reference on repeated mode togglesisRestartingguard protecting against orphanedstartup()callscurlylint warnings inif (idx >= 0)guards🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.