From 43318c6f914d46d93646b4b0c888260347cec11a Mon Sep 17 00:00:00 2001 From: Max Sherman Date: Fri, 7 Oct 2016 16:33:31 -0700 Subject: [PATCH] iwdp debugger #4 -- implement file cache for managing files served from iwdp Summary: This provides an abstraction for translating between URLs and files on disk. URLs are provided by the IWDP server, and map to js bundles or source maps. Reviewed By: jgebhardt Differential Revision: D3989142 fbshipit-source-id: 6c64090a32f4866c5e2e66eca450793a7d3bb7e6 --- .../lib/DebuggerConnection.js | 65 +++------ .../lib/FileCache.js | 123 ++++++++++++++++++ 2 files changed, 143 insertions(+), 45 deletions(-) create mode 100644 pkg/nuclide-debugger-iwdp-rpc/lib/FileCache.js diff --git a/pkg/nuclide-debugger-iwdp-rpc/lib/DebuggerConnection.js b/pkg/nuclide-debugger-iwdp-rpc/lib/DebuggerConnection.js index 0981203ed5..851987f4e8 100644 --- a/pkg/nuclide-debugger-iwdp-rpc/lib/DebuggerConnection.js +++ b/pkg/nuclide-debugger-iwdp-rpc/lib/DebuggerConnection.js @@ -11,11 +11,10 @@ import UniversalDisposable from '../../commons-node/UniversalDisposable'; import WS from 'ws'; -import xfetch from '../../commons-node/xfetch'; -import fsPromise from '../../commons-node/fsPromise'; import {Observable} from 'rxjs'; import {createWebSocketListener} from './createWebSocketListener'; import {logger} from './logger'; +import {FileCache} from './FileCache'; import type {IosDeviceInfo} from './types'; @@ -24,11 +23,10 @@ const {log} = logger; export class DebuggerConnection { _webSocket: WS; _disposables: UniversalDisposable; - // TODO implement file cache to manage this. - _package: ?string; + _fileCache: FileCache; constructor(iosDeviceInfo: IosDeviceInfo, sendMessageToClient: (message: string) => void) { - this._package = null; + this._fileCache = new FileCache(); const {webSocketDebuggerUrl} = iosDeviceInfo; const webSocket = new WS(webSocketDebuggerUrl); this._webSocket = webSocket; @@ -37,6 +35,7 @@ export class DebuggerConnection { this._disposables = new UniversalDisposable( translatedMessages.subscribe(sendMessageToClient), () => webSocket.close(), + this._fileCache, ); log(`DebuggerConnection created with device info: ${JSON.stringify(iosDeviceInfo)}`); } @@ -46,53 +45,29 @@ export class DebuggerConnection { } _translateMessagesForClient(socketMessages: Observable): Observable { - return socketMessages.mergeMap(message => { - const obj = JSON.parse(message); - if (obj.method === 'Debugger.scriptParsed') { - const {params} = obj; - if (params == null) { + return socketMessages + .map(JSON.parse) + .mergeMap((message: {method: string}) => { + if (message.method === 'Debugger.scriptParsed') { + return Observable.fromPromise(this._fileCache.handleScriptParsed(message)); + } else { return Observable.of(message); } - const {url} = params; - if (url == null || !url.startsWith('http:')) { - return Observable.of(message); - } - // The file is being served by the webserver hosted by the target, so we should download it. - // TODO Move this logic to a File cache. - return Observable.fromPromise((async () => { - log(`Got url: ${url}`); - this._package = url; - const response = await xfetch(url, {}); - const text = await response.text(); - const tempPath = await fsPromise.tempfile({prefix: 'jsfile', suffix: '.js'}); - await fsPromise.writeFile(tempPath, text); - obj.params.url = `file://${tempPath}`; - - // Also source maps - const SOURCE_MAP_REGEX = /\/\/# sourceMappingURL=(.+)$/; - const matches = SOURCE_MAP_REGEX.exec(text); - const sourceMapUrl = `http://localhost:8081${matches[1]}`; - - const response2 = await xfetch(sourceMapUrl, {}); - const text2 = await response2.text(); - const base64Text = new Buffer(text2).toString('base64'); - obj.params.sourceMapURL = `data:application/json;base64,${base64Text}`; - const newMessage = JSON.stringify(obj); - log(`Sending: ${newMessage.substring(0, 5000)}`); - return newMessage; - })()); - } - return Observable.of(message); - }); + }) + .map(obj => { + const message = JSON.stringify(obj); + log(`Sending to client: ${message.substring(0, 5000)}`); + return message; + }); } _translateMessageForServer(message: string): string { const obj = JSON.parse(message); if (obj.method === 'Debugger.setBreakpointByUrl') { - obj.params.url = this._package; - const newMessage = JSON.stringify(obj); - log(`Sending message to proxy: ${newMessage}`); - return newMessage; + const updatedObj = this._fileCache.handleSetBreakpointByUrl(obj); + const updatedMessage = JSON.stringify(updatedObj); + log(`Sending message to proxy: ${updatedMessage}`); + return updatedMessage; } else { return message; } diff --git a/pkg/nuclide-debugger-iwdp-rpc/lib/FileCache.js b/pkg/nuclide-debugger-iwdp-rpc/lib/FileCache.js new file mode 100644 index 0000000000..0bcf5325fb --- /dev/null +++ b/pkg/nuclide-debugger-iwdp-rpc/lib/FileCache.js @@ -0,0 +1,123 @@ +'use babel'; +/* @flow */ + +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +import UniversalDisposable from '../../commons-node/UniversalDisposable'; +import xfetch from '../../commons-node/xfetch'; +import fsPromise from '../../commons-node/fsPromise'; +import nuclideUri from '../../commons-node/nuclideUri'; +import {logger} from './logger'; + +const {log} = logger; + +type FileData = { + filePath: string, // Path to file on disk. + url: string, // Url bundle is served from. + sourceMapUrl?: string, // Url that chrome devtools understands and can decode to get source maps. +}; + +type Url = string; + +const SOURCE_MAP_REGEX = /\/\/# sourceMappingURL=(.+)$/; +const SOURCE_MAP_PREFIX = 'data:application/json;base64,'; + +export class FileCache { + _disposables: UniversalDisposable; + _filePathToFileData: Map; + _urlToFileData: Map; + + constructor() { + this._filePathToFileData = new Map(); + this._urlToFileData = new Map(); + this._disposables = new UniversalDisposable( + () => this._filePathToFileData.clear(), + () => this._urlToFileData.clear(), + ); + } + + async handleScriptParsed(obj: Object): Promise { + const {params} = obj; + if (params == null) { + return obj; + } + const {url: urlString} = params; + if (urlString == null || urlString === '') { + return obj; + } + const url = new URL(urlString); + if (url.protocol !== 'http:') { + return obj; + } + const fileData = this._urlToFileData.get(urlString); + if (fileData != null) { + updateMessageObjWithFileData(obj, fileData); + return obj; + } + const newFileData = await createFileData(url); + this._urlToFileData.set(newFileData.url, newFileData); + this._filePathToFileData.set(newFileData.filePath, newFileData); + updateMessageObjWithFileData(obj, newFileData); + return obj; + } + + handleSetBreakpointByUrl(obj: Object): Object { + const filePath = obj.params.url; + const fileData = this._filePathToFileData.get(filePath); + if (fileData == null) { + return obj; + } + obj.params.url = fileData.url; + return obj; + } + + async dispose(): Promise { + this._disposables.dispose(); + } +} + +async function createFileData(url: URL): Promise { + // Handle the bundle file. + log(`FileCache got url: ${url.toString()}`); + const fileResponse = await xfetch(url.toString(), {}); + const basename = nuclideUri.basename(url.pathname); + const [fileText, filePath] = await Promise.all([ + fileResponse.text(), + fsPromise.tempfile({prefix: basename, suffix: '.js'}), + ]); + await fsPromise.writeFile(filePath, fileText); + const fileSystemUrl = `file://${filePath}`; + + const matches = SOURCE_MAP_REGEX.exec(fileText); + if (matches == null) { + return { + filePath: fileSystemUrl, + url: url.toString(), + }; + } + + // Handle source maps for the bundle. + const sourceMapUrl = `${url.origin}${matches[1]}`; + const sourceMapResponse = await xfetch(sourceMapUrl, {}); + const sourceMap = await sourceMapResponse.text(); + const base64SourceMap = new Buffer(sourceMap).toString('base64'); + return { + filePath: fileSystemUrl, + url: url.toString(), + sourceMapUrl: `${SOURCE_MAP_PREFIX}${base64SourceMap}`, + }; +} + +function updateMessageObjWithFileData( + obj: {params: {url: string, sourceMapURL?: string}}, + fileData: FileData, +): void { + obj.params.url = fileData.filePath; + obj.params.sourceMapURL = fileData.sourceMapUrl; +}