Skip to content

Commit

Permalink
Simply script uri converter (#10728)
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne authored Jul 11, 2022
1 parent 990c016 commit 47b7c29
Show file tree
Hide file tree
Showing 8 changed files with 42 additions and 166 deletions.
12 changes: 9 additions & 3 deletions src/kernels/ipywidgets/commonMessageCoordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -277,7 +283,7 @@ export class CommonMessageCoordinator {
this.serviceContainer.get<IConfigurationService>(IConfigurationService),
this.serviceContainer.get<IHttpClient>(IHttpClient),
this.serviceContainer.get<IWidgetScriptSourceProviderFactory>(IWidgetScriptSourceProviderFactory),
this.serviceContainer.get<ILocalResourceUriConverter>(ILocalResourceUriConverter)
this.serviceContainer.get<boolean>(IsWebExtension)
);
this.disposables.push(this.ipyWidgetScriptSource.postMessage(this.cacheOrSend, this));
}
Expand Down
30 changes: 17 additions & 13 deletions src/kernels/ipywidgets/ipyWidgetScriptSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,24 +42,26 @@ export class IPyWidgetScriptSource {
* Key value pair of widget modules along with the version that needs to be loaded.
*/
private pendingModuleRequests = new Map<string, { moduleVersion?: string; requestId?: string }>();
private readonly uriConverter: ILocalResourceUriConverter;
private readonly uriTranslationRequests = new ResourceMap<Deferred<Uri>>();
constructor(
private readonly document: NotebookDocument,
private readonly kernelProvider: IKernelProvider,
disposables: IDisposableRegistry,
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<Uri>());
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);
Expand All @@ -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();
Expand Down
126 changes: 13 additions & 113 deletions src/kernels/ipywidgets/scriptUriConverter.ts
Original file line number Diff line number Diff line change
@@ -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<Uri> {
return this.requestUriEmitter.event;
}
public get rootScriptFolder(): Uri {
return this._rootScriptFolder;
}
private readonly _rootScriptFolder: Uri;
private readonly createTargetWidgetScriptsFolder: Promise<Uri>;
private readonly targetWidgetScriptsFolder: Uri;
private readonly resourcesMappedToExtensionFolder = new Map<string, Promise<Uri>>();
private readonly uriConversionPromises = new Map<string, Deferred<Uri>>();
private requestUriEmitter = new EventEmitter<Uri>();
constructor(private readonly isWebExtension: boolean, private readonly converter: (input: Uri) => Promise<Uri>) {}

/**
* 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<Uri> {
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<Uri>();
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<Uri>());
// 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<Uri> {
// 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);
}
}
9 changes: 1 addition & 8 deletions src/kernels/ipywidgets/serviceRegistry.node.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
}
9 changes: 1 addition & 8 deletions src/kernels/ipywidgets/serviceRegistry.web.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
}
15 changes: 0 additions & 15 deletions src/kernels/ipywidgets/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -102,15 +96,6 @@ export interface ILocalResourceUriConverter {
* ```
*/
asWebviewUri(localResource: Uri): Promise<Uri>;
/**
* 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<Uri>;
/**
* This is the response to the requestUri event
*/
resolveUri(request: Uri, result: Uri): void;
}

export const INbExtensionsPathProvider = Symbol('INbExtensionsPathProvider');
Expand Down
3 changes: 0 additions & 3 deletions src/notebooks/controllers/controllerRegistration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
) {
Expand Down Expand Up @@ -130,7 +128,6 @@ export class ControllerRegistration implements IControllerRegistration {
this.appShell,
this.browser,
this.extensionChecker,
this.resourceConverter,
this.serviceContainer
);
controller.onDidDispose(
Expand Down
4 changes: 1 addition & 3 deletions src/notebooks/controllers/vscodeNotebookController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down

0 comments on commit 47b7c29

Please sign in to comment.