From 570ebe9981ee73b550be62db14aaaa2370834c98 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Wed, 12 Aug 2020 08:39:15 +0000 Subject: [PATCH] fix #8358: stream webview resources to avoid blocking a web socket Signed-off-by: Anton Kosyakov --- CHANGELOG.md | 3 ++ .../browser/plugin-ext-frontend-module.ts | 4 -- .../src/main/browser/webview/webview.ts | 20 ++++++--- .../src/main/common/webview-protocol.ts | 21 --------- .../main/node/plugin-ext-backend-module.ts | 9 ---- .../main/node/webview-resource-loader-impl.ts | 43 ------------------- 6 files changed, 16 insertions(+), 84 deletions(-) delete mode 100644 packages/plugin-ext/src/main/node/webview-resource-loader-impl.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a39cbc6372ae9..ad5d637cdc00c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ - On the frontend `EnvVariableServer` should be used instead to access the current user home and available drives. - [[userstorage]](#1.5.0_usestorage_as_fs_provider) `UserStorageService` was replaced by the user data fs provider [#7908](https://github.com/eclipse-theia/theia/pull/7908) + +- [[webview]](#1.5.0_webview_resource_streaming) webview resources are streamed instead of loading one by one the entire content and blocking the web socket [#8359](https://github.com/eclipse-theia/theia/pull/8359) + - Consequently, `WebviewResourceLoader` is removed. One should change `DiskFileSystemProvider` to customize resource loading instead. - [[user-storage]](#1.5.0_root_user_storage_uri) settings URI must start with `/user` root to satisfy expectations of `FileService` []() - If you implement a custom user storage make sure to check old relative locations, otherwise it can cause user data loss. diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index 5f141786f9ba5..da88ad2daaf0a 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -60,7 +60,6 @@ import { WebviewWidget } from './webview/webview'; import { WebviewEnvironment } from './webview/webview-environment'; import { WebviewThemeDataProvider } from './webview/webview-theme-data-provider'; import { bindWebviewPreferences } from './webview/webview-preferences'; -import { WebviewResourceLoader, WebviewResourceLoaderPath } from '../common/webview-protocol'; import { WebviewResourceCache } from './webview/webview-resource-cache'; import { PluginIconThemeService, PluginIconThemeFactory, PluginIconThemeDefinition, PluginIconTheme } from './plugin-icon-theme-service'; import { PluginTreeViewNodeLabelProvider } from './view/plugin-tree-view-node-label-provider'; @@ -152,9 +151,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bindWebviewPreferences(bind); bind(WebviewEnvironment).toSelf().inSingletonScope(); bind(WebviewThemeDataProvider).toSelf().inSingletonScope(); - bind(WebviewResourceLoader).toDynamicValue(ctx => - WebSocketConnectionProvider.createProxy(ctx.container, WebviewResourceLoaderPath) - ).inSingletonScope(); bind(WebviewResourceCache).toSelf().inSingletonScope(); bind(WebviewWidget).toSelf(); bind(WebviewWidgetFactory).toDynamicValue(ctx => new WebviewWidgetFactory(ctx.container)).inSingletonScope(); diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index ecfba6c3a4baa..8941034d0a394 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -42,9 +42,11 @@ import { WebviewThemeDataProvider } from './webview-theme-data-provider'; import { ExternalUriService } from '@theia/core/lib/browser/external-uri-service'; import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; import { WebviewPreferences } from './webview-preferences'; -import { WebviewResourceLoader } from '../../common/webview-protocol'; import { WebviewResourceCache } from './webview-resource-cache'; import { Endpoint } from '@theia/core/lib/browser/endpoint'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { FileOperationError, FileOperationResult } from '@theia/filesystem/lib/common/files'; +import { BinaryBufferReadableStream } from '@theia/core/lib/common/buffer'; // Style from core const TRANSPARENT_OVERLAY_STYLE = 'theia-transparent-overlay'; @@ -130,8 +132,8 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget { @inject(WebviewPreferences) protected readonly preferences: WebviewPreferences; - @inject(WebviewResourceLoader) - protected readonly resourceLoader: WebviewResourceLoader; + @inject(FileService) + protected readonly fileService: FileService; @inject(WebviewResourceCache) protected readonly resourceCache: WebviewResourceCache; @@ -432,11 +434,15 @@ export class WebviewWidget extends BaseWidget implements StatefulWidget { continue; } let cached = await this.resourceCache.match(cacheUrl); - const response = await this.resourceLoader.load({ uri: normalizedUri.toString(), eTag: cached && cached.eTag }); - if (response) { - const { buffer, eTag } = response; - cached = { body: () => new Uint8Array(buffer), eTag: eTag }; + try { + const result = await this.fileService.readFileStream(normalizedUri, { etag: cached?.eTag }); + const { buffer } = await BinaryBufferReadableStream.toBuffer(result.value); + cached = { body: () => buffer, eTag: result.etag }; this.resourceCache.put(cacheUrl, cached); + } catch (e) { + if (!(e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_MODIFIED_SINCE)) { + throw e; + } } if (cached) { const data = await cached.body(); diff --git a/packages/plugin-ext/src/main/common/webview-protocol.ts b/packages/plugin-ext/src/main/common/webview-protocol.ts index 51c62f9a8df76..a9ebf4bc4e75b 100644 --- a/packages/plugin-ext/src/main/common/webview-protocol.ts +++ b/packages/plugin-ext/src/main/common/webview-protocol.ts @@ -26,24 +26,3 @@ export namespace WebviewExternalEndpoint { export const pattern = 'THEIA_WEBVIEW_EXTERNAL_ENDPOINT'; export const defaultPattern = '{{uuid}}.webview.{{hostname}}'; } - -export interface LoadWebviewResourceParams { - uri: string - eTag?: string -} - -export interface LoadWebviewResourceResult { - buffer: number[] - eTag: string -} - -export const WebviewResourceLoader = Symbol('WebviewResourceLoader'); -export interface WebviewResourceLoader { - /** - * Loads initial webview resource data. - * Returns `undefined` if a resource has not beed modified. - * Throws if a resource cannot be loaded. - */ - load(params: LoadWebviewResourceParams): Promise -} -export const WebviewResourceLoaderPath = '/services/webview-resource-loader'; diff --git a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts index 418ac7e209fce..440e5f138f032 100644 --- a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts +++ b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts @@ -34,19 +34,10 @@ import { PluginPathsService, pluginPathsServicePath } from '../common/plugin-pat import { PluginPathsServiceImpl } from './paths/plugin-paths-service'; import { PluginServerHandler } from './plugin-server-handler'; import { PluginCliContribution } from './plugin-cli-contribution'; -import { WebviewResourceLoaderImpl } from './webview-resource-loader-impl'; -import { WebviewResourceLoaderPath } from '../common/webview-protocol'; import { PluginTheiaEnvironment } from '../common/plugin-theia-environment'; import { PluginTheiaDeployerParticipant } from './plugin-theia-deployer-participant'; export function bindMainBackend(bind: interfaces.Bind): void { - bind(WebviewResourceLoaderImpl).toSelf().inSingletonScope(); - bind(ConnectionHandler).toDynamicValue(ctx => - new JsonRpcConnectionHandler(WebviewResourceLoaderPath, () => - ctx.container.get(WebviewResourceLoaderImpl) - ) - ).inSingletonScope(); - bind(PluginApiContribution).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(PluginApiContribution); diff --git a/packages/plugin-ext/src/main/node/webview-resource-loader-impl.ts b/packages/plugin-ext/src/main/node/webview-resource-loader-impl.ts deleted file mode 100644 index 16d769a327cde..0000000000000 --- a/packages/plugin-ext/src/main/node/webview-resource-loader-impl.ts +++ /dev/null @@ -1,43 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2019 TypeFox and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import * as fs from 'fs-extra'; -import * as crypto from 'crypto'; -import { injectable } from 'inversify'; -import { WebviewResourceLoader, LoadWebviewResourceParams, LoadWebviewResourceResult } from '../common/webview-protocol'; -import { FileUri } from '@theia/core/lib/node/file-uri'; - -@injectable() -export class WebviewResourceLoaderImpl implements WebviewResourceLoader { - - async load(params: LoadWebviewResourceParams): Promise { - const fsPath = FileUri.fsPath(params.uri); - const stat = await fs.stat(fsPath); - const eTag = this.compileETag(fsPath, stat); - if ('eTag' in params && params.eTag === eTag) { - return undefined; - } - const buffer = await fs.readFile(FileUri.fsPath(params.uri)); - return { buffer: buffer.toJSON().data, eTag }; - } - - protected compileETag(fsPath: string, stat: fs.Stats): string { - return crypto.createHash('md5') - .update(fsPath + stat.mtime.getTime() + stat.size, 'utf8') - .digest('base64'); - } - -}