Reimplement debug adapter based on node2 adapter

This commit is contained in:
Vladimir Kotikov 2017-01-31 16:14:23 +03:00
Родитель dec006c021
Коммит e45838cbf8
12 изменённых файлов: 732 добавлений и 707 удалений

4
SampleApplication/.vscode/launch.json поставляемый
Просмотреть файл

@ -7,7 +7,6 @@
"type": "reactnative",
"request": "launch",
"platform": "android",
"internalDebuggerPort": 9090,
"sourceMaps": true,
"outDir": "${workspaceRoot}/.vscode/.react"
},
@ -18,9 +17,8 @@
"request": "launch",
"platform": "ios",
"target": "iPhone 5s",
"internalDebuggerPort": 9090,
"sourceMaps": true,
"outDir": "${workspaceRoot}/.vscode/.react"
}
]
}
}

Просмотреть файл

@ -1,243 +1,232 @@
{
"name": "vscode-react-native",
"displayName": "React Native Tools",
"version": "0.2.5",
"private": true,
"publisher": "vsmobile",
"icon": "images/icon.svg",
"galleryBanner": {
"color": "#3B3738",
"theme": "dark"
},
"description": "Code-hinting, debugging and integrated commands for React Native",
"bugs": "https://github.com/Microsoft/vscode-react-native/issues",
"license": "SEE LICENSE IN LICENSE.txt",
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/vscode-react-native"
},
"engines": {
"vscode": "^0.10.1"
},
"categories": [
"Debuggers",
"Other"
"name": "vscode-react-native",
"displayName": "React Native Tools",
"version": "0.2.5",
"private": true,
"publisher": "vsmobile",
"icon": "images/icon.svg",
"galleryBanner": {
"color": "#3B3738",
"theme": "dark"
},
"description": "Code-hinting, debugging and integrated commands for React Native",
"bugs": "https://github.com/Microsoft/vscode-react-native/issues",
"license": "SEE LICENSE IN LICENSE.txt",
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/vscode-react-native"
},
"engines": {
"vscode": "^0.10.1"
},
"categories": [
"Debuggers",
"Other"
],
"activationEvents": [
"*"
],
"main": "./out/extension/rn-extension",
"contributes": {
"commands": [
{
"command": "reactNative.runAndroid",
"title": "React Native: Run Android"
},
{
"command": "reactNative.runIos",
"title": "React Native: Run iOS"
},
{
"command": "reactNative.startPackager",
"title": "React Native: Start Packager"
},
{
"command": "reactNative.startExponentPackager",
"title": "React Native: Start Exponent Packager"
},
{
"command": "reactNative.stopPackager",
"title": "React Native: Stop Packager"
},
{
"command": "reactNative.restartPackager",
"title": "React Native: Restart Packager"
},
{
"command": "reactNative.publishToExpHost",
"title": "React Native: Publish to Exponent"
}
],
"activationEvents": [
"*"
],
"main": "./out/extension/rn-extension",
"contributes": {
"commands": [
{
"command": "reactNative.runAndroid",
"title": "React Native: Run Android"
},
{
"command": "reactNative.runIos",
"title": "React Native: Run iOS"
},
{
"command": "reactNative.startPackager",
"title": "React Native: Start Packager"
},
{
"command": "reactNative.startExponentPackager",
"title": "React Native: Start Exponent Packager"
},
{
"command": "reactNative.stopPackager",
"title": "React Native: Stop Packager"
},
{
"command": "reactNative.restartPackager",
"title": "React Native: Restart Packager"
},
{
"command": "reactNative.publishToExpHost",
"title": "React Native: Publish to Exponent"
}
"debuggers": [
{
"type": "reactnative",
"label": "React Native",
"program": "./out/debugger/reactNativeDebugEntryPoint.js",
"runtime": "node",
"enableBreakpointsFor": {
"languageIds": [
"javascript",
"typescript",
"javascriptreact",
"typescriptreact"
]
},
"initialConfigurations": [
{
"name": "Debug Android",
"program": "${workspaceRoot}/.vscode/launchReactNative.js",
"type": "reactnative",
"request": "launch",
"platform": "android",
"sourceMaps": true,
"outDir": "${workspaceRoot}/.vscode/.react"
},
{
"name": "Debug iOS",
"program": "${workspaceRoot}/.vscode/launchReactNative.js",
"type": "reactnative",
"request": "launch",
"platform": "ios",
"target": "iPhone 5s",
"sourceMaps": true,
"outDir": "${workspaceRoot}/.vscode/.react"
},
{
"name": "Attach to packager",
"program": "${workspaceRoot}/.vscode/launchReactNative.js",
"type": "reactnative",
"request": "attach",
"sourceMaps": true,
"outDir": "${workspaceRoot}/.vscode/.react"
},
{
"name": "Debug in Exponent",
"program": "${workspaceRoot}/.vscode/launchReactNative.js",
"type": "reactnative",
"request": "launch",
"platform": "exponent",
"sourceMaps": true,
"outDir": "${workspaceRoot}/.vscode/.react"
}
],
"debuggers": [
{
"type": "reactnative",
"label": "React Native",
"program": "./out/debugger/reactNativeDebugEntryPoint.js",
"runtime": "node",
"enableBreakpointsFor": {
"languageIds": [
"javascript",
"typescript",
"javascriptreact",
"typescriptreact"
]
},
"initialConfigurations": [
{
"name": "Debug Android",
"program": "${workspaceRoot}/.vscode/launchReactNative.js",
"type": "reactnative",
"request": "launch",
"platform": "android",
"internalDebuggerPort": 9090,
"sourceMaps": true,
"outDir": "${workspaceRoot}/.vscode/.react"
},
{
"name": "Debug iOS",
"program": "${workspaceRoot}/.vscode/launchReactNative.js",
"type": "reactnative",
"request": "launch",
"platform": "ios",
"target": "iPhone 5s",
"internalDebuggerPort": 9090,
"sourceMaps": true,
"outDir": "${workspaceRoot}/.vscode/.react"
},
{
"name": "Attach to packager",
"program": "${workspaceRoot}/.vscode/launchReactNative.js",
"type": "reactnative",
"request": "attach",
"internalDebuggerPort": 9090,
"sourceMaps": true,
"outDir": "${workspaceRoot}/.vscode/.react"
},
{
"name": "Debug in Exponent",
"program": "${workspaceRoot}/.vscode/launchReactNative.js",
"type": "reactnative",
"request": "launch",
"platform": "exponent",
"internalDebuggerPort": 9090,
"sourceMaps": true,
"outDir": "${workspaceRoot}/.vscode/.react"
}
],
"configurationAttributes": {
"attach": {
"required": [
"program"
],
"properties": {
"program": {
"type": "string",
"description": "The path to launchReactNative.js in the vscode folder"
},
"internalDebuggerPort": {
"type": "number",
"description": "A port to be used to enable automatic reloading of breakpoints when sourcemaps change.",
"default": 9090
},
"sourceMaps": {
"type": "boolean",
"description": "Whether to use JavaScript source maps to map the generated bundled code back to its original sources",
"default": false
},
"outDir": {
"type": "string",
"description": "The location of the generated JavaScript code (the bundle file). Normally this should be \"${workspaceRoot}/.vscode/.react\"",
"default": null
}
}
},
"launch": {
"required": [
"program",
"platform"
],
"properties": {
"platform": {
"type": "string",
"description": "The platform ('ios' or 'android') to target"
},
"program": {
"type": "string",
"description": "The path to launchReactNative.js in the vscode folder"
},
"target": {
"type": "string",
"description": "'simulator', 'device', or the name of the emulator to run on"
},
"internalDebuggerPort": {
"type": "number",
"description": "A port to be used to enable automatic reloading of breakpoints when sourcemaps change.",
"default": 9090
},
"sourceMaps": {
"type": "boolean",
"description": "Whether to use JavaScript source maps to map the generated bundled code back to its original sources",
"default": false
},
"logCatArguments": {
"type": "array",
"description": "Arguments to be used for LogCat (The LogCat output will appear on an Output Channel). It can either be an array such as: [\":S\", \"ReactNative:V\", \"ReactNativeJS:V\"] or a string such as \":S ReactNative:V ReactNativeJS:V\"",
"default": [
"*:S",
"ReactNative:V",
"ReactNativeJS:V"
]
},
"outDir": {
"type": "string",
"description": "The location of the generated JavaScript code (the bundle file). Normally this should be \"${workspaceRoot}/.vscode/.react\"",
"default": null
},
"iosRelativeProjectPath": {
"type": "string",
"description": "Relative path to the ios/ folder, if it is not located on the project root.",
"default": "ios"
},
"variant": {
"type": "string",
"description": "A variant to be passed to react-native run-android, e.g. 'devDebug' to specify --variant=devDebug"
}
}
}
}
"configurationAttributes": {
"attach": {
"required": [
"program"
],
"properties": {
"program": {
"type": "string",
"description": "The path to launchReactNative.js in the vscode folder"
},
"sourceMaps": {
"type": "boolean",
"description": "Whether to use JavaScript source maps to map the generated bundled code back to its original sources",
"default": false
},
"outDir": {
"type": "string",
"description": "The location of the generated JavaScript code (the bundle file). Normally this should be \"${workspaceRoot}/.vscode/.react\"",
"default": null
}
}
]
},
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"compile": "node ./node_modules/vscode/bin/compile -watch -p ./",
"vscode:prepublish": "gulp",
"test": "node ./node_modules/vscode/bin/test",
"postinstall": "node ./node_modules/vscode/bin/install"
},
"dependencies": {
"extract-opts": "2.2.0",
"flatten-source-map": "0.0.2",
"options": "0.0.6",
"q": "1.4.1",
"semver": "5.1.0",
"strip-json-comments": "2.0.1",
"typechecker": "2.0.8",
"ultron": "1.0.2",
"vscode-extension-telemetry": "0.0.5",
"ws": "1.0.1"
},
"devDependencies": {
"del": "^2.2.0",
"gulp": "^3.9.1",
"gulp-mocha": "^2.2.0",
"gulp-preprocess": "^2.0.0",
"gulp-sourcemaps": "^1.6.0",
"gulp-tslint": "^4.3.3",
"gulp-typescript": "^2.12.1",
"gulp-util": "^3.0.7",
"minimist": "^1.2.0",
"mocha": "^2.4.5",
"mocha-teamcity-reporter": "^1.0.0",
"mock-fs": "^3.8.0",
"run-sequence": "^1.1.5",
"should": "^8.3.0",
"sinon": "^1.17.3",
"source-map-support": "^0.4.0",
"through2": "^2.0.1",
"tslint": "^3.6.0",
"typescript": "^1.8.9",
"vsce": "^1.3.0",
"vscode": "^0.11.14"
}
},
"launch": {
"required": [
"program",
"platform"
],
"properties": {
"platform": {
"type": "string",
"description": "The platform ('ios' or 'android') to target"
},
"program": {
"type": "string",
"description": "The path to launchReactNative.js in the vscode folder"
},
"target": {
"type": "string",
"description": "'simulator', 'device', or the name of the emulator to run on"
},
"sourceMaps": {
"type": "boolean",
"description": "Whether to use JavaScript source maps to map the generated bundled code back to its original sources",
"default": false
},
"logCatArguments": {
"type": "array",
"description": "Arguments to be used for LogCat (The LogCat output will appear on an Output Channel). It can either be an array such as: [\":S\", \"ReactNative:V\", \"ReactNativeJS:V\"] or a string such as \":S ReactNative:V ReactNativeJS:V\"",
"default": [
"*:S",
"ReactNative:V",
"ReactNativeJS:V"
]
},
"outDir": {
"type": "string",
"description": "The location of the generated JavaScript code (the bundle file). Normally this should be \"${workspaceRoot}/.vscode/.react\"",
"default": null
},
"iosRelativeProjectPath": {
"type": "string",
"description": "Relative path to the ios/ folder, if it is not located on the project root.",
"default": "ios"
},
"variant": {
"type": "string",
"description": "A variant to be passed to react-native run-android, e.g. 'devDebug' to specify --variant=devDebug"
}
}
}
}
}
]
},
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"compile": "node ./node_modules/vscode/bin/compile -watch -p ./",
"vscode:prepublish": "gulp",
"test": "node ./node_modules/vscode/bin/test",
"postinstall": "node ./node_modules/vscode/bin/install"
},
"dependencies": {
"extract-opts": "2.2.0",
"flatten-source-map": "0.0.2",
"options": "0.0.6",
"q": "1.4.1",
"semver": "5.1.0",
"strip-json-comments": "2.0.1",
"typechecker": "2.0.8",
"ultron": "1.0.2",
"vscode-extension-telemetry": "0.0.5",
"ws": "1.0.1"
},
"devDependencies": {
"del": "^2.2.0",
"gulp": "^3.9.1",
"gulp-mocha": "^2.2.0",
"gulp-preprocess": "^2.0.0",
"gulp-sourcemaps": "^1.6.0",
"gulp-tslint": "^4.3.3",
"gulp-typescript": "^2.12.1",
"gulp-util": "^3.0.7",
"minimist": "^1.2.0",
"mocha": "^2.4.5",
"mocha-teamcity-reporter": "^1.0.0",
"mock-fs": "^3.8.0",
"run-sequence": "^1.1.5",
"should": "^8.3.0",
"sinon": "^1.17.3",
"source-map-support": "^0.4.0",
"through2": "^2.0.1",
"tslint": "^3.6.0",
"typescript": "^1.8.9",
"vsce": "^1.3.0",
"vscode": "^0.11.14"
},
"extensionDependencies": [
"ms-vscode.node-debug2"
]
}

Просмотреть файл

@ -103,10 +103,10 @@ export class StreamLogger implements ILogger {
}
export class NodeDebugAdapterLogger implements ILogger {
private debugSession: NodeDebugSession;
private debugAdapterPackage: typeof VSCodeDebugAdapter;
private debugSession: ChromeDebuggerCorePackage.ChromeDebugSession;
private debugAdapterPackage: typeof VSCodeDebugAdapterPackage;
public constructor(adapterPackage: typeof VSCodeDebugAdapter, debugSession: NodeDebugSession) {
public constructor(adapterPackage: typeof VSCodeDebugAdapterPackage, debugSession: ChromeDebuggerCorePackage.ChromeDebugSession) {
this.debugAdapterPackage = adapterPackage;
this.debugSession = debugSession;
}
@ -148,4 +148,4 @@ export class NodeDebugAdapterLogger implements ILogger {
// Do nothing
return;
}
}
}

Просмотреть файл

@ -5,6 +5,7 @@ import * as vm from "vm";
import * as Q from "q";
import * as path from "path";
import * as WebSocket from "ws";
import { EventEmitter } from "events";
import {ScriptImporter} from "./scriptImporter";
import {Packager} from "../common/packager";
import {ErrorHelper} from "../common/error/errorHelper";
@ -32,8 +33,9 @@ interface DebuggerWorkerSandbox {
postMessageArgument: RNAppMessage; // We use this argument to pass messages to the worker
}
interface RNAppMessage {
export interface RNAppMessage {
method: string;
url?: string;
// These objects have also other properties but that we don't currently use
}
@ -41,7 +43,7 @@ function printDebuggingError(message: string, reason: any) {
Log.logWarning(ErrorHelper.getNestedWarning(reason, `${message}. Debugging won't work: Try reloading the JS from inside the app, or Reconnect the VS Code debugger`));
}
export class SandboxedAppWorker {
export class SandboxedAppWorker implements IDebuggeeWorker {
/** This class will run the RN App logic inside a sandbox. The framework to run the logic is provided by the file
* debuggerWorker.js (designed to run on a WebWorker). We load that file inside a sandbox, and then we use the
* PROCESS_MESSAGE_INSIDE_SANDBOX script to execute the logic to respond to a message inside the sandbox.
@ -51,7 +53,6 @@ export class SandboxedAppWorker {
*/
private packagerPort: number;
private sourcesStoragePath: string;
private debugAdapterPort: number;
private postReplyToApp: (message: any) => void;
private sandbox: DebuggerWorkerSandbox;
@ -65,13 +66,12 @@ export class SandboxedAppWorker {
private static PROCESS_MESSAGE_INSIDE_SANDBOX = "onmessage({ data: postMessageArgument });";
constructor(packagerPort: number, sourcesStoragePath: string, debugAdapterPort: number, postReplyToApp: (message: any) => void, {
constructor(packagerPort: number, sourcesStoragePath: string, postReplyToApp: (message: any) => void, {
nodeFileSystem = new FileSystem(),
scriptImporter = new ScriptImporter(packagerPort, sourcesStoragePath),
} = {}) {
this.packagerPort = packagerPort;
this.sourcesStoragePath = sourcesStoragePath;
this.debugAdapterPort = debugAdapterPort;
this.postReplyToApp = postReplyToApp;
this.scriptToReceiveMessageInSandbox = new vm.Script(SandboxedAppWorker.PROCESS_MESSAGE_INSIDE_SANDBOX);
@ -114,6 +114,8 @@ export class SandboxedAppWorker {
this.runInSandbox(scriptToRunPath, "var onmessage = null; " + fileContents));
}
public stop() { /* no-op */ }
public postMessage(object: RNAppMessage): void {
this.sandbox.postMessageArgument = object;
this.scriptToReceiveMessageInSandbox.runInContext(this.sandboxContext);
@ -171,7 +173,7 @@ export class SandboxedAppWorker {
this.pendingScriptImport = defer.promise;
// The next line converts to any due to the incorrect typing on node.d.ts of vm.runInThisContext
this.scriptImporter.downloadAppScript(url, this.debugAdapterPort)
this.scriptImporter.downloadAppScript(url)
.then(downloadedScript =>
this.runInSandbox(downloadedScript.filepath, downloadedScript.contents))
.done(() => {
@ -191,7 +193,13 @@ export class SandboxedAppWorker {
}
}
export class MultipleLifetimesAppWorker {
export interface IDebuggeeWorker {
start(): Q.Promise<any>;
stop(): void;
postMessage(message: RNAppMessage): void;
}
export class MultipleLifetimesAppWorker extends EventEmitter {
/** This class will create a SandboxedAppWorker that will run the RN App logic, and then create a socket
* and send the RN App messages to the SandboxedAppWorker. The only RN App message that this class handles
* is the prepareJSRuntime, which we reply to the RN App that the sandbox was created successfully.
@ -199,23 +207,25 @@ export class MultipleLifetimesAppWorker {
*/
private packagerPort: number;
private sourcesStoragePath: string;
private debugAdapterPort: number;
private socketToApp: WebSocket;
private singleLifetimeWorker: SandboxedAppWorker;
private singleLifetimeWorker: IDebuggeeWorker;
private sandboxedAppConstructor: (storagePath: string, adapterPort: number, messageFunction: (message: any) => void) => SandboxedAppWorker;
private sandboxedAppConstructor: (storagePath: string, messageFunction: (message: any) => void) => IDebuggeeWorker;
private webSocketConstructor: (url: string) => WebSocket;
private executionLimiter = new ExecutionsLimiter();
constructor(packagerPort: number, sourcesStoragePath: string, debugAdapterPort: number, {
sandboxedAppConstructor = (path: string, port: number, messageFunc: (message: any) => void) =>
new SandboxedAppWorker(packagerPort, path, port, messageFunc),
constructor(packagerPort: number, sourcesStoragePath: string, {
sandboxedAppConstructor = (path: string, messageFunc: (message: any) => void) =>
new SandboxedAppWorker(packagerPort, path, messageFunc),
webSocketConstructor = (url: string) => new WebSocket(url),
}: {
sandboxedAppConstructor?: (path: string, messageFunc: (message: any) => void) => IDebuggeeWorker;
webSocketConstructor?: (url: string) => WebSocket
} = {}) {
super();
this.packagerPort = packagerPort;
this.sourcesStoragePath = sourcesStoragePath;
this.debugAdapterPort = debugAdapterPort;
console.assert(!!this.sourcesStoragePath, "The sourcesStoragePath argument was null or empty");
this.sandboxedAppConstructor = sandboxedAppConstructor;
@ -232,12 +242,30 @@ export class MultipleLifetimesAppWorker {
});
}
public stop() {
if (this.socketToApp) {
this.socketToApp.removeAllListeners();
this.socketToApp.close();
}
if (this.singleLifetimeWorker) {
this.singleLifetimeWorker.stop();
}
}
private startNewWorkerLifetime(): Q.Promise<void> {
this.singleLifetimeWorker = this.sandboxedAppConstructor(this.sourcesStoragePath, this.debugAdapterPort, (message) => {
if (this.singleLifetimeWorker) {
return Q.resolve<void>(void 0);
}
this.singleLifetimeWorker = this.sandboxedAppConstructor(this.sourcesStoragePath, (message) => {
this.sendMessageToApp(message);
});
Log.logInternalMessage(LogLevel.Info, "A new app worker lifetime was created.");
return this.singleLifetimeWorker.start();
return this.singleLifetimeWorker.start()
.then(startedEvent => {
this.emit("connected", startedEvent);
});
}
private createSocketToApp(warnOnFailure: boolean = false): Q.Promise<void> {
@ -299,6 +327,7 @@ export class MultipleLifetimesAppWorker {
this.gotPrepareJSRuntime(object);
} else if (object.method === "$disconnected") {
// We need to shutdown the current app worker, and create a new lifetime
this.singleLifetimeWorker.stop();
this.singleLifetimeWorker = null;
} else if (object.method) {
// All the other messages are handled by the single lifetime worker

Просмотреть файл

@ -0,0 +1,131 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
import * as Q from "q";
import * as path from "path";
import * as child_process from "child_process";
import {ScriptImporter} from "./scriptImporter";
import {FileSystem} from "../common/node/fileSystem";
import { Log } from "../common/log/log";
import { ErrorHelper } from "../common/error/errorHelper";
import { IDebuggeeWorker, RNAppMessage } from "./appWorker";
// tslint:disable-next-line:align
const WORKER_BOOTSTRAP = `
// Hacks to avoid accessing unitialized variables
var onmessage=null, self=global;
// Avoid Node's GLOBAL deprecation warning
global.GLOBAL=global;
process.on("message", function(message){
if (onmessage) onmessage(message);
});
postMessage = function(message){
process.send(message);
};
importScripts = function(scriptUrl){
var scriptCode = require("fs").readFileSync(scriptUrl, "utf8");
require("vm").runInThisContext(scriptCode, { filename: scriptUrl });
};`;
const WORKER_DONE = `// Notify debugger that we're done with loading
// and started listening for IPC messages
postMessage({workerLoaded:true});`;
function printDebuggingError(message: string, reason: any) {
Log.logWarning(ErrorHelper.getNestedWarning(reason, `${message}. Debugging won't work: Try reloading the JS from inside the app, or Reconnect the VS Code debugger`));
}
// TODO: Add more logging
// TODO: Replace all console logging with Log calls
/** This class will run the RN App logic inside a forked Node process. The framework to run the logic is provided by the file
* debuggerWorker.js (designed to run on a WebWorker). We add a couple of tweaks (mostly to polyfill WebWorker API) to that
* file and load it inside of a process.
* On this side we listen to IPC messages and either respond to them or redirect them to packager via MultipleLifetimeAppWorker's
* instance. We also intercept packager's signal to load the bundle's code and mutate the message with path to file we've downloaded
* to let importScripts function take this file.
*/
export class ForkedAppWorker implements IDebuggeeWorker {
private nodeFileSystem: FileSystem = new FileSystem();
private scriptImporter: ScriptImporter;
private debuggeeProcess: child_process.ChildProcess = null;
/** A deferred that we use to make sure that worker has been loaded completely defore start sending IPC messages */
private workerLoaded = Q.defer<void>();
constructor(
private packagerPort: number,
private sourcesStoragePath: string,
private postReplyToApp: (message: any) => void
) {
this.scriptImporter = new ScriptImporter(packagerPort, sourcesStoragePath);
}
public stop() {
if (this.debuggeeProcess) {
console.log(`KILLING ${this.debuggeeProcess.pid} !!!!!!!!!!!!!!!!!!!!!!!!!!`);
this.debuggeeProcess.kill();
this.debuggeeProcess = null;
}
}
public start(): Q.Promise<number> {
let scriptToRunPath = path.resolve(this.sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILENAME);
return this.scriptImporter.downloadDebuggerWorker(this.sourcesStoragePath)
.then(() => this.nodeFileSystem.readFile(scriptToRunPath, "utf8"))
.then((workerContent: string) => {
// Add our customizations to debugger worker to get it working smoothly
// in Node env and polyfill WebWorkers API over Node's IPC.
const modifiedDebuggeeContent = [WORKER_BOOTSTRAP, workerContent, WORKER_DONE].join("\n");
return this.nodeFileSystem.writeFile(scriptToRunPath, modifiedDebuggeeContent);
})
.then(() => {
// Create a random port to use for debugging
const port = Math.round(Math.random() * 40000 + 3000);
// Start forked Node process in debugging mode
this.debuggeeProcess = child_process.fork(scriptToRunPath, [], {
// Note that we set --debug-brk flag to pause the process on the first line - this is
// required for debug adapter to set the breakpoints BEFORE the debuggee has started.
// The adapter will continue execution once it's done with breakpoints.
execArgv: [`--inspect=${port}`, "--debug-brk"],
})
// TODO: shouldn't this be of RNAppMessage?
.on("message", (message: any) => {
// 'workerLoaded' is a special message that indicates that worker is done with loading.
// We need to wait for it before doing any IPC because process.send doesn't seems to care
// about whether the messahe has been received or not and the first messages are often get
// discarded by spawned process
if (message && message.workerLoaded) {
this.workerLoaded.resolve(void 0);
return;
}
this.postReplyToApp(message);
});
console.log(`SPAWNED ${this.debuggeeProcess.pid} !!!!!!!!!!!!!!!!!!!!!!!!!!`);
// Resolve with port debugger server is listening on
// This will be sent to subscribers of MLAppWorker in "connected" event
return port;
});
}
public postMessage(rnMessage: RNAppMessage): void {
// Before sending messages, make sure that the worker is loaded
this.workerLoaded.promise
.then(() => {
if (rnMessage.method !== "executeApplicationScript") return Q.resolve(rnMessage);
// When packager asks worker to load bundle we download that bundle first
// and then set url field to point to that downloaded bundle, so the worker
// will take our modified bundle
return this.scriptImporter.downloadAppScript(rnMessage.url)
.then(downloadedScript => Object.assign({}, rnMessage, { url: downloadedScript.filepath }));
})
.done((message: RNAppMessage) => this.debuggeeProcess.send({ data: message }),
reason => printDebuggingError(`Couldn't import script at <${rnMessage.url}>`, reason));
}
}

Просмотреть файл

@ -1,53 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
import * as fs from "fs";
import * as path from "path";
import {MultipleLifetimesAppWorker} from "./appWorker";
import {ErrorHelper} from "../common/error/errorHelper";
import {InternalErrorCode} from "../common/error/internalErrorCode";
import {ScriptImporter} from "./scriptImporter";
import {TelemetryHelper} from "../common/telemetryHelper";
import {Log} from "../common/log/log";
import {RemoteExtension} from "../common/remoteExtension";
import {EntryPointHandler, ProcessType} from "../common/entryPointHandler";
export class Launcher {
private workspaceRootPath: string;
private projectRootPath: string;
private remoteExtension: RemoteExtension;
constructor(workspaceRootPath: string, projectRootPath: string) {
this.workspaceRootPath = workspaceRootPath;
this.projectRootPath = projectRootPath;
this.remoteExtension = RemoteExtension.atProjectRootPath(this.projectRootPath);
}
public launch(): void {
const debugAdapterPort = parseInt(process.argv[2], 10) || 9090;
// Enable telemetry
new EntryPointHandler(ProcessType.Debugee).runApp("react-native-debug-process", () => this.getAppVersion(),
ErrorHelper.getInternalError(InternalErrorCode.DebuggingFailed), this.projectRootPath, () => {
return TelemetryHelper.generate("launch", (generator) => {
const sourcesStoragePath = path.join(this.workspaceRootPath, ".vscode", ".react");
return this.remoteExtension.getPackagerPort().then(packagerPort => {
let scriptImporter = new ScriptImporter(packagerPort, sourcesStoragePath);
return scriptImporter.downloadDebuggerWorker(sourcesStoragePath).then(() => {
Log.logMessage("Downloaded debuggerWorker.js (Logic to run the React Native app) from the Packager.");
}).then(() => {
generator.step("Starting App Worker");
Log.logMessage("Starting debugger app worker.");
return new MultipleLifetimesAppWorker(packagerPort, sourcesStoragePath, debugAdapterPort).start();
}).then(() =>
Log.logMessage("Debugging session started successfully."));
});
});
}
);
}
private getAppVersion() {
return JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf-8")).version;
}
}

66
src/debugger/nodeDebugAdapter.d.ts поставляемый
Просмотреть файл

@ -4,48 +4,52 @@
// These typings do not reflect the typings as intended to be used
// but rather as they exist in truth, so we can reach into the internals
// and access what we need.
declare module VSCodeDebugAdapter {
declare module VSCodeDebugAdapterPackage {
class DebugSession {
public static run: Function;
public sendEvent(event: VSCodeDebugAdapter.InitializedEvent): void;
public start(input: any, output: any): void;
public launchRequest(response: any, args: any): void;
public attachRequest(response: any, args: any): void;
public disconnectRequest(response: any, args: any): void;
public static run(debugSession: typeof DebugSession): void;
}
class InitializedEvent {
class InitializedEvent extends Event {
constructor();
}
class OutputEvent {
class OutputEvent extends Event {
constructor(message: string, destination?: string);
}
class TerminatedEvent {
class TerminatedEvent extends Event {
constructor();
}
class Event {
public event: string;
public body: any;
}
interface Request {
command: string;
arguments?: any;
}
}
declare class SourceMaps {
constructor(session: NodeDebugSession, generatedCodeDirectory: string, generatedCodeGlobs: string[]);
declare module ChromeDebuggerCorePackage {
abstract class ChromeDebugAdapter {
protected _attachMode: boolean;
protected doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void>;
}
interface IChromeDebugSessionOpts {
logFilePath?: string;
adapter: typeof ChromeDebugAdapter;
extensionName: string;
}
class ChromeDebugSession extends VSCodeDebugAdapterPackage.DebugSession {
protected _debugAdapter: any;
constructor(debuggerLinesAndColumnsStartAt1?: boolean, isServer?: boolean, opts?: IChromeDebugSessionOpts);
public sendEvent(event: VSCodeDebugAdapterPackage.InitializedEvent): void;
protected dispatchRequest(request: { command: string }): void;
}
}
declare class NodeDebugSession extends VSCodeDebugAdapter.DebugSession {
public _sourceMaps: SourceMaps;
declare module Node2DebugAdapterPackage {
class Node2DebugAdapter extends ChromeDebuggerCorePackage.ChromeDebugAdapter {
protected doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void>
}
}
interface ILaunchRequestArgs {
platform: string;
target?: string;
internalDebuggerPort?: any;
iosRelativeProjectPath?: string;
args: string[];
logCatArguments: any;
program: string;
variant?: string;
}
interface IAttachRequestArgs {
internalDebuggerPort?: any;
args: string[];
program: string;
platform: string;
}

530
src/debugger/nodeDebugWrapper.ts Executable file → Normal file
Просмотреть файл

@ -1,276 +1,254 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
import * as Q from "q";
import * as path from "path";
import * as http from "http";
import * as fs from "fs";
import stripJsonComments = require("strip-json-comments");
import {Telemetry} from "../common/telemetry";
import {TelemetryHelper} from "../common/telemetryHelper";
import {RemoteExtension} from "../common/remoteExtension";
import {IOSPlatform} from "./ios/iOSPlatform";
import {PlatformResolver} from "./platformResolver";
import {IRunOptions} from "../common/launchArgs";
import {TargetPlatformHelper} from "../common/targetPlatformHelper";
import {ExtensionTelemetryReporter, ReassignableTelemetryReporter} from "../common/telemetryReporters";
import {NodeDebugAdapterLogger} from "../common/log/loggers";
import {Log} from "../common/log/log";
import {GeneralMobilePlatform} from "../common/generalMobilePlatform";
export class NodeDebugWrapper {
private projectRootPath: string;
private remoteExtension: RemoteExtension;
private telemetryReporter: ReassignableTelemetryReporter;
private appName: string;
private version: string;
private mobilePlatformOptions: IRunOptions;
private vscodeDebugAdapterPackage: typeof VSCodeDebugAdapter;
private nodeDebugSession: typeof NodeDebugSession;
private sourceMapsConstructor: typeof SourceMaps;
private originalLaunchRequest: (response: any, args: any) => void;
public constructor(appName: string, version: string, telemetryReporter: ReassignableTelemetryReporter,
debugAdapter: typeof VSCodeDebugAdapter, debugSession: typeof NodeDebugSession, sourceMaps: typeof SourceMaps) {
this.appName = appName;
this.version = version;
this.telemetryReporter = telemetryReporter;
this.vscodeDebugAdapterPackage = debugAdapter;
this.nodeDebugSession = debugSession;
this.sourceMapsConstructor = sourceMaps;
this.originalLaunchRequest = this.nodeDebugSession.prototype.launchRequest;
}
/**
* Calls customize methods for all requests needed
*/
public customizeNodeAdapterRequests(): void {
this.customizeLaunchRequest();
this.customizeAttachRequest();
this.customizeDisconnectRequest();
}
/**
* Intecept the "launchRequest" instance method of NodeDebugSession to interpret arguments.
* Launch should:
* - Run the packager if needed
* - Compile and run application
* - Prewarm bundle
*/
private customizeLaunchRequest(): void {
const nodeDebugWrapper = this;
this.nodeDebugSession.prototype.launchRequest = function (request: any, args: ILaunchRequestArgs) {
nodeDebugWrapper.requestSetup(this, args);
nodeDebugWrapper.mobilePlatformOptions.target = args.target || "simulator";
nodeDebugWrapper.mobilePlatformOptions.iosRelativeProjectPath = !nodeDebugWrapper.isNullOrUndefined(args.iosRelativeProjectPath) ?
args.iosRelativeProjectPath :
IOSPlatform.DEFAULT_IOS_PROJECT_RELATIVE_PATH;
// We add the parameter if it's defined (adapter crashes otherwise)
if (!nodeDebugWrapper.isNullOrUndefined(args.logCatArguments)) {
nodeDebugWrapper.mobilePlatformOptions.logCatArguments = [nodeDebugWrapper.parseLogCatArguments(args.logCatArguments)];
}
if (!nodeDebugWrapper.isNullOrUndefined(args.variant)) {
nodeDebugWrapper.mobilePlatformOptions.variant = args.variant;
}
return TelemetryHelper.generate("launch", (generator) => {
const resolver = new PlatformResolver();
return nodeDebugWrapper.remoteExtension.getPackagerPort()
.then(packagerPort => {
nodeDebugWrapper.mobilePlatformOptions.packagerPort = packagerPort;
const mobilePlatform = resolver.resolveMobilePlatform(args.platform, nodeDebugWrapper.mobilePlatformOptions);
return Q({})
.then(() => {
generator.step("checkPlatformCompatibility");
TargetPlatformHelper.checkTargetPlatformSupport(nodeDebugWrapper.mobilePlatformOptions.platform);
generator.step("startPackager");
return mobilePlatform.startPackager();
})
.then(() => {
// We've seen that if we don't prewarm the bundle cache, the app fails on the first attempt to connect to the debugger logic
// and the user needs to Reload JS manually. We prewarm it to prevent that issue
generator.step("prewarmBundleCache");
Log.logMessage("Prewarming bundle cache. This may take a while ...");
return mobilePlatform.prewarmBundleCache();
})
.then(() => {
generator.step("mobilePlatform.runApp");
Log.logMessage("Building and running application.");
return mobilePlatform.runApp();
})
.then(() =>
nodeDebugWrapper.attachRequest(this, request, args, mobilePlatform));
}).catch(error =>
nodeDebugWrapper.bailOut(this, error.message));
});
};
}
/**
* Intecept the "attachRequest" instance method of NodeDebugSession to interpret arguments
*/
private customizeAttachRequest(): void {
const nodeDebugWrapper = this;
this.nodeDebugSession.prototype.attachRequest = function (request: any, args: IAttachRequestArgs) {
nodeDebugWrapper.requestSetup(this, args);
nodeDebugWrapper.attachRequest(this, request, args, new GeneralMobilePlatform(nodeDebugWrapper.mobilePlatformOptions));
};
}
/**
* Intecept the "disconnectRequest" instance method of NodeDebugSession to interpret arguments
*/
private customizeDisconnectRequest(): void {
const originalRequest = this.nodeDebugSession.prototype.disconnectRequest;
const nodeDebugWrapper = this;
this.nodeDebugSession.prototype.disconnectRequest = function (response: any, args: any): void {
// First we tell the extension to stop monitoring the logcat, and then we disconnect the debugging session
if (nodeDebugWrapper.mobilePlatformOptions.platform === "android") {
nodeDebugWrapper.remoteExtension.stopMonitoringLogcat()
.catch(reason =>
Log.logError(`WARNING: Couldn't stop monitoring logcat: ${reason.message || reason}\n`))
.finally(() =>
originalRequest.call(this, response, args));
} else {
originalRequest.call(this, response, args);
}
};
}
/**
* Makes the required setup for request customization
* - Enables telemetry
* - Sets up mobilePlatformOptions, remote extension and projectRootPath
* - Starts debug server
* - Create global logger
*/
private requestSetup(debugSession: NodeDebugSession, args: any) {
this.projectRootPath = this.getProjectRoot(args);
this.remoteExtension = RemoteExtension.atProjectRootPath(this.projectRootPath);
this.mobilePlatformOptions = {
projectRoot: this.projectRootPath,
platform: args.platform,
};
// Start to send telemetry
this.telemetryReporter.reassignTo(new ExtensionTelemetryReporter(
this.appName, this.version, Telemetry.APPINSIGHTS_INSTRUMENTATIONKEY, this.projectRootPath));
// Create a server waiting for messages to re-initialize the debug session;
const debugServerListeningPort = this.createReinitializeServer(debugSession, args.internalDebuggerPort, args.outDir);
args.args = [debugServerListeningPort.toString()];
Log.SetGlobalLogger(new NodeDebugAdapterLogger(this.vscodeDebugAdapterPackage, debugSession));
}
/**
* Runs logic needed to attach.
* Attach should:
* - Enable js debugging
*/
private attachRequest(debugSession: NodeDebugSession, request: any, args: any, mobilePlatform: any): Q.Promise<void> {
return TelemetryHelper.generate("attach", (generator) => {
return Q({})
.then(() => {
generator.step("mobilePlatform.enableJSDebuggingMode");
if (mobilePlatform) {
return mobilePlatform.enableJSDebuggingMode();
} else {
Log.logMessage("Debugger ready. Enable remote debugging in app.");
}
}).then(() =>
this.originalLaunchRequest.call(debugSession, request, args))
.catch(error =>
this.bailOut(debugSession, error.message));
});
}
/**
* Creates internal debug server and returns the port that the server is hook up into.
*/
private createReinitializeServer(debugSession: NodeDebugSession, internalDebuggerPort: string, sourcesDir: string): number {
// Create the server
const server = http.createServer((req, res) => {
res.statusCode = 404;
if (req.url === "/refreshBreakpoints") {
res.statusCode = 200;
if (debugSession) {
const sourceMaps = debugSession._sourceMaps;
if (sourceMaps) {
// Flush any cached source maps
// Rather than cleaning internal caches we recreate
// SourceMaps to add downloaded bundle map to cache
const bundlePattern = path.join(sourcesDir, "*.bundle");
const sourceMaps = new this.sourceMapsConstructor(debugSession, sourcesDir, [bundlePattern]);
debugSession._sourceMaps = sourceMaps;
}
// Send an "initialized" event to trigger breakpoints to be re-sent
debugSession.sendEvent(new this.vscodeDebugAdapterPackage.InitializedEvent());
}
}
res.end();
});
// Setup listen port and on error response
const port = parseInt(internalDebuggerPort, 10) || 9090;
server.listen(port);
server.on("error", (err: Error) => {
TelemetryHelper.sendSimpleEvent("reinitializeServerError");
Log.logError("Error in debug adapter server: " + err.toString());
Log.logMessage("Breakpoints may not update. Consider restarting and specifying a different 'internalDebuggerPort' in launch.json");
});
// Return listen port
return port;
}
/**
* Logs error to user and finishes the debugging process.
*/
private bailOut(debugSession: NodeDebugSession, message: string): void {
Log.logError(`Could not debug. ${message}`);
debugSession.sendEvent(new this.vscodeDebugAdapterPackage.TerminatedEvent());
process.exit(1);
}
/**
* Parses log cat arguments to a string
*/
private parseLogCatArguments(userProvidedLogCatArguments: any): string {
return Array.isArray(userProvidedLogCatArguments)
? userProvidedLogCatArguments.join(" ") // If it's an array, we join the arguments
: userProvidedLogCatArguments; // If not, we leave it as-is
}
/**
* Helper method to know if a value is either null or undefined
*/
private isNullOrUndefined(value: any): boolean {
return typeof value === "undefined" || value === null;
}
/**
* Parses settings.json file for workspace root property
*/
private getProjectRoot(args: any): string {
try {
let vsCodeRoot = path.resolve(args.program, "../..");
let settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
let settingsContent = fs.readFileSync(settingsPath, "utf8");
settingsContent = stripJsonComments(settingsContent);
let parsedSettings = JSON.parse(settingsContent);
let projectRootPath = parsedSettings["react-native-tools"].projectRoot;
return path.resolve(vsCodeRoot, projectRootPath);
} catch (e) {
return path.resolve(args.program, "../..");
}
}
}
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
import * as Q from "q";
import * as path from "path";
import * as fs from "fs";
import stripJsonComments = require("strip-json-comments");
import {Telemetry} from "../common/telemetry";
import {TelemetryHelper} from "../common/telemetryHelper";
import {RemoteExtension} from "../common/remoteExtension";
import {IOSPlatform} from "./ios/iOSPlatform";
import {PlatformResolver} from "./platformResolver";
import {IRunOptions} from "../common/launchArgs";
import {TargetPlatformHelper} from "../common/targetPlatformHelper";
import {ExtensionTelemetryReporter, ReassignableTelemetryReporter} from "../common/telemetryReporters";
import {NodeDebugAdapterLogger} from "../common/log/loggers";
import {Log} from "../common/log/log";
import {GeneralMobilePlatform} from "../common/generalMobilePlatform";
import { MultipleLifetimesAppWorker } from "./appWorker";
import { ForkedAppWorker } from "./forkedAppWorker";
export function makeSession(debugSessionClass: typeof ChromeDebuggerCorePackage.ChromeDebugSession,
debugSessionOpts: ChromeDebuggerCorePackage.IChromeDebugSessionOpts,
debugAdapterPackage: typeof VSCodeDebugAdapterPackage,
telemetryReporter: ReassignableTelemetryReporter,
appName: string, version: string): typeof ChromeDebuggerCorePackage.ChromeDebugSession {
return class extends debugSessionClass {
private projectRootPath: string;
private remoteExtension: RemoteExtension;
private mobilePlatformOptions: IRunOptions;
private appWorker: MultipleLifetimesAppWorker = null;
private packagerPort: number;
constructor(debuggerLinesAndColumnsStartAt1?: boolean, isServer?: boolean) {
super(debuggerLinesAndColumnsStartAt1, isServer, debugSessionOpts);
}
// Override ChromeDebugSession's sendEvent to control what we will send to client
public sendEvent(event: VSCodeDebugAdapterPackage.Event): void {
// Do not send "terminated" events signaling about session's restart to client as it would cause it
// to restart adapter's process, while we want to stay alive and don't want to interrupt connection
// to packager. Also in this case we need to reinstantiate the debug adapter, as the current
// implementation of ChromeDebugAdapter doesn't allow us to reattach to another debug target easily.
// As of now it's easier to throw previous instance out and create a new one.
if (event.event === "terminated" && event.body && event.body.restart === true) {
// Casting debugAdapter to any to make this compile, as TS doesn't allow instantiating abstract classes
this._debugAdapter = new (<any>debugSessionOpts.adapter)(debugSessionOpts, this);
return;
}
super.sendEvent(event);
}
protected dispatchRequest(request: VSCodeDebugAdapterPackage.Request): void {
if (request.command === "disconnect")
return this.disconnect(request);
if (request.command === "attach")
return this.attach(request);
if (request.command === "launch")
return this.launch(request);
return super.dispatchRequest(request);
}
private launch(request: VSCodeDebugAdapterPackage.Request): void {
this.requestSetup(request.arguments);
this.mobilePlatformOptions.target = request.arguments.target || "simulator";
this.mobilePlatformOptions.iosRelativeProjectPath = !isNullOrUndefined(request.arguments.iosRelativeProjectPath) ?
request.arguments.iosRelativeProjectPath :
IOSPlatform.DEFAULT_IOS_PROJECT_RELATIVE_PATH;
// We add the parameter if it's defined (adapter crashes otherwise)
if (!isNullOrUndefined(request.arguments.logCatArguments)) {
this.mobilePlatformOptions.logCatArguments = [parseLogCatArguments(request.arguments.logCatArguments)];
}
if (!isNullOrUndefined(request.arguments.variant)) {
this.mobilePlatformOptions.variant = request.arguments.variant;
}
TelemetryHelper.generate("launch", (generator) => {
return this.remoteExtension.getPackagerPort()
.then((packagerPort: number) => {
this.packagerPort = packagerPort;
this.mobilePlatformOptions.packagerPort = packagerPort;
const mobilePlatform = new PlatformResolver()
.resolveMobilePlatform(request.arguments.platform, this.mobilePlatformOptions);
generator.step("checkPlatformCompatibility");
TargetPlatformHelper.checkTargetPlatformSupport(this.mobilePlatformOptions.platform);
generator.step("startPackager");
return mobilePlatform.startPackager()
.then(() => {
// We've seen that if we don't prewarm the bundle cache, the app fails on the first attempt to connect to the debugger logic
// and the user needs to Reload JS manually. We prewarm it to prevent that issue
generator.step("prewarmBundleCache");
Log.logMessage("Prewarming bundle cache. This may take a while ...");
return mobilePlatform.prewarmBundleCache();
})
.then(() => {
generator.step("mobilePlatform.runApp");
Log.logMessage("Building and running application.");
return mobilePlatform.runApp();
})
.then(() => {
return this.attachRequest(request, mobilePlatform);
});
});
});
}
private attach(request: VSCodeDebugAdapterPackage.Request): void {
this.requestSetup(request.arguments);
this.attachRequest(request);
}
private disconnect(request: VSCodeDebugAdapterPackage.Request): void {
// The client is about to disconnect so first we need to stop app worker
this.appWorker.stop();
// Then we tell the extension to stop monitoring the logcat, and then we disconnect the debugging session
if (this.mobilePlatformOptions.platform === "android") {
this.remoteExtension.stopMonitoringLogcat()
.catch(reason => Log.logError(`WARNING: Couldn't stop monitoring logcat: ${reason.message || reason}\n`))
.finally(() => super.dispatchRequest(request));
} else {
super.dispatchRequest(request);
}
}
private requestSetup(args: any) {
this.projectRootPath = getProjectRoot(args);
this.remoteExtension = RemoteExtension.atProjectRootPath(this.projectRootPath);
this.mobilePlatformOptions = {
projectRoot: this.projectRootPath,
platform: args.platform,
};
// Start to send telemetry
telemetryReporter.reassignTo(new ExtensionTelemetryReporter(
appName, version, Telemetry.APPINSIGHTS_INSTRUMENTATIONKEY, this.projectRootPath));
Log.SetGlobalLogger(new NodeDebugAdapterLogger(debugAdapterPackage, this));
}
/**
* Runs logic needed to attach.
* Attach should:
* - Enable js debugging
*/
private attachRequest(request: VSCodeDebugAdapterPackage.Request, mobilePlatform?: GeneralMobilePlatform): Q.Promise<void> {
return TelemetryHelper.generate("attach", (generator) => {
return Q({})
.then(() => {
generator.step("mobilePlatform.enableJSDebuggingMode");
if (mobilePlatform) {
return mobilePlatform.enableJSDebuggingMode();
} else {
Log.logMessage("Debugger ready. Enable remote debugging in app.");
}
})
.then(() => {
Log.logMessage("Starting debugger app worker.");
// TODO: remove dependency on args.program - "program" property is technically
// no more required in launch configuration and could be removed
const workspaceRootPath = path.resolve(path.dirname(request.arguments.program), "..");
const sourcesStoragePath = path.join(workspaceRootPath, ".vscode", ".react");
// If launch is invoked first time, appWorker is undefined, so create it here
this.appWorker = new MultipleLifetimesAppWorker(this.packagerPort, sourcesStoragePath, {
// Inject our custom debuggee worker
sandboxedAppConstructor: (path: string, messageFunc: (message: any) => void) =>
new ForkedAppWorker(this.packagerPort, path, messageFunc),
}
);
this.appWorker.on("connected", (port: number) => {
// Don't mutate original request to avoid side effects
let attachArguments = Object.assign({}, request.arguments, { port, restart: true, request: "attach" });
let attachRequest = Object.assign({}, request, { command: "attach", arguments: attachArguments });
super.dispatchRequest(attachRequest);
});
return this.appWorker.start();
})
.catch(error => this.bailOut(error.message));
});
}
/**
* Logs error to user and finishes the debugging process.
*/
private bailOut(message: string): void {
Log.logError(`Could not debug. ${message}`);
// use public terminateSession from Node2DebugAdapter
// this.terminateSession(message);
// this._session.sendEvent(new TerminatedEvent());
// process.exit(1);
};
};
}
export function makeAdapter(debugAdapterClass: typeof Node2DebugAdapterPackage.Node2DebugAdapter): typeof Node2DebugAdapterPackage.Node2DebugAdapter {
return class extends debugAdapterClass {
public doAttach(port: number, targetUrl?: string, address?: string, timeout?: number): Promise<void> {
// HACK: Overwrite ChromeDebug's _attachMode to let Node2 adapter
// to set up breakpoints on initial pause event
this._attachMode = false;
return super.doAttach(port, targetUrl, address, timeout);
}
};
}
/**
* Parses log cat arguments to a string
*/
function parseLogCatArguments(userProvidedLogCatArguments: any): string {
return Array.isArray(userProvidedLogCatArguments)
? userProvidedLogCatArguments.join(" ") // If it's an array, we join the arguments
: userProvidedLogCatArguments; // If not, we leave it as-is
}
/**
* Helper method to know if a value is either null or undefined
*/
function isNullOrUndefined(value: any): boolean {
return typeof value === "undefined" || value === null;
}
/**
* Parses settings.json file for workspace root property
*/
function getProjectRoot(args: any): string {
try {
let vsCodeRoot = path.resolve(args.program, "../..");
let settingsPath = path.resolve(vsCodeRoot, ".vscode/settings.json");
let settingsContent = fs.readFileSync(settingsPath, "utf8");
settingsContent = stripJsonComments(settingsContent);
let parsedSettings = JSON.parse(settingsContent);
let projectRootPath = parsedSettings["react-native-tools"].projectRoot;
return path.resolve(vsCodeRoot, projectRootPath);
} catch (e) {
return path.resolve(args.program, "../..");
}
}

Просмотреть файл

@ -9,11 +9,11 @@ import {EntryPointHandler, ProcessType} from "../common/entryPointHandler";
import {ErrorHelper} from "../common/error/errorHelper";
import {InternalErrorCode} from "../common/error/internalErrorCode";
import {NullTelemetryReporter, ReassignableTelemetryReporter} from "../common/telemetryReporters";
import {NodeDebugWrapper} from "./nodeDebugWrapper";
import { makeAdapter, makeSession } from "./nodeDebugWrapper";
const version = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf-8")).version;
const telemetryReporter = new ReassignableTelemetryReporter(new NullTelemetryReporter());
const appName = "react-native-debug-adapter";
const extensionName = "react-native-debug-adapter";
function bailOut(reason: string): void {
// Things have gone wrong in initialization: Report the error to telemetry and exit
@ -22,7 +22,7 @@ function bailOut(reason: string): void {
}
// Enable telemetry
new EntryPointHandler(ProcessType.Debugger).runApp(appName, () => version,
new EntryPointHandler(ProcessType.Debugger).runApp(extensionName, () => version,
ErrorHelper.getInternalError(InternalErrorCode.DebuggingFailed), telemetryReporter, () => {
/**
@ -31,14 +31,20 @@ new EntryPointHandler(ProcessType.Debugger).runApp(appName, () => version,
* We look for node debug adapter on client's computer so we can jump of on top of that.
*/
let nodeDebugFolder: string;
let vscodeDebugAdapterPackage: typeof VSCodeDebugAdapter;
let VSCodeDebugAdapter: typeof VSCodeDebugAdapterPackage;
let Node2DebugAdapter: typeof Node2DebugAdapterPackage.Node2DebugAdapter;
let ChromeDebuggerPackage: typeof ChromeDebuggerCorePackage;
// nodeDebugLocation.json is dynamically generated on extension activation.
// If it fails, we must not have been in a react native project
try {
/* tslint:disable:no-var-requires */
nodeDebugFolder = require("./nodeDebugLocation.json").nodeDebugPath;
vscodeDebugAdapterPackage = require(path.join(nodeDebugFolder, "node_modules", "vscode-debugadapter"));
// FIXME: uncomment this before submitting
// nodeDebugFolder = require("./nodeDebugLocation.json").nodeDebugPath;
nodeDebugFolder = "/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/ms-vscode.node-debug2";
VSCodeDebugAdapter = require(path.join(nodeDebugFolder, "node_modules/vscode-debugadapter"));
ChromeDebuggerPackage = require(path.join(nodeDebugFolder, "node_modules/vscode-chrome-debug-core"));
Node2DebugAdapter = require(path.join(nodeDebugFolder, "out/src/nodeDebugAdapter")).NodeDebugAdapter;
/* tslint:enable:no-var-requires */
} catch (e) {
// Nothing we can do here: can't even communicate back because we don't know how to speak debug adapter
@ -46,48 +52,28 @@ new EntryPointHandler(ProcessType.Debugger).runApp(appName, () => version,
}
/**
* We did find node debug adapter. Lets get the debugSession from it.
* And add our customizations to the requests.
* We did find chrome debugger package and node2 debug adapter. Lets create debug
* session and adapter with our customizations.
*/
// Temporarily dummy out the DebugSession.run function so we do not start the debug adapter until we are ready
const originalDebugSessionRun = vscodeDebugAdapterPackage.DebugSession.run;
vscodeDebugAdapterPackage.DebugSession.run = () => { };
let nodeDebug: { NodeDebugSession: typeof NodeDebugSession };
let sourceMaps: { SourceMaps: typeof SourceMaps };
let session: typeof ChromeDebuggerCorePackage.ChromeDebugSession;
let adapter: typeof Node2DebugAdapterPackage.Node2DebugAdapter;
try {
/* tslint:disable:no-var-requires */
nodeDebug = require(path.join(nodeDebugFolder, "out", "node", "nodeDebug"));
sourceMaps = require(path.join(nodeDebugFolder, "out", "node", "sourceMaps"));
/* tslint:enable:no-var-requires */
/* Create customised react-native debug adapter based on Node-debug2 adapter */
adapter = makeAdapter(Node2DebugAdapter);
// Create a debug session class based on ChromeDebugSession
session = makeSession(ChromeDebuggerPackage.ChromeDebugSession,
{ adapter, extensionName }, VSCodeDebugAdapter, telemetryReporter, extensionName, version);
} catch (e) {
// Unable to find nodeDebug, but we can make our own communication channel now
const debugSession = new vscodeDebugAdapterPackage.DebugSession();
// Note: this will not work in the context of debugging the debug adapter and communicating over a socket,
// but in that case we have much better ways to investigate errors.
debugSession.start(process.stdin, process.stdout);
debugSession.sendEvent(new vscodeDebugAdapterPackage.OutputEvent("Unable to start debug adapter: " + e.toString(), "stderr"));
debugSession.sendEvent(new vscodeDebugAdapterPackage.TerminatedEvent());
bailOut("cannotFindNodeDebugAdapter");
}
vscodeDebugAdapterPackage.DebugSession.run = originalDebugSessionRun;
// Customize node adapter requests
try {
let nodeDebugWrapper = new NodeDebugWrapper(appName, version, telemetryReporter,
vscodeDebugAdapterPackage, nodeDebug.NodeDebugSession, sourceMaps.SourceMaps);
nodeDebugWrapper.customizeNodeAdapterRequests();
} catch (e) {
const debugSession = new vscodeDebugAdapterPackage.DebugSession();
debugSession.sendEvent(new vscodeDebugAdapterPackage.OutputEvent("Unable to start debug adapter: " + e.toString(), "stderr"));
debugSession.sendEvent(new vscodeDebugAdapterPackage.TerminatedEvent());
// TODO: this is declared as abstract in vscode-chrome-debug-core.
// Need to check whether that possible to instantiate it here
const debugSession = new ChromeDebuggerPackage.ChromeDebugSession();
debugSession.sendEvent(new VSCodeDebugAdapter.OutputEvent("Unable to start debug adapter: " + e.toString(), "stderr"));
debugSession.sendEvent(new VSCodeDebugAdapter.TerminatedEvent());
bailOut(e.toString());
}
// Run the debug session for the node debug adapter with our modified requests
vscodeDebugAdapterPackage.DebugSession.run(nodeDebug.NodeDebugSession);
ChromeDebuggerPackage.ChromeDebugSession.run(session);
});

Просмотреть файл

@ -29,7 +29,7 @@ export class ScriptImporter {
this.sourceMapUtil = new SourceMapUtil();
}
public downloadAppScript(scriptUrlString: string, debugAdapterPort: number): Q.Promise<DownloadedScript> {
public downloadAppScript(scriptUrlString: string): Q.Promise<DownloadedScript> {
const parsedScriptUrl = url.parse(scriptUrlString);
const overriddenScriptUrlString = (parsedScriptUrl.hostname === "localhost") ? this.overridePackagerPort(scriptUrlString) : scriptUrlString;
// We'll get the source code, and store it locally to have a better debugging experience
@ -52,9 +52,6 @@ export class ScriptImporter {
.then((scriptFilePath: string) => {
Log.logInternalMessage(LogLevel.Info, `Script ${overriddenScriptUrlString} downloaded to ${scriptFilePath}`);
return { contents: scriptBody, filepath: scriptFilePath };
}).finally(() => {
// Request that the debug adapter update breakpoints and sourcemaps now that we have written them
return new Request().request(`http://localhost:${debugAdapterPort}/refreshBreakpoints`);
});
});
}

Просмотреть файл

@ -68,9 +68,7 @@ export function activate(context: vscode.ExtensionContext): void {
warnWhenReactNativeVersionIsNotSupported();
entryPointHandler.runFunction("debugger.setupLauncherStub",
ErrorHelper.getInternalError(InternalErrorCode.DebuggerStubLauncherFailed), () =>
setupReactNativeDebugger()
.then(() =>
setupAndDispose(new ReactDirManager(), context))
setupAndDispose(new ReactDirManager(), context)
.then(() =>
setupAndDispose(new ExtensionServer(projectRootPath, globalPackager, packagerStatusIndicator, globalExponentHelper), context))
.then(() => {}));
@ -106,8 +104,7 @@ function configureLogLevel(): void {
}
function configureNodeDebuggerLocation(): Q.Promise<void> {
const nodeDebugExtension = vscode.extensions.getExtension("ms-vscode.node-debug") // We try to get the new version
|| vscode.extensions.getExtension("andreweinand.node-debug"); // If it's not available, we try to get the old version
const nodeDebugExtension = vscode.extensions.getExtension("ms-vscode.node-debug2");
if (!nodeDebugExtension) {
return Q.reject<void>(ErrorHelper.getInternalError(InternalErrorCode.CouldNotFindLocationOfNodeDebugger));
}
@ -154,33 +151,3 @@ function registerVSCodeCommand(
`commandPalette.${commandName}`, error,
commandHandler)));
}
/**
* Sets up the debugger for the React Native project by dropping
* the debugger stub into the workspace
*/
function setupReactNativeDebugger(): Q.Promise<void> {
const launcherPath = require.resolve("../debugger/launcher");
const pkg = require("../../package.json");
const extensionVersionNumber = pkg.version;
const extensionName = pkg.name;
let debuggerEntryCode =
`// This file is automatically generated by ${extensionName}@${extensionVersionNumber}
// Please do not modify it manually. All changes will be lost.
try {
var path = require("path");
var Launcher = require(${JSON.stringify(launcherPath)}).Launcher;
new Launcher("${workspaceRootPath.replace(/\\/g, "\\\\")}", "${projectRootPath.replace(/\\/g, "\\\\")}").launch();
} catch (e) {
throw new Error("Unable to launch application. Try deleting .vscode/launchReactNative.js and restarting vscode.");
}`;
const vsCodeFolder = path.join(workspaceRootPath, ".vscode");
const debugStub = path.join(vsCodeFolder, "launchReactNative.js");
return fsUtil.ensureDirectory(vsCodeFolder)
.then(() => fsUtil.ensureFileWithContents(debugStub, debuggerEntryCode))
.catch((err: Error) => {
vscode.window.showErrorMessage(err.message);
});
}

Просмотреть файл

@ -18,7 +18,6 @@ suite("appWorker", function() {
suite("SandboxedAppWorker", function() {
const sourcesStoragePath = path.resolve(__dirname, "assets");
const startScriptFileName = require.resolve(path.join(sourcesStoragePath, ScriptImporter.DEBUGGER_WORKER_FILE_BASENAME));
const debugAdapterPort = 9090;
let sandboxedWorker: AppWorker.SandboxedAppWorker;
let downloadAppScriptStub = sinon.stub();
@ -34,7 +33,7 @@ suite("appWorker", function() {
downloadAppScript: downloadAppScriptStub,
};
sandboxedWorker = new AppWorker.SandboxedAppWorker(packagerPort, sourcesStoragePath, debugAdapterPort, postReplyFunction, {
sandboxedWorker = new AppWorker.SandboxedAppWorker(packagerPort, sourcesStoragePath, postReplyFunction, {
nodeFileSystem: nodeFileSystemMock,
scriptImporter: scriptImporterMock,
});
@ -68,7 +67,7 @@ suite("appWorker", function() {
const testScriptContents = "postMessage('inImport')";
const scriptImportDeferred = Q.defer<void>();
downloadAppScriptStub.withArgs(scriptImportPath, debugAdapterPort).returns(scriptImportDeferred.promise.then(() => {
downloadAppScriptStub.withArgs(scriptImportPath).returns(scriptImportDeferred.promise.then(() => {
return {
contents: testScriptContents,
filepath: scriptImportPath,
@ -117,7 +116,6 @@ suite("appWorker", function() {
suite("MultipleLifetimesAppWorker", function() {
const sourcesStoragePath = path.resolve(__dirname, "assets");
const debugAdapterPort = 9090;
let multipleLifetimesWorker: AppWorker.MultipleLifetimesAppWorker;
let sandboxedAppWorkerStub: Sinon.SinonStub;
@ -144,7 +142,7 @@ suite("appWorker", function() {
packagerIsRunning = sinon.stub(Packager, "isPackagerRunning");
packagerIsRunning.returns(Q.resolve(true));
multipleLifetimesWorker = new AppWorker.MultipleLifetimesAppWorker(packagerPort, sourcesStoragePath, debugAdapterPort, {
multipleLifetimesWorker = new AppWorker.MultipleLifetimesAppWorker(packagerPort, sourcesStoragePath, {
sandboxedAppConstructor: sandboxedAppConstructor,
webSocketConstructor: webSocketConstructor,
});
@ -152,6 +150,7 @@ suite("appWorker", function() {
teardown(function() {
// Reset everything
multipleLifetimesWorker.stop();
multipleLifetimesWorker = null;
webSocket = null;
sandboxedAppWorkerStub = null;