| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as vscode from 'vscode'; |
| import { Ffx, FfxLog, FuchsiaDevice } from '../ffx'; |
| import * as logger from '../logger'; |
| |
| export const TOGGLE_LOG_VIEW_COMMAND = 'workbench.view.extension.fuchsia-logging'; |
| |
| export class LoggingViewProvider implements vscode.WebviewViewProvider { |
| public static readonly viewType = 'vscode-fuchsia.loggingView'; |
| |
| private view?: vscode.WebviewView; |
| private process?: FfxLog; |
| private currentDevice?: string; |
| private lastVisibilitySeen: boolean = false; |
| |
| constructor( |
| private readonly extensionUri: vscode.Uri, |
| private readonly ffx: Ffx, |
| ) { |
| // NB: we're explicitly using an async closure here |
| // event tho onSetTarget isn't expecting it. We're doing |
| // UI operations in response to events, it's fine to run |
| // "in the background" |
| this.ffx.onSetTarget(async (device) => { |
| if (this.currentDevice !== device?.nodeName) { |
| this.currentDevice = device?.nodeName; |
| if (this.view?.visible) { |
| await this.resetView(); |
| this.startListeningForLogs(); |
| } |
| } |
| }); |
| } |
| |
| public get currentTargetDevice(): string | undefined { |
| return this.currentDevice; |
| } |
| |
| public resolveWebviewView( |
| webviewView: vscode.WebviewView, |
| context: vscode.WebviewViewResolveContext<unknown>, |
| token: vscode.CancellationToken |
| ): void { |
| this.startListeningForLogs(); |
| |
| this.view = webviewView; |
| |
| webviewView.webview.options = { |
| // Allow scripts in the webview |
| enableScripts: true, |
| |
| localResourceRoots: [ |
| this.extensionUri |
| ] |
| }; |
| |
| webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); |
| |
| webviewView.webview.onDidReceiveMessage(message => { |
| switch (message.command) { |
| case 'pauseLogStreaming': |
| this.process?.stop(); |
| break; |
| case 'resumeLogStreaming': |
| this.startListeningForLogs('now'); |
| break; |
| } |
| }); |
| |
| this.lastVisibilitySeen = this.view.visible; |
| this.view.onDidChangeVisibility(() => { |
| if (!this.view?.visible) { |
| // If the view is hidden, we stop ffx. |
| this.lastVisibilitySeen = false; |
| this.process?.stop(); |
| } else if (!this.lastVisibilitySeen) { |
| // If the view was hidden and it becomes visible then we re-run ffx |
| this.lastVisibilitySeen = true; |
| this.startListeningForLogs(); |
| } |
| }); |
| } |
| |
| public async resetAndShowView(device: FuchsiaDevice | undefined) { |
| // If the device changed, update it. |
| if (this.currentDevice !== device?.nodeName) { |
| this.currentDevice = device?.nodeName; |
| |
| // If the device changed and the view was already visible, clear it and start |
| // streaming logs from the new device. |
| if (this.view?.visible) { |
| await this.resetView(); |
| this.startListeningForLogs(); |
| return; |
| } |
| } |
| |
| // If we don't have a webview, make sure to create one. |
| if (this.view === undefined) { |
| await vscode.commands.executeCommand(TOGGLE_LOG_VIEW_COMMAND); |
| } |
| |
| // If the view wasn't visible, make it visible. This will result in streaming logs for the |
| // currently selected device. |
| if (!this.view?.visible) { |
| this.view?.show(); |
| } |
| } |
| |
| private async addLog(log: Object) { |
| await this.view?.webview.postMessage({ type: 'addLog', log }); |
| } |
| |
| private async resetView() { |
| await this.view?.webview.postMessage({ type: 'reset' }); |
| } |
| |
| private startListeningForLogs(since?: string) { |
| this.process?.stop(); |
| const args = since ? ['--since', since] : []; |
| this.process = this.ffx.runFfxJsonStreaming( |
| this.currentDevice, |
| ['log', ...args], |
| (data: Object) => { |
| // this was... not marked async before, but postMessage is an async operation |
| // so there's not much to do about this here. We've gotta just go async & hope. |
| this.addLog(data).catch((err) => logger.error('unable to show log line', undefined, err)); |
| }); |
| } |
| |
| private _getHtmlForWebview(webview: vscode.Webview): string { |
| // Get the local path to main script run in the webview, then convert it to a uri we can use in |
| // the webview. |
| const scriptUri = webview.asWebviewUri( |
| vscode.Uri.joinPath(this.extensionUri, 'dist', 'webviews-logging.js')); |
| |
| // Do the same for the stylesheet. |
| const styleResetUri = webview.asWebviewUri( |
| vscode.Uri.joinPath(this.extensionUri, 'media', 'reset.css')); |
| const styleVSCodeUri = webview.asWebviewUri( |
| vscode.Uri.joinPath(this.extensionUri, 'media', 'vscode.css')); |
| const styleMainUri = webview.asWebviewUri( |
| vscode.Uri.joinPath(this.extensionUri, 'media', 'webview-logging.css')); |
| const codiconsCssUri = webview.asWebviewUri( |
| vscode.Uri.joinPath(this.extensionUri, 'media', 'codicon.css')); |
| const codiconsUri = webview.asWebviewUri( |
| vscode.Uri.joinPath(this.extensionUri, 'media', 'codicon.ttf')); |
| |
| // Use a nonce to only allow a specific script to be run. |
| const nonce = this.getNonce(); |
| |
| // TODO(fxbug.dev/100046): remove the unsafe-inline CSP once the global stylesheet has been |
| // elimiated since everything will be web components. |
| return `<!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <!-- |
| Use a content security policy to only allow loading images from https or from our |
| extension directory, and only allow scripts that have a specific nonce. |
| --> |
| <meta http-equiv="Content-Security-Policy" content="default-src 'none'; |
| style-src ${webview.cspSource}; |
| script-src 'nonce-${nonce}'; |
| font-src ${webview.cspSource}"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <link href="${styleResetUri}" rel="stylesheet"> |
| <link href="${styleVSCodeUri}" rel="stylesheet"> |
| <link href="${codiconsUri}" rel="preload" as="font" crossorigin/> |
| <link href="${codiconsCssUri}" rel="stylesheet"/> |
| <link href="${styleMainUri}" rel="stylesheet"> |
| |
| <title>Fuchsia Logs</title> |
| </head> |
| <body> |
| <script nonce="${nonce}" src="${scriptUri}"></script> |
| </body> |
| </html>`; |
| } |
| |
| private getNonce() { |
| let text = ''; |
| const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; |
| for (let i = 0; i < 32; i++) { |
| text += possible.charAt(Math.floor(Math.random() * possible.length)); |
| } |
| return text; |
| } |
| } |