Skip to content
This repository has been archived by the owner on Dec 13, 2018. It is now read-only.

Commit

Permalink
iwdp debugger #4 -- implement file cache for managing files served fr…
Browse files Browse the repository at this point in the history
…om 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
  • Loading branch information
johnislarry authored and Facebook Github Bot committed Oct 7, 2016
1 parent f39d82f commit 43318c6
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 45 deletions.
65 changes: 20 additions & 45 deletions pkg/nuclide-debugger-iwdp-rpc/lib/DebuggerConnection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
Expand All @@ -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)}`);
}
Expand All @@ -46,53 +45,29 @@ export class DebuggerConnection {
}

_translateMessagesForClient(socketMessages: Observable<string>): Observable<string> {
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;
}
Expand Down
123 changes: 123 additions & 0 deletions pkg/nuclide-debugger-iwdp-rpc/lib/FileCache.js
Original file line number Diff line number Diff line change
@@ -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<Url, FileData>;
_urlToFileData: Map<Url, FileData>;

constructor() {
this._filePathToFileData = new Map();
this._urlToFileData = new Map();
this._disposables = new UniversalDisposable(
() => this._filePathToFileData.clear(),
() => this._urlToFileData.clear(),
);
}

async handleScriptParsed(obj: Object): Promise<Object> {
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<void> {
this._disposables.dispose();
}
}

async function createFileData(url: URL): Promise<FileData> {
// 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;
}

0 comments on commit 43318c6

Please sign in to comment.