From ee8c045bf5c5243cc8ed8118fc993bb0d96a44aa Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 2 May 2022 21:55:12 +0200 Subject: [PATCH] add caching --- .../node/remoteExtensionHostAgentServer.ts | 5 +- src/vs/server/node/webClientServer.ts | 53 +++++++++++-------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 7532f811127..224d368bc38 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -35,7 +35,7 @@ import { ManagementConnection } from 'vs/server/node/remoteExtensionManagement'; import { determineServerConnectionToken, requestHasValidConnectionToken as httpRequestHasValidConnectionToken, ServerConnectionToken, ServerConnectionTokenParseError, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken'; import { IServerEnvironmentService, ServerParsedArgs } from 'vs/server/node/serverEnvironmentService'; import { setupServerServices, SocketServer } from 'vs/server/node/serverServices'; -import { serveError, serveFile, WebClientServer } from 'vs/server/node/webClientServer'; +import { CacheControl, serveError, serveFile, WebClientServer } from 'vs/server/node/webClientServer'; const SHUTDOWN_TIMEOUT = 5 * 60 * 1000; @@ -151,8 +151,7 @@ export class RemoteExtensionHostAgentServer extends Disposable implements IServe if (requestOrigin && this._webEndpointOriginChecker.matches(requestOrigin)) { responseHeaders['Access-Control-Allow-Origin'] = requestOrigin; } - - return serveFile(this._logService, req, res, filePath, responseHeaders); + return serveFile(filePath, CacheControl.ETAG, this._logService, req, res, responseHeaders); } // workbench web UI diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index 12a8dc53633..fef10e3bb7d 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; +import { promises as fsp, createReadStream } from 'fs'; import * as path from 'path'; import * as http from 'http'; import * as url from 'url'; -import * as util from 'util'; import * as cookie from 'cookie'; import * as crypto from 'crypto'; import { isEqualOrParent } from 'vs/base/common/extpath'; @@ -27,6 +26,7 @@ import { URI } from 'vs/base/common/uri'; import { streamToBuffer } from 'vs/base/common/buffer'; import { IProductConfiguration } from 'vs/base/common/product'; import { isString } from 'vs/base/common/types'; +import { CharCode } from 'vs/base/common/charCode'; const textMimeType = { '.html': 'text/html', @@ -44,32 +44,44 @@ export async function serveError(req: http.IncomingMessage, res: http.ServerResp res.end(errorMessage); } +export const enum CacheControl { + NO_CACHING, ETAG, NO_EXPIRY +} + /** * Serve a file at a given path or 404 if the file is missing. */ -export async function serveFile(logService: ILogService, req: http.IncomingMessage, res: http.ServerResponse, filePath: string, responseHeaders: Record = Object.create(null)): Promise { +export async function serveFile(filePath: string, cacheControl: CacheControl, logService: ILogService, req: http.IncomingMessage, res: http.ServerResponse, responseHeaders: Record): Promise { try { - const stat = await util.promisify(fs.stat)(filePath); + const stat = await fsp.stat(filePath); // throws an error if file doesn't exist + if (cacheControl === CacheControl.ETAG) { - // Check if file modified since - const etag = `W/"${[stat.ino, stat.size, stat.mtime.getTime()].join('-')}"`; // weak validator (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) - if (req.headers['if-none-match'] === etag) { - res.writeHead(304); - return res.end(); + // Check if file modified since + const etag = `W/"${[stat.ino, stat.size, stat.mtime.getTime()].join('-')}"`; // weak validator (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) + if (req.headers['if-none-match'] === etag) { + res.writeHead(304); + return res.end(); + } + + responseHeaders['Etag'] = etag; + } else if (cacheControl === CacheControl.NO_EXPIRY) { + responseHeaders['Cache-Control'] = 'public, max-age=31536000'; + } else if (cacheControl === CacheControl.NO_CACHING) { + responseHeaders['Cache-Control'] = 'no-store'; } - // Headers responseHeaders['Content-Type'] = textMimeType[extname(filePath)] || getMediaMime(filePath) || 'text/plain'; - responseHeaders['Etag'] = etag; res.writeHead(200, responseHeaders); // Data - fs.createReadStream(filePath).pipe(res); + createReadStream(filePath).pipe(res); } catch (error) { if (error.code !== 'ENOENT') { logService.error(error); console.error(error.toString()); + } else { + console.error(`File not found: ${filePath}`); } res.writeHead(404, { 'Content-Type': 'text/plain' }); @@ -78,14 +90,14 @@ export async function serveFile(logService: ILogService, req: http.IncomingMessa } const APP_ROOT = dirname(FileAccess.asFileUri('', require).fsPath); -const CHARCODE_SLASH = '/'.charCodeAt(0); +const CHARCODE_SLASH = CharCode.Slash; export class WebClientServer { private readonly _webExtensionResourceUrlTemplate: URI | undefined; - private readonly _staticRoute; - private readonly _callbackRoute; + private readonly _staticRoute: string; + private readonly _callbackRoute: string; constructor( private readonly _connectionToken: ServerConnectionToken, @@ -132,7 +144,6 @@ export class WebClientServer { return serveError(req, res, 500, 'Internal Server Error.'); } } - /** * Handle HTTP requests for /static/* */ @@ -148,7 +159,7 @@ export class WebClientServer { return serveError(req, res, 400, `Bad request.`); } - return serveFile(this._logService, req, res, filePath, headers); + return serveFile(filePath, this._environmentService.isBuilt ? CacheControl.NO_EXPIRY : CacheControl.NO_CACHING, this._logService, req, res, headers); } private _getResourceURLTemplateAuthority(uri: URI): string | undefined { @@ -312,13 +323,11 @@ export class WebClientServer { WORKBENCH_AUTH_SESSION: authSessionInfo ? asJSON(authSessionInfo) : '', WORKBENCH_WEB_BASE_URL: this._staticRoute, }; - //if (!this._environmentService.isBuilt) { - //values.WORKBENCH_BUILTIN_EXTENSIONS = //get built in extensions - //} + let data; try { - const workbenchTemplate = (await util.promisify(fs.readFile)(filePath)).toString(); + const workbenchTemplate = (await fsp.readFile(filePath)).toString(); data = workbenchTemplate.replace(/\{\{([^}]+)\}\}/g, (_, key) => values[key] ?? 'undefined'); } catch (e) { res.writeHead(404, { 'Content-Type': 'text/plain' }); @@ -385,7 +394,7 @@ export class WebClientServer { */ private async _handleCallback(res: http.ServerResponse): Promise { const filePath = FileAccess.asFileUri('vs/code/browser/workbench/callback.html', require).fsPath; - const data = (await util.promisify(fs.readFile)(filePath)).toString(); + const data = (await fsp.readFile(filePath)).toString(); const cspDirectives = [ 'default-src \'self\';', 'img-src \'self\' https: data: blob:;',