Skip to content

Commit cbf890c

Browse files
committed
fix(vscode-extension): rebuild the log channel after disposal
Log caches its output channel for the life of the process, and activate registered that channel as a subscription. Disposing the subscriptions closed it while the cached reference stayed, so every later write threw 'Channel has been closed' — including the first line of a second activate. Activate now registers a disposable that drops the cache instead, so the next caller builds a fresh channel. Reverting this makes the new test fail with that exact error, plus cascades into 19 other failures, which is what surfaced it while covering the entry point. Signed-off-by: Stephen Whitlock <s.whitlock@live.com>
1 parent c782e50 commit cbf890c

5 files changed

Lines changed: 76 additions & 14 deletions

File tree

plugin/vscode/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010
### Added
1111
- Drop stale results when a file changes outside the editor
1212

13+
### Fixed
14+
- Rebuild the log channel after disposal, so logging survives a second
15+
activation instead of failing with 'Channel has been closed'
16+
1317
[1.0.1](https://github.com/jshwi/docsig/releases/tag/vscode-extension-v1.0.1) - 2026-07-10
1418
------------------------------------------------------------------------
1519
### Fixed

plugin/vscode/src/main/extension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ export function activate(context: vscode.ExtensionContext): void {
8787
const watcher = vscode.workspace.createFileSystemWatcher("**/*.py");
8888

8989
context.subscriptions.push(
90-
Log.outputChannel(),
90+
// not the channel itself: Log caches it, so disposing it directly
91+
// would leave the cached reference pointing at a closed channel
92+
{ dispose: () => Log.disposeChannel() },
9193
collection,
9294
service,
9395
vscode.workspace.onDidOpenTextDocument((document) =>

plugin/vscode/src/main/messages/Log.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ export function outputChannel(): vscode.OutputChannel {
1313
return channel;
1414
}
1515

16+
/**
17+
* Close the shared channel so the next caller gets a fresh one.
18+
*
19+
* The channel is cached for the life of the process, so disposing it
20+
* without dropping the reference leaves every later write throwing
21+
* "Channel has been closed". Activate registers this rather than the
22+
* channel itself, which is what makes a second activate work.
23+
*/
24+
export function disposeChannel(): void {
25+
channel?.dispose();
26+
channel = undefined;
27+
}
28+
1629
function emit(message: string): void {
1730
const line = `${PREFIX} ${message}`;
1831
outputChannel().appendLine(line);

plugin/vscode/src/test/Extension.test.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,11 @@ function configurationEvent(
6363
};
6464
}
6565

66-
function disposeAllButChannel(context: vscode.ExtensionContext): void {
67-
const channel = Log.outputChannel();
68-
context.subscriptions
69-
.filter((subscription) => subscription !== channel)
70-
.forEach((subscription) => {
71-
subscription.dispose();
72-
});
66+
function disposeAll(context: vscode.ExtensionContext): void {
67+
context.subscriptions.forEach((subscription) => {
68+
subscription.dispose();
69+
});
70+
context.subscriptions.length = 0;
7371
}
7472

7573
suite("extension", () => {
@@ -174,9 +172,7 @@ suite("extension", () => {
174172
try {
175173
assert.ok(context.subscriptions.length > 0);
176174
} finally {
177-
// the output channel is cached by Log for the life of the
178-
// process, so disposing it here closes it for every later test
179-
disposeAllButChannel(context);
175+
disposeAll(context);
180176
}
181177
});
182178

@@ -192,9 +188,27 @@ suite("extension", () => {
192188
try {
193189
assert.ok(context.subscriptions.length > 0);
194190
} finally {
195-
// the output channel is cached by Log for the life of the
196-
// process, so disposing it here closes it for every later test
197-
disposeAllButChannel(context);
191+
disposeAll(context);
192+
}
193+
});
194+
195+
test("activate runs a second time after a full dispose", () => {
196+
const context = mockExtensionContext("/tmp/docsig-ext", "/tmp/docsig-st");
197+
sinon.stub(vscode.window, "visibleTextEditors").value([]);
198+
199+
activate(context);
200+
disposeAll(context);
201+
202+
// the channel registered above is Log's cached one; if disposing it
203+
// did not drop the cache, this second run would throw "Channel has
204+
// been closed" from the first line of activate
205+
activate(context);
206+
207+
try {
208+
Log.debug("still writable");
209+
assert.ok(context.subscriptions.length > 0);
210+
} finally {
211+
disposeAll(context);
198212
}
199213
});
200214

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as assert from "node:assert/strict";
2+
import { disposeChannel, outputChannel } from "../../main/messages/Log";
3+
4+
suite("Log", () => {
5+
test("outputChannel returns the same channel while cached", () => {
6+
assert.equal(outputChannel(), outputChannel());
7+
});
8+
9+
test("disposeChannel drops the cache so the next call rebuilds", () => {
10+
const first = outputChannel();
11+
12+
disposeChannel();
13+
14+
const second = outputChannel();
15+
16+
assert.notEqual(first, second);
17+
18+
// the replacement is usable; the old one is closed for good
19+
second.appendLine("after dispose");
20+
});
21+
22+
test("disposeChannel is safe with nothing cached", () => {
23+
disposeChannel();
24+
25+
assert.doesNotThrow(() => {
26+
disposeChannel();
27+
});
28+
});
29+
});

0 commit comments

Comments
 (0)