From 19fbf6feafa8cbd394f9f1216e8688136c069f80 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 8 Jul 2022 08:10:26 +1000 Subject: [PATCH] Simply script uri converter --- .../ipywidgets/commonMessageCoordinator.ts | 12 +- .../ipywidgets/ipyWidgetScriptSource.ts | 30 +++-- src/kernels/ipywidgets/scriptUriConverter.ts | 126 ++---------------- .../ipywidgets/serviceRegistry.node.ts | 9 +- src/kernels/ipywidgets/serviceRegistry.web.ts | 9 +- src/kernels/ipywidgets/types.ts | 15 --- .../controllers/controllerRegistration.ts | 3 - .../controllers/vscodeNotebookController.ts | 4 +- 8 files changed, 42 insertions(+), 166 deletions(-) diff --git a/src/kernels/ipywidgets/commonMessageCoordinator.ts b/src/kernels/ipywidgets/commonMessageCoordinator.ts index 041ad686ebc..17f1382c209 100644 --- a/src/kernels/ipywidgets/commonMessageCoordinator.ts +++ b/src/kernels/ipywidgets/commonMessageCoordinator.ts @@ -8,7 +8,13 @@ import { Event, EventEmitter, NotebookDocument } from 'vscode'; import { IApplicationShell, ICommandManager } from '../../platform/common/application/types'; import { STANDARD_OUTPUT_CHANNEL } from '../../platform/common/constants'; import { traceVerbose, traceError, traceInfo, traceInfoIfCI } from '../../platform/logging'; -import { IDisposableRegistry, IOutputChannel, IConfigurationService, IHttpClient } from '../../platform/common/types'; +import { + IDisposableRegistry, + IOutputChannel, + IConfigurationService, + IHttpClient, + IsWebExtension +} from '../../platform/common/types'; import { Common, DataScience } from '../../platform/common/utils/localize'; import { noop } from '../../platform/common/utils/misc'; import { stripAnsi } from '../../platform/common/utils/regexp'; @@ -26,7 +32,7 @@ import { Commands } from '../../platform/common/constants'; import { IKernelProvider } from '../types'; import { IPyWidgetMessageDispatcherFactory } from './ipyWidgetMessageDispatcherFactory'; import { IPyWidgetScriptSource } from './ipyWidgetScriptSource'; -import { IIPyWidgetMessageDispatcher, ILocalResourceUriConverter, IWidgetScriptSourceProviderFactory } from './types'; +import { IIPyWidgetMessageDispatcher, IWidgetScriptSourceProviderFactory } from './types'; import { ConsoleForegroundColors } from '../../platform/logging/types'; import { createDeferred } from '../../platform/common/utils/async'; import { IWebviewCommunication } from '../../platform/webviews/types'; @@ -277,7 +283,7 @@ export class CommonMessageCoordinator { this.serviceContainer.get(IConfigurationService), this.serviceContainer.get(IHttpClient), this.serviceContainer.get(IWidgetScriptSourceProviderFactory), - this.serviceContainer.get(ILocalResourceUriConverter) + this.serviceContainer.get(IsWebExtension) ); this.disposables.push(this.ipyWidgetScriptSource.postMessage(this.cacheOrSend, this)); } diff --git a/src/kernels/ipywidgets/ipyWidgetScriptSource.ts b/src/kernels/ipywidgets/ipyWidgetScriptSource.ts index c5501c3e4fb..0a75ff0dea7 100644 --- a/src/kernels/ipywidgets/ipyWidgetScriptSource.ts +++ b/src/kernels/ipywidgets/ipyWidgetScriptSource.ts @@ -14,7 +14,9 @@ import { ILocalResourceUriConverter, IWidgetScriptSourceProviderFactory, WidgetS import { ConsoleForegroundColors } from '../../platform/logging/types'; import { getAssociatedNotebookDocument } from '../helpers'; import { noop } from '../../platform/common/utils/misc'; -import { createDeferred } from '../../platform/common/utils/async'; +import { createDeferred, Deferred } from '../../platform/common/utils/async'; +import { ScriptUriConverter } from './scriptUriConverter'; +import { ResourceMap } from '../../platform/vscode-path/map'; export class IPyWidgetScriptSource { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -40,6 +42,8 @@ export class IPyWidgetScriptSource { * Key value pair of widget modules along with the version that needs to be loaded. */ private pendingModuleRequests = new Map(); + private readonly uriConverter: ILocalResourceUriConverter; + private readonly uriTranslationRequests = new ResourceMap>(); constructor( private readonly document: NotebookDocument, private readonly kernelProvider: IKernelProvider, @@ -47,17 +51,17 @@ export class IPyWidgetScriptSource { private readonly configurationSettings: IConfigurationService, private readonly httpClient: IHttpClient, private readonly sourceProviderFactory: IWidgetScriptSourceProviderFactory, - private readonly uriConverter: ILocalResourceUriConverter + isWebExtension: boolean ) { - uriConverter.requestUri( - (e) => - this.postEmitter.fire({ - message: InteractiveWindowMessages.ConvertUriForUseInWebViewRequest, - payload: e - }), - undefined, - disposables - ); + this.uriConverter = new ScriptUriConverter(isWebExtension, (resource) => { + if (!this.uriTranslationRequests.has(resource)) + this.uriTranslationRequests.set(resource, createDeferred()); + this.postEmitter.fire({ + message: InteractiveWindowMessages.ConvertUriForUseInWebViewRequest, + payload: resource + }); + return this.uriTranslationRequests.get(resource)!.promise; + }); // Don't leave dangling promises. this.isWebViewOnline.promise.ignoreErrors(); disposables.push(this); @@ -82,8 +86,8 @@ export class IPyWidgetScriptSource { public onMessage(message: string, payload?: any): void { if (message === InteractiveWindowMessages.ConvertUriForUseInWebViewResponse) { const response: undefined | { request: Uri; response: Uri } = payload; - if (response) { - this.uriConverter.resolveUri(response.request, response.response); + if (response && this.uriTranslationRequests.has(response.request)) { + this.uriTranslationRequests.get(response.request)!.resolve(response.response); } } else if (message === IPyWidgetMessages.IPyWidgets_Ready) { this.sendBaseUrl(); diff --git a/src/kernels/ipywidgets/scriptUriConverter.ts b/src/kernels/ipywidgets/scriptUriConverter.ts index 349f8cead38..5d41f1f8109 100644 --- a/src/kernels/ipywidgets/scriptUriConverter.ts +++ b/src/kernels/ipywidgets/scriptUriConverter.ts @@ -1,122 +1,22 @@ -import { EventEmitter, Event, Uri, FileType } from 'vscode'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; import { ILocalResourceUriConverter } from './types'; -import * as uriPath from '../../platform/vscode-path/resources'; -import { inject, injectable } from 'inversify'; -import { IFileSystem } from '../../platform/common/platform/types'; -import { IExtensionContext, IsWebExtension } from '../../platform/common/types'; -import { sha256 } from 'hash.js'; -import { createDeferred, Deferred } from '../../platform/common/utils/async'; -import { traceInfo, traceError } from '../../platform/logging'; -import { getComparisonKey } from '../../platform/vscode-path/resources'; -import { getFilePath } from '../../platform/common/platform/fs-paths'; -/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports */ -const sanitize = require('sanitize-filename'); -@injectable() export class ScriptUriConverter implements ILocalResourceUriConverter { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public get requestUri(): Event { - return this.requestUriEmitter.event; - } - public get rootScriptFolder(): Uri { - return this._rootScriptFolder; - } - private readonly _rootScriptFolder: Uri; - private readonly createTargetWidgetScriptsFolder: Promise; - private readonly targetWidgetScriptsFolder: Uri; - private readonly resourcesMappedToExtensionFolder = new Map>(); - private readonly uriConversionPromises = new Map>(); - private requestUriEmitter = new EventEmitter(); + constructor(private readonly isWebExtension: boolean, private readonly converter: (input: Uri) => Promise) {} /** * This method is called to convert a Uri to a format such that it can be used in a webview. - * WebViews only allow files that are part of extension and the same directory where notebook lives. - * To ensure widgets can find the js files, we copy the script file to a into the extensionr folder `tmp/nbextensions`. - * (storing files in `tmp/nbextensions` is relatively safe as this folder gets deleted when ever a user updates to a new version of VSC). - * Hence we need to copy for every version of the extension. - * Copying into global workspace folder would also work, but over time this folder size could grow (in an unmanaged way). + * Some times we have resources in the extension/tmp folder that contain static resources such as JS, image files and the like. + * The webview can load them, however the Uri needs to be in a special format for that to work. + * Note: Currently this is only use for Jupyter Widgets. */ - public async asWebviewUri(localResource: Uri): Promise { - if (this.isWeb) { - // We cannot create folders on the web, return this as is. - return localResource; - } - // Make a copy of the local file if not already in the correct location - if (!this.isInScriptPath(localResource)) { - const key = getComparisonKey(localResource); - if (!this.resourcesMappedToExtensionFolder.has(key)) { - const deferred = createDeferred(); - this.resourcesMappedToExtensionFolder.set(key, deferred.promise); - try { - // Create a file name such that it will be unique and consistent across VSC reloads. - // Only if original file has been modified should we create a new copy of the sam file. - const fileHash: string = await this.fs.getFileHash(localResource); - const uniqueFileName = sanitize( - sha256() - .update(`${getFilePath(localResource)}${fileHash}`) - .digest('hex') - ); - const targetFolder = await this.createTargetWidgetScriptsFolder; - const mappedResource = uriPath.joinPath( - targetFolder, - `${uniqueFileName}${uriPath.basename(localResource)}` - ); - if (!(await this.fs.exists(mappedResource))) { - await this.fs.copy(localResource, mappedResource); - } - traceInfo( - `Widget Script file ${getFilePath(localResource)} mapped to ${getFilePath(mappedResource)}` - ); - deferred.resolve(mappedResource); - } catch (ex) { - traceError(`Failed to map widget Script file ${getFilePath(localResource)}`); - deferred.reject(ex); - } - } - localResource = await this.resourcesMappedToExtensionFolder.get(key)!; - } - const key = getComparisonKey(localResource); - if (!this.uriConversionPromises.has(key)) { - this.uriConversionPromises.set(key, createDeferred()); - // Send a request for the translation. - this.requestUriEmitter.fire(localResource); - } - return this.uriConversionPromises.get(key)!.promise; - } - - public resolveUri(request: Uri, result: Uri): void { - const key = getComparisonKey(request); - if (this.uriConversionPromises.get(key)) { - this.uriConversionPromises.get(key)!.resolve(result); - } - } - - constructor( - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IExtensionContext) extensionContext: IExtensionContext, - @inject(IsWebExtension) private readonly isWeb: boolean - ) { - // Scripts have to be written somewhere we can: - // - Write to disk - // - Convert into a URI that can be loaded - // For now only extensionUri is convertable (notebook code adds this path as a localResourceRoot) - // but that doesn't work in web because it's readonly. - // This is pending: https://github.com/microsoft/vscode/issues/149868 - this._rootScriptFolder = uriPath.joinPath(extensionContext.extensionUri, 'tmp', 'scripts'); - this.targetWidgetScriptsFolder = uriPath.joinPath(this._rootScriptFolder, 'nbextensions'); - if (!this.isWeb) { - this.createTargetWidgetScriptsFolder = this.fs - .exists(this.targetWidgetScriptsFolder, FileType.Directory) - .then(async (exists) => { - if (!exists) { - await this.fs.createDirectory(this.targetWidgetScriptsFolder); - } - return this.targetWidgetScriptsFolder; - }); - } - } - - private isInScriptPath(uri: Uri) { - return uriPath.isEqualOrParent(uri, this._rootScriptFolder, false); + public async asWebviewUri(resource: Uri): Promise { + // In the case of web extension, we don't have any local resources, everything is served remotely either via + // remote jupyter server or via a web extension. + // Hence no need to transform the uri in web extension. + return this.isWebExtension ? resource : this.converter(resource); } } diff --git a/src/kernels/ipywidgets/serviceRegistry.node.ts b/src/kernels/ipywidgets/serviceRegistry.node.ts index 1a548eedea8..182b077c6ff 100644 --- a/src/kernels/ipywidgets/serviceRegistry.node.ts +++ b/src/kernels/ipywidgets/serviceRegistry.node.ts @@ -1,12 +1,6 @@ import { IServiceManager } from '../../platform/ioc/types'; import { ScriptSourceProviderFactory } from './scriptSourceProviderFactory.node'; -import { ScriptUriConverter } from './scriptUriConverter'; -import { - IIPyWidgetScriptManagerFactory, - ILocalResourceUriConverter, - INbExtensionsPathProvider, - IWidgetScriptSourceProviderFactory -} from './types'; +import { IIPyWidgetScriptManagerFactory, INbExtensionsPathProvider, IWidgetScriptSourceProviderFactory } from './types'; import { IPyWidgetMessageDispatcherFactory } from './ipyWidgetMessageDispatcherFactory'; import { NbExtensionsPathProvider } from './nbExtensionsPathProvider.node'; import { IPyWidgetScriptManagerFactory } from './ipyWidgetScriptManagerFactory.node'; @@ -17,7 +11,6 @@ export function registerTypes(serviceManager: IServiceManager, _isDevMode: boole IPyWidgetMessageDispatcherFactory ); serviceManager.addSingleton(IWidgetScriptSourceProviderFactory, ScriptSourceProviderFactory); - serviceManager.add(ILocalResourceUriConverter, ScriptUriConverter); serviceManager.addSingleton(IIPyWidgetScriptManagerFactory, IPyWidgetScriptManagerFactory); serviceManager.addSingleton(INbExtensionsPathProvider, NbExtensionsPathProvider); } diff --git a/src/kernels/ipywidgets/serviceRegistry.web.ts b/src/kernels/ipywidgets/serviceRegistry.web.ts index 224b0feb5e0..92ca7b61c2a 100644 --- a/src/kernels/ipywidgets/serviceRegistry.web.ts +++ b/src/kernels/ipywidgets/serviceRegistry.web.ts @@ -1,12 +1,6 @@ import { IServiceManager } from '../../platform/ioc/types'; import { ScriptSourceProviderFactory } from './scriptSourceProviderFactory.web'; -import { ScriptUriConverter } from './scriptUriConverter'; -import { - IIPyWidgetScriptManagerFactory, - ILocalResourceUriConverter, - INbExtensionsPathProvider, - IWidgetScriptSourceProviderFactory -} from './types'; +import { IIPyWidgetScriptManagerFactory, INbExtensionsPathProvider, IWidgetScriptSourceProviderFactory } from './types'; import { IPyWidgetMessageDispatcherFactory } from './ipyWidgetMessageDispatcherFactory'; import { NbExtensionsPathProvider } from './nbExtensionsPathProvider.web'; import { IPyWidgetScriptManagerFactory } from './ipyWidgetScriptManagerFactory.web'; @@ -17,7 +11,6 @@ export function registerTypes(serviceManager: IServiceManager, _isDevMode: boole IPyWidgetMessageDispatcherFactory ); serviceManager.addSingleton(IWidgetScriptSourceProviderFactory, ScriptSourceProviderFactory); - serviceManager.add(ILocalResourceUriConverter, ScriptUriConverter); serviceManager.addSingleton(IIPyWidgetScriptManagerFactory, IPyWidgetScriptManagerFactory); serviceManager.addSingleton(INbExtensionsPathProvider, NbExtensionsPathProvider); } diff --git a/src/kernels/ipywidgets/types.ts b/src/kernels/ipywidgets/types.ts index 9aceed9b38e..a239248197f 100644 --- a/src/kernels/ipywidgets/types.ts +++ b/src/kernels/ipywidgets/types.ts @@ -80,16 +80,10 @@ export interface IWidgetScriptSourceProviderFactory { ): IWidgetScriptSourceProvider[]; } -export const ILocalResourceUriConverter = Symbol('ILocalResourceUriConverter'); - /** * Given a local resource this will convert the Uri into a form such that it can be used in a WebView. */ export interface ILocalResourceUriConverter { - /** - * Root folder that scripts should be copied to. - */ - readonly rootScriptFolder: Uri; /** * Convert a uri for the local file system to one that can be used inside webviews. * @@ -102,15 +96,6 @@ export interface ILocalResourceUriConverter { * ``` */ asWebviewUri(localResource: Uri): Promise; - /** - * The converter will post an event when it needs to convert the webview URI - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - requestUri: Event; - /** - * This is the response to the requestUri event - */ - resolveUri(request: Uri, result: Uri): void; } export const INbExtensionsPathProvider = Symbol('INbExtensionsPathProvider'); diff --git a/src/notebooks/controllers/controllerRegistration.ts b/src/notebooks/controllers/controllerRegistration.ts index 9687582405e..63134ef69c8 100644 --- a/src/notebooks/controllers/controllerRegistration.ts +++ b/src/notebooks/controllers/controllerRegistration.ts @@ -4,7 +4,6 @@ import { inject, injectable } from 'inversify'; import { Event, EventEmitter } from 'vscode'; import { getDisplayNameOrNameOfKernelConnection } from '../../kernels/helpers'; -import { ILocalResourceUriConverter } from '../../kernels/ipywidgets/types'; import { computeServerId } from '../../kernels/jupyter/jupyterUtils'; import { IJupyterServerUriStorage, IServerConnectionType } from '../../kernels/jupyter/types'; import { IKernelProvider, isLocalConnection, isRemoteConnection, KernelConnectionMetadata } from '../../kernels/types'; @@ -68,7 +67,6 @@ export class ControllerRegistration implements IControllerRegistration { @inject(IApplicationShell) private readonly appShell: IApplicationShell, @inject(IBrowserService) private readonly browser: IBrowserService, @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @inject(ILocalResourceUriConverter) private readonly resourceConverter: ILocalResourceUriConverter, @inject(IServerConnectionType) private readonly serverConnectionType: IServerConnectionType, @inject(IJupyterServerUriStorage) private readonly serverUriStorage: IJupyterServerUriStorage ) { @@ -130,7 +128,6 @@ export class ControllerRegistration implements IControllerRegistration { this.appShell, this.browser, this.extensionChecker, - this.resourceConverter, this.serviceContainer ); controller.onDidDispose( diff --git a/src/notebooks/controllers/vscodeNotebookController.ts b/src/notebooks/controllers/vscodeNotebookController.ts index 8207655583a..903aaef7769 100644 --- a/src/notebooks/controllers/vscodeNotebookController.ts +++ b/src/notebooks/controllers/vscodeNotebookController.ts @@ -71,7 +71,6 @@ import { getNotebookMetadata, isJupyterNotebook } from '../../platform/common/ut import { ConsoleForegroundColors, TraceOptions } from '../../platform/logging/types'; import { KernelConnector } from './kernelConnector'; import { IVSCodeNotebookController } from './types'; -import { ILocalResourceUriConverter } from '../../kernels/ipywidgets/types'; import { isCancellationError } from '../../platform/common/cancellation'; import { CellExecutionCreator } from '../../kernels/execution/cellExecutionCreator'; import { @@ -154,7 +153,6 @@ export class VSCodeNotebookController implements Disposable, IVSCodeNotebookCont private readonly appShell: IApplicationShell, private readonly browser: IBrowserService, private readonly extensionChecker: IPythonExtensionChecker, - scriptConverter: ILocalResourceUriConverter, private serviceContainer: IServiceContainer ) { disposableRegistry.push(this); @@ -170,7 +168,7 @@ export class VSCodeNotebookController implements Disposable, IVSCodeNotebookCont label, this.handleExecution.bind(this), this.getRendererScripts(), - [scriptConverter.rootScriptFolder] + [] ); // Fill in extended info for our controller