From b4088bfa8e453759bc9d4e2b6ac1e6192cb1f7ba Mon Sep 17 00:00:00 2001 From: Debian Date: Wed, 18 Feb 2026 14:42:13 +0000 Subject: [PATCH] fix: guard webview access after panel disposal Both StatusPanel and ConfigPanel could crash with 'Webview is disposed' when _update() or _pollStatus() ran after the panel was closed. Added _disposed flag checked before any webview.html assignment. --- apps/extension/src/panels/setup.ts | 4 + apps/extension/src/panels/status.ts | 491 ++++++++++++++-------------- 2 files changed, 252 insertions(+), 243 deletions(-) diff --git a/apps/extension/src/panels/setup.ts b/apps/extension/src/panels/setup.ts index 93977f58..f272752e 100644 --- a/apps/extension/src/panels/setup.ts +++ b/apps/extension/src/panels/setup.ts @@ -33,6 +33,7 @@ export class ConfigPanel { public static currentPanel: ConfigPanel | undefined; private readonly _panel: vscode.WebviewPanel; private _disposables: vscode.Disposable[] = []; + private _disposed = false; private static readonly CONFIG_PATH = path.join(os.homedir(), '.openclaw', 'openclaw.json'); private static _customPath: string | undefined; @@ -76,6 +77,7 @@ export class ConfigPanel { } public dispose() { + this._disposed = true; ConfigPanel.currentPanel = undefined; this._panel.dispose(); this._disposables.forEach(d => d.dispose()); @@ -105,8 +107,10 @@ export class ConfigPanel { } private _update() { + if (this._disposed) return; const config = this._readConfig(); const targetPath = ConfigPanel._getConfigPath(); + if (this._disposed) return; this._panel.webview.html = this._getHtml(config, targetPath); } diff --git a/apps/extension/src/panels/status.ts b/apps/extension/src/panels/status.ts index 08183919..0b54e9b4 100644 --- a/apps/extension/src/panels/status.ts +++ b/apps/extension/src/panels/status.ts @@ -38,34 +38,35 @@ type RunResult = { exitCode?: number | null; }; -export class StatusPanel { - public static currentPanel: StatusPanel | undefined; - private readonly _panel: vscode.WebviewPanel; - private _disposables: vscode.Disposable[] = []; - private _refreshing = false; - private _refreshQueued = false; - private _lastStatus?: GatewayStatus; - private _lastStatusAt = 0; - private _isVisible = true; - private _gatewayTerminal?: vscode.Terminal; - - private constructor(panel: vscode.WebviewPanel) { - this._panel = panel; - this._isVisible = panel.visible; - void this._update(); - this._panel.onDidDispose(() => this.dispose(), null, this._disposables); - this._panel.onDidChangeViewState(e => { - this._isVisible = e.webviewPanel.visible; - if (this._isVisible) { - void this._update(); - } - }, null, this._disposables); - this._panel.webview.onDidReceiveMessage(msg => { - if (msg.command === 'refresh') { - if (!this._isVisible) return; - void this._update(); - } else if (msg.command === 'gateway-start') { - void this._runGateway('start'); +export class StatusPanel { + public static currentPanel: StatusPanel | undefined; + private readonly _panel: vscode.WebviewPanel; + private _disposables: vscode.Disposable[] = []; + private _refreshing = false; + private _refreshQueued = false; + private _lastStatus?: GatewayStatus; + private _lastStatusAt = 0; + private _isVisible = true; + private _disposed = false; + private _gatewayTerminal?: vscode.Terminal; + + private constructor(panel: vscode.WebviewPanel) { + this._panel = panel; + this._isVisible = panel.visible; + void this._update(); + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + this._panel.onDidChangeViewState(e => { + this._isVisible = e.webviewPanel.visible; + if (this._isVisible) { + void this._update(); + } + }, null, this._disposables); + this._panel.webview.onDidReceiveMessage(msg => { + if (msg.command === 'refresh') { + if (!this._isVisible) return; + void this._update(); + } else if (msg.command === 'gateway-start') { + void this._runGateway('start'); } else if (msg.command === 'gateway-stop') { void this._runGateway('stop'); } else if (msg.command === 'gateway-restart') { @@ -101,67 +102,70 @@ export class StatusPanel { } public dispose() { + this._disposed = true; StatusPanel.currentPanel = undefined; this._panel.dispose(); this._disposables.forEach(d => d.dispose()); } - private async _runGateway(action: string) { - const terminal = this._ensureGatewayTerminal(); - terminal.show(); - if (action === 'restart') { - const startCmd = 'openclaw gateway'; - terminal.sendText('\u0003', false); - await new Promise(r => setTimeout(r, 1500)); - terminal.sendText(startCmd, true); - vscode.window.showInformationMessage(`Sent: Ctrl+C then ${startCmd}`); - } else { - if (action === 'start') { - const cmd = 'openclaw gateway'; - terminal.sendText(cmd, true); - vscode.window.showInformationMessage(`Sent: ${cmd}`); - } else { - terminal.sendText('\u0003', false); - vscode.window.showInformationMessage('Sent: Ctrl+C'); - } - } - await this._update(); - await this._pollStatus(20000, 2000); - } - - private async _update() { - if (this._refreshing) { - this._refreshQueued = true; - return; - } - this._refreshing = true; - try { - const status = await this._getStatus(); - this._panel.webview.html = this._getHtml(status); - } finally { - this._refreshing = false; - if (this._refreshQueued) { - this._refreshQueued = false; - void this._update(); - } - } - } - - private async _getStatus(): Promise { - const now = Date.now(); - if (this._lastStatus && now - this._lastStatusAt < 3000) { - return this._lastStatus; - } - const status = await this._getStatusFresh(); - this._lastStatus = status; - this._lastStatusAt = now; - return status; - } - - private async _getStatusFresh(): Promise { - const updatedAt = new Date().toLocaleTimeString(); - const { command, result, cliPath } = await this._runOpenClaw(['gateway', 'status'], 4000); - const out = (result.stdout || result.stderr).trim(); + private async _runGateway(action: string) { + const terminal = this._ensureGatewayTerminal(); + terminal.show(); + if (action === 'restart') { + const startCmd = 'openclaw gateway'; + terminal.sendText('\u0003', false); + await new Promise(r => setTimeout(r, 1500)); + terminal.sendText(startCmd, true); + vscode.window.showInformationMessage(`Sent: Ctrl+C then ${startCmd}`); + } else { + if (action === 'start') { + const cmd = 'openclaw gateway'; + terminal.sendText(cmd, true); + vscode.window.showInformationMessage(`Sent: ${cmd}`); + } else { + terminal.sendText('\u0003', false); + vscode.window.showInformationMessage('Sent: Ctrl+C'); + } + } + await this._update(); + await this._pollStatus(20000, 2000); + } + + private async _update() { + if (this._refreshing) { + this._refreshQueued = true; + return; + } + this._refreshing = true; + try { + if (this._disposed) return; + const status = await this._getStatus(); + if (this._disposed) return; + this._panel.webview.html = this._getHtml(status); + } finally { + this._refreshing = false; + if (this._refreshQueued) { + this._refreshQueued = false; + void this._update(); + } + } + } + + private async _getStatus(): Promise { + const now = Date.now(); + if (this._lastStatus && now - this._lastStatusAt < 3000) { + return this._lastStatus; + } + const status = await this._getStatusFresh(); + this._lastStatus = status; + this._lastStatusAt = now; + return status; + } + + private async _getStatusFresh(): Promise { + const updatedAt = new Date().toLocaleTimeString(); + const { command, result, cliPath } = await this._runOpenClaw(['gateway', 'status'], 4000); + const out = (result.stdout || result.stderr).trim(); if (out.length > 0) { const parsed = this._parseStatus(out); const trimmedErr = result.stderr.trim(); @@ -220,41 +224,42 @@ export class StatusPanel { }; } - return { - installed: !result.notFound, - running: false, - exitCode: result.code, - cliPath, - command, - stderr: result.stderr.trim() || undefined, - pathEnv: result.pathEnv, - raw: 'Failed to read gateway status.', - error: result.error, - updatedAt, - }; - } - - private async _pollStatus(maxMs: number, intervalMs: number) { - const deadline = Date.now() + maxMs; - while (Date.now() < deadline) { - if (!this._isVisible) return; - const status = await this._getStatusFresh(); - this._panel.webview.html = this._getHtml(status); - if (status.running) return; - await new Promise(r => setTimeout(r, intervalMs)); - } - } - - private _ensureGatewayTerminal() { - if (this._gatewayTerminal) return this._gatewayTerminal; - const existing = vscode.window.terminals.find(t => t.name === 'OpenClaw Gateway'); - if (existing) { - this._gatewayTerminal = existing; - return existing; - } - this._gatewayTerminal = vscode.window.createTerminal('OpenClaw Gateway'); - return this._gatewayTerminal; - } + return { + installed: !result.notFound, + running: false, + exitCode: result.code, + cliPath, + command, + stderr: result.stderr.trim() || undefined, + pathEnv: result.pathEnv, + raw: 'Failed to read gateway status.', + error: result.error, + updatedAt, + }; + } + + private async _pollStatus(maxMs: number, intervalMs: number) { + const deadline = Date.now() + maxMs; + while (Date.now() < deadline) { + if (!this._isVisible || this._disposed) return; + const status = await this._getStatusFresh(); + if (this._disposed) return; + this._panel.webview.html = this._getHtml(status); + if (status.running) return; + await new Promise(r => setTimeout(r, intervalMs)); + } + } + + private _ensureGatewayTerminal() { + if (this._gatewayTerminal) return this._gatewayTerminal; + const existing = vscode.window.terminals.find(t => t.name === 'OpenClaw Gateway'); + if (existing) { + this._gatewayTerminal = existing; + return existing; + } + this._gatewayTerminal = vscode.window.createTerminal('OpenClaw Gateway'); + return this._gatewayTerminal; + } private _runCommand(cmd: string, timeoutMs: number): Promise { const env = this._buildExecEnv(); @@ -647,14 +652,14 @@ export class StatusPanel { return cleaned; } - private _parseStatus(out: string) { - const lower = out.toLowerCase(); - let running = /rpc probe:\s*ok/.test(lower); - if (/rpc probe:\s*failed/.test(lower)) running = false; - if (!/rpc probe:\s*(ok|failed)/.test(lower)) { - running = /runtime:\s*running|gateway:\s*running/.test(lower) || /running|active|started/.test(lower); - if (/not running|stopped|inactive|down|runtime:\s*stopped/.test(lower)) running = false; - } + private _parseStatus(out: string) { + const lower = out.toLowerCase(); + let running = /rpc probe:\s*ok/.test(lower); + if (/rpc probe:\s*failed/.test(lower)) running = false; + if (!/rpc probe:\s*(ok|failed)/.test(lower)) { + running = /runtime:\s*running|gateway:\s*running/.test(lower) || /running|active|started/.test(lower); + if (/not running|stopped|inactive|down|runtime:\s*stopped/.test(lower)) running = false; + } const pid = out.match(/pid[:\s]+(\d+)/i)?.[1]; const port = out.match(/port[:=\s]+(\d+)/i)?.[1]; @@ -708,122 +713,122 @@ export class StatusPanel { .replace(/'/g, '''); } - private _getHtml(status: GatewayStatus): string { - const { running, installed, dashboard, updatedAt } = status; - const safeDashboard = this._escapeHtml(dashboard || '—'); - return ` - - - - - - - -

OpenClaw Gateway

-
Last updated: ${updatedAt}
-
- - ${running ? 'Running' : installed ? 'Stopped' : 'Not Installed'} - ${running ? 'Healthy' : installed ? 'Offline' : 'Missing'} -
-
-
-
Status${running ? 'Running' : installed ? 'Stopped' : 'Missing'}
-
-
-
-
- Dashboard - ${dashboard ? `${safeDashboard}` : `${safeDashboard}`} -
-
-
- ${installed - ? (running - ? '' - : '') - : ''} - ${installed ? '' : ''} - -
- `;