diff --git a/FASTBuildMonitorVSCode/package.json b/FASTBuildMonitorVSCode/package.json index 66e3cf8..2bd24d7 100644 --- a/FASTBuildMonitorVSCode/package.json +++ b/FASTBuildMonitorVSCode/package.json @@ -31,6 +31,24 @@ "activationEvents": [], "main": "./out/extension.js", "contributes": { + "viewsContainers": { + "panel": [ + { + "id": "fbuildMonitorPanel", + "title": "FASTBuild Monitor", + "icon": "icon.png" + } + ] + }, + "views": { + "fbuildMonitorPanel": [ + { + "type": "webview", + "id": "fbuildMonitor.fbuildMonitorView", + "name": "FASTBuild Monitor" + } + ] + }, "commands": [ { "command": "fbuildMonitor.show", diff --git a/FASTBuildMonitorVSCode/src/buildMonitorService.ts b/FASTBuildMonitorVSCode/src/buildMonitorService.ts index 8a74e2f..37054a6 100644 --- a/FASTBuildMonitorVSCode/src/buildMonitorService.ts +++ b/FASTBuildMonitorVSCode/src/buildMonitorService.ts @@ -11,6 +11,7 @@ import { ReportProgressEvent, } from "./buildWatcher"; import { BuildJob, BuildJobStatus, BuildSession, WorkerInfo } from "./models"; +import { Logger } from "./utilities/logger"; /** * Service that tracks build state from FASTBuild log events. @@ -23,7 +24,7 @@ export class BuildMonitorService extends EventEmitter { private currentSession: BuildSession | undefined; private monitoring = false; - constructor(customLogPath?: string, ) { + constructor(private logger: Logger, customLogPath?: string) { super(); this.watcher = new BuildWatcher(customLogPath); @@ -66,6 +67,7 @@ export class BuildMonitorService extends EventEmitter { if (this.monitoring) { return; } + this.logger.info("Starting build monitoring..."); this.monitoring = true; this.watcher.start(pollIntervalMs); this.emit("stateChanged"); @@ -75,6 +77,7 @@ export class BuildMonitorService extends EventEmitter { if (!this.monitoring) { return; } + this.logger.info("Stopping build monitoring..."); this.monitoring = false; this.watcher.stop(); this.emit("stateChanged"); diff --git a/FASTBuildMonitorVSCode/src/extension.ts b/FASTBuildMonitorVSCode/src/extension.ts index a0cbd8e..18471d9 100644 --- a/FASTBuildMonitorVSCode/src/extension.ts +++ b/FASTBuildMonitorVSCode/src/extension.ts @@ -3,7 +3,7 @@ import * as vscode from "vscode"; import { BuildMonitorService } from "./buildMonitorService"; -import { MonitorPanel } from "./monitorPanel"; +import { MonitorPanelProvider } from "./monitorPanelProvider"; import { Logger } from "./utilities/logger"; import { ConfigIssue, ConfigManager } from "./utilities/configManager"; @@ -53,33 +53,35 @@ function initializeMonitorPanel( const config = vscode.workspace.getConfiguration("fbuildMonitor"); const customLogPath = config.get("logPath", ""); - service = new BuildMonitorService(customLogPath || undefined); + service = new BuildMonitorService(logger, customLogPath || undefined); logger.info( "Initialized BuildMonitorService with log path: " + service.getLogPath(), ); + MonitorPanelProvider.getOrCreate(context, service); + // Show panel command context.subscriptions.push( vscode.commands.registerCommand("fbuildMonitor.show", () => { - MonitorPanel.createOrShow(context.extensionUri, service!); + MonitorPanelProvider.getOrCreate(context, service!).show(); }), ); - + // Start monitoring command context.subscriptions.push( vscode.commands.registerCommand("fbuildMonitor.start", () => { - const pollInterval = config.get("pollIntervalMs", 500); - service!.start(pollInterval); + const panelProviderInstance = MonitorPanelProvider.getOrCreate(context, service!); + panelProviderInstance.start(); // Also open the panel if not already open - MonitorPanel.createOrShow(context.extensionUri, service!); + panelProviderInstance.show(); }), ); // Stop monitoring command context.subscriptions.push( vscode.commands.registerCommand("fbuildMonitor.stop", () => { - service!.stop(); + MonitorPanelProvider.getInstance()?.stop(); }), ); diff --git a/FASTBuildMonitorVSCode/src/monitorPanel.ts b/FASTBuildMonitorVSCode/src/monitorPanel.ts deleted file mode 100644 index 97df11c..0000000 --- a/FASTBuildMonitorVSCode/src/monitorPanel.ts +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -/* eslint-disable @typescript-eslint/restrict-template-expressions */ - -import * as vscode from 'vscode'; -import { BuildMonitorService } from './buildMonitorService'; -import { getNonce } from './utilities/getNonce'; -import { getUri } from './utilities/getUri'; - -export class MonitorPanel { - public static readonly viewType = 'fbuildMonitor'; - private static instance: MonitorPanel | undefined; - - private readonly panel: vscode.WebviewPanel; - private readonly service: BuildMonitorService; - private readonly extensionUri: vscode.Uri; - private disposables: vscode.Disposable[] = []; - private refreshTimer: ReturnType | undefined; - - public static createOrShow(extensionUri: vscode.Uri, service: BuildMonitorService): MonitorPanel { - const column = vscode.ViewColumn.Beside; - - if (MonitorPanel.instance) { - MonitorPanel.instance.panel.reveal(column); - return MonitorPanel.instance; - } - - const panel = vscode.window.createWebviewPanel( - MonitorPanel.viewType, - 'FASTBuild Monitor', - column, - { - enableScripts: true, - retainContextWhenHidden: true, - localResourceRoots: [ - vscode.Uri.joinPath(extensionUri, 'out'), - vscode.Uri.joinPath(extensionUri, 'webview-ui/build'), - ], - }, - ); - - MonitorPanel.instance = new MonitorPanel(panel, extensionUri, service); - return MonitorPanel.instance; - } - - public static getInstance(): MonitorPanel | undefined { - return MonitorPanel.instance; - } - - private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, service: BuildMonitorService) { - this.panel = panel; - this.extensionUri = extensionUri; - this.service = service; - - this.panel.webview.html = this.getWebviewContent(this.panel.webview, extensionUri); - - // Listen for messages from the webview - this.panel.webview.onDidReceiveMessage( - (message: { command: string }) => this.handleWebviewMessage(message), - undefined, - this.disposables, - ); - - // When the panel is disposed - this.panel.onDidDispose(() => this.dispose(), undefined, this.disposables); - - // Listen for state changes from the service - this.service.on('stateChanged', () => this.sendSnapshot()); - - // Periodic refresh for running timers (active jobs durations, elapsed time) - this.refreshTimer = setInterval(() => { - if (this.service.getCurrentSession() && !this.service.getCurrentSession()?.endTime) { - this.sendSnapshot(); - } - }, 1000); - - // Auto-start if configured - const config = vscode.workspace.getConfiguration('fbuildMonitor'); - if (config.get('autoStart', true)) { - const pollInterval = config.get('pollIntervalMs', 500); - this.service.start(pollInterval); - } - - // Send initial snapshot - this.sendSnapshot(); - } - - private handleWebviewMessage(message: { command: string }): void { - switch (message.command) { - case 'start': { - const config = vscode.workspace.getConfiguration('fbuildMonitor'); - const pollInterval = config.get('pollIntervalMs', 500); - this.service.start(pollInterval); - break; - } - case 'stop': - this.service.stop(); - break; - case 'requestSnapshot': - this.sendSnapshot(); - break; - } - } - - private sendSnapshot(): void { - const snapshot = this.service.getSnapshot(); - this.panel.webview.postMessage({ type: 'snapshot', data: snapshot }); - } - - public dispose(): void { - MonitorPanel.instance = undefined; - - if (this.refreshTimer) { - clearInterval(this.refreshTimer); - } - - this.panel.dispose(); - - while (this.disposables.length) { - const d = this.disposables.pop(); - d?.dispose(); - } - } - - private getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri): string { - const stylesUri = getUri(webview, extensionUri, ['webview-ui', 'build', 'assets', 'monitorPanel.css']); - const scriptUri = getUri(webview, extensionUri, ['webview-ui', 'build', 'assets', 'monitorPanel.js']); - const nonce = getNonce(); - - return ` - - - - - - - FASTBuild Monitor - - -
- - - `; - } -} diff --git a/FASTBuildMonitorVSCode/src/monitorPanelProvider.ts b/FASTBuildMonitorVSCode/src/monitorPanelProvider.ts new file mode 100644 index 0000000..7e8ad6d --- /dev/null +++ b/FASTBuildMonitorVSCode/src/monitorPanelProvider.ts @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/restrict-template-expressions */ + +import * as vscode from 'vscode'; +import { BuildMonitorService } from './buildMonitorService'; +import { getNonce } from './utilities/getNonce'; +import { getUri } from './utilities/getUri'; + +export class MonitorPanelProvider implements vscode.WebviewViewProvider { + public static readonly viewType = 'fbuildMonitor.fbuildMonitorView'; + private _view?: vscode.WebviewView; + private static instance: MonitorPanelProvider | undefined; + + private readonly service: BuildMonitorService; + private readonly extensionUri: vscode.Uri; + private disposables: vscode.Disposable[] = []; + private refreshTimer: ReturnType | undefined; + private isMonitoringOverride: boolean = false; + + public static getOrCreate(context: vscode.ExtensionContext, service: BuildMonitorService): MonitorPanelProvider { + if (MonitorPanelProvider.instance) { + return MonitorPanelProvider.instance; + } + + MonitorPanelProvider.instance = new MonitorPanelProvider(context.extensionUri, service); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(MonitorPanelProvider.viewType, MonitorPanelProvider.instance)); + return MonitorPanelProvider.instance; + } + + public static getInstance(): MonitorPanelProvider | undefined { + return MonitorPanelProvider.instance; + } + + private constructor(extensionUri: vscode.Uri, service: BuildMonitorService) { + this.extensionUri = extensionUri; + this.service = service; + } + + resolveWebviewView(webviewView: vscode.WebviewView, _context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken): Thenable | void { + this._view = webviewView; + + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.joinPath(this.extensionUri, 'out'), + vscode.Uri.joinPath(this.extensionUri, 'webview-ui/build'), + ], + }; + + webviewView.webview.html = this.getWebviewContent(webviewView.webview, this.extensionUri); + + // Listen for messages from the webview + webviewView.webview.onDidReceiveMessage( + (message: { command: string }) => this.handleWebviewMessage(message), + undefined, + this.disposables, + ); + + // When the panel is disposed + webviewView.onDidDispose(() => this.dispose(), undefined, this.disposables); + + // Listen for visibility changes, stop monitoring when hidden, start monitoring when shown (if override is set). + webviewView.onDidChangeVisibility(() => { + if (webviewView.visible) { + if(this.isMonitoringOverride) { + this.start(true /* force */); + } + this.sendSnapshot(); + } else { + this.stop(false /* don't override */); + } + }); + + // Listen for state changes from the service + this.service.on('stateChanged', () => this.sendSnapshot()); + + // Periodic refresh for running timers (active jobs durations, elapsed time) + if (this.refreshTimer) { + clearInterval(this.refreshTimer); + } + this.refreshTimer = setInterval(() => { + if ( + this.service.isMonitoring() && + this.service.getCurrentSession() && + !this.service.getCurrentSession()?.endTime + ) { + this.sendSnapshot(); + } + }, 1000); + + // Auto-start if configured + const config = vscode.workspace.getConfiguration('fbuildMonitor'); + if (config.get('autoStart', true)) { + this.start(); + } + + // Send initial snapshot + this.sendSnapshot(); + } + + private handleWebviewMessage(message: { command: string }): void { + switch (message.command) { + case 'start': { + this.start(); + break; + } + case 'stop': + this.stop(); + break; + case 'requestSnapshot': + this.sendSnapshot(); + break; + } + } + + private sendSnapshot(): void { + const snapshot = this.service.getSnapshot(); + this._view?.webview.postMessage({ type: 'snapshot', data: snapshot }); + } + + public start(force: boolean = false) { + this.isMonitoringOverride = true; + if (!force && !this.isViewVisible()) { + return; + } + const config = vscode.workspace.getConfiguration("fbuildMonitor"); + const pollInterval = config.get("pollIntervalMs", 500); + this.service.start(pollInterval); + } + + public stop(override: boolean = true){ + this.service.stop(); + if (override) { + this.isMonitoringOverride = false; + } + } + + public show(){ + vscode.commands.executeCommand(`${MonitorPanelProvider.viewType}.focus`); + } + + private isViewVisible(): boolean { + return this._view?.visible ?? false; + } + + public dispose(): void { + if (this.refreshTimer) { + clearInterval(this.refreshTimer); + } + + // this._view?.dispose(); + + while (this.disposables.length) { + const d = this.disposables.pop(); + d?.dispose(); + } + } + + private getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri): string { + const stylesUri = getUri(webview, extensionUri, ['webview-ui', 'build', 'assets', 'monitorPanel.css']); + const scriptUri = getUri(webview, extensionUri, ['webview-ui', 'build', 'assets', 'monitorPanel.js']); + const nonce = getNonce(); + + return ` + + + + + + + FASTBuild Monitor + + +
+ + + `; + } +}