diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2b8102d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: node_js + +node_js: + - "8.0" + +script: + - npm run build + - npm test + - npm run tslint diff --git a/.vscode/launch.json b/.vscode/launch.json index abcb221..2da8ae4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,11 +2,11 @@ "version": "0.1.0", "configurations": [ { - "name": "launch as server", + "name": "Launch as server", "type": "node", "request": "launch", - "program": "${workspaceRoot}/out/debug-adapter/webKitDebug.js", - "runtimeArgs": ["--nolazy"], + "program": "${workspaceRoot}/out/debug-adapter/nativeScriptDebug.js", + "runtimeArgs": ["--nolazy"], "args": [ "--server=4712" ], "stopOnEntry": false, "sourceMaps": true, @@ -14,71 +14,32 @@ "cwd": "${workspaceFolder}" }, { - "name": "launch in extension host", + "name": "Launch in extension host", "type": "extensionHost", "request": "launch", - // Path to VSCode executable + // Path to VSCode executablensDebugClient "runtimeExecutable": "${execPath}", "args": [ "--extensionDevelopmentPath=${workspaceRoot}" ], "stopOnEntry": false, "sourceMaps": true, - "outFiles": [ "${workspaceFolder}/out/**/*.js" ], + "outFiles": [ "${workspaceFolder}/out/**/*.js" ] }, - { - "name": "run tests on mac", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", - "runtimeArgs": [ "--nolazy" ], - "args": [ - "--opts", "${workspaceRoot}/src/tests/config/mocha.opts", - "--config", "${workspaceRoot}/src/tests/config/mac.json", - "${workspaceRoot}/out/tests/" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}/out", - "cwd": "${workspaceRoot}" - }, - { - "name": "run tests on win", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", - "runtimeArgs": [ "--nolazy" ], - "args": [ - "--opts", "${workspaceRoot}/src/tests/config/mocha.opts", - "--config", "${workspaceRoot}/src/tests/config/win.json", - "${workspaceRoot}/out/tests/" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}/out", - "cwd": "${workspaceRoot}" - }, - { - "name": "run tests (custom)", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", - "runtimeArgs": [ "--nolazy" ], - "args": [ - "--opts", "${workspaceRoot}/src/tests/config/mocha.opts", - "--config", "${workspaceRoot}/src/tests/config/custom.json", - "${workspaceRoot}/out/tests/" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}/out", - "cwd": "${workspaceRoot}" - } + { + "name": "Run tests", + "type": "node", + "request": "launch", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", "test", "--" + ] + } ], - "compounds": [ - { - "name": "Extension + Server", - "configurations": [ "launch in extension host", "launch as server" ] - } - ] + "compounds": [ + { + "name": "Extension + Server", + "configurations": [ "Launch in extension host", "Launch as server" ] + } + ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index de4c0bf..cd7cee8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "editor.insertSpaces": true, "files.trimTrailingWhitespace": true, + "editor.renderWhitespace": "all", "files.exclude": { ".git": true, "bin": true, diff --git a/package.json b/package.json index 1514180..40dca2c 100644 --- a/package.json +++ b/package.json @@ -25,25 +25,25 @@ ], "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { - "node-ipc": "8.10.3", - "source-map": "0.6.1", + "lodash": "^4.17.10", + "semver": "^5.5.0", "universal-analytics": "0.4.13", "uuid": "^3.2.1", - "vscode-chrome-debug-core": "~3.9.0", - "vscode-debugadapter": "1.26.0", - "vscode-debugprotocol": "1.26.0", - "xmlhttprequest": "https://github.com/telerik/node-XMLHttpRequest/tarball/master" + "vscode-chrome-debug-core": "^3.23.11", + "vscode-debugadapter": "^1.28.0-pre.2" }, "devDependencies": { - "@types/mocha": "2.2.41", + "@types/lodash": "^4.14.109", + "@types/mocha": "^5.2.1", "@types/node": "6.0.46", - "@types/source-map": "~0.1.0", - "chrome-remote-debug-protocol": "https://github.com/roblourens/chrome-remote-debug-protocol/tarball/master", - "mocha": "2.5.3", + "mocha": "^5.2.0", + "sinon": "^5.0.10", + "tslint": "5.10.0", + "tslint-eslint-rules": "^5.3.1", "typescript": "2.6.2", "vsce": "~1.36.0", "vscode": "~1.1.10", - "vscode-debugadapter-testsupport": "1.26.0" + "vscode-debugprotocol": "^1.28.0-pre.1" }, "scripts": { "clean": "git clean -fdx", @@ -51,10 +51,9 @@ "build": "tsc -p ./src", "package": "vsce package", "full-build": "npm run clean && npm install && npm run build && npm run package", - "launch-as-server": "node --nolazy ./out/debug-adapter/webKitDebug.js --server=4712", - "test-mac": "mocha --opts ./src/tests/config/mocha.opts --config ../../src/tests/config/mac.json ./out/tests", - "test-win": "mocha --opts ./src/tests/config/mocha.opts --config ../../src/tests/config/win.json ./out/tests", - "test-custom": "mocha --opts ./src/tests/config/mocha.opts --config ../../src/tests/config/custom.json ./out/tests" + "launch-as-server": "node --nolazy ./out/debug-adapter/nativeScriptDebug.js --server=4712", + "test": "mocha --opts ./src/tests/config/mocha.opts", + "tslint": "tslint -p ./src/tsconfig.json -c tslint.json" }, "main": "./out/main", "activationEvents": [ @@ -129,7 +128,7 @@ "typescript" ] }, - "program": "./out/debug-adapter/webKitDebug.js", + "program": "./out/debug-adapter/nativeScriptDebug.js", "runtime": "node", "initialConfigurations": [ { diff --git a/src/analytics/analyticsBaseInfo.ts b/src/analytics/analyticsBaseInfo.ts index 0de72b5..6651382 100644 --- a/src/analytics/analyticsBaseInfo.ts +++ b/src/analytics/analyticsBaseInfo.ts @@ -2,12 +2,12 @@ export enum OperatingSystem { Windows, Linux, OSX, - Other + Other, } -export interface AnalyticsBaseInfo { - operatingSystem: OperatingSystem, - cliVersion: string, - extensionVersion: string, - clientId: string -} \ No newline at end of file +export interface IAnalyticsBaseInfo { + operatingSystem: OperatingSystem; + cliVersion: string; + extensionVersion: string; + clientId: string; +} diff --git a/src/analytics/analyticsService.ts b/src/analytics/analyticsService.ts index b488e36..e25dbb0 100644 --- a/src/analytics/analyticsService.ts +++ b/src/analytics/analyticsService.ts @@ -1,82 +1,91 @@ -import * as os from 'os'; -import { Version } from '../common/version'; -import { GUAService } from './guaService'; -import { AnalyticsBaseInfo, OperatingSystem } from './analyticsBaseInfo'; -import { Services } from '../services/extensionHostServices'; -import * as utils from '../common/utilities'; +import * as _ from 'lodash'; +import * as uuid from 'uuid'; import * as vscode from 'vscode'; -import * as uuid from "uuid"; +import { ILogger } from '../common/logger'; +import { services } from '../services/extensionHostServices'; +import { IAnalyticsBaseInfo, OperatingSystem } from './analyticsBaseInfo'; +import { GUAService } from './guaService'; export class AnalyticsService { - private static HAS_ANALYTICS_PROMPT_SHOWN_KEY = "nativescript.hasAnalyticsPromptShown"; - private static CLIENT_ID_KEY = "nativescript.analyticsClientId"; + private static HAS_ANALYTICS_PROMPT_SHOWN_KEY = 'nativescript.hasAnalyticsPromptShown'; + private static CLIENT_ID_KEY = 'nativescript.analyticsClientId'; + private static DOCS_LINK = 'https://github.com/NativeScript/nativescript-vscode-extension/blob/master/README.md#how-to-disable-the-analytics'; private static ANALYTICS_PROMPT_MESSAGE = `Help us improve the NativeScript extension by allowing Progress to collect anonymous usage data. - For more information about the gathered information and how it is used, read our [privacy statement](https://www.progress.com/legal/privacy-policy). - You can [disable the analytics and data collection](https://github.com/NativeScript/nativescript-vscode-extension/blob/master/README.md#how-to-disable-the-analytics) at any given time. + For more information about the gathered information and how it is used, + read our [privacy statement](https://www.progress.com/legal/privacy-policy). + You can [disable the analytics and data collection](${AnalyticsService.DOCS_LINK}) at any given time. Do you want to enable analytics?`; - private static ANALYTICS_PROMPT_ACCEPT_ACTION = "Yes"; - private static ANALYTICS_PROMPT_DENY_ACTION = "No"; + + private static ANALYTICS_PROMPT_ACCEPT_ACTION = 'Yes'; + private static ANALYTICS_PROMPT_DENY_ACTION = 'No'; + + private static getOperatingSystem(): OperatingSystem { + switch (process.platform) { + case 'win32': + return OperatingSystem.Windows; + case 'darwin': + return OperatingSystem.OSX; + case 'linux': + case 'freebsd': + return OperatingSystem.Linux; + default: + return OperatingSystem.Other; + } + } private _globalState: vscode.Memento; - private _baseInfo: AnalyticsBaseInfo; + private _logger: ILogger; + private _baseInfo: IAnalyticsBaseInfo; private _gua: GUAService; private _analyticsEnabled: boolean; - constructor(globalState: vscode.Memento) { + constructor(globalState: vscode.Memento, cliVersion: string, extensionVersion: string, logger: ILogger) { this._globalState = globalState; + this._logger = logger; vscode.workspace.onDidChangeConfiguration(() => this.updateAnalyticsEnabled()); this._baseInfo = { - cliVersion: Services.cli().version.toString(), - extensionVersion: utils.getInstalledExtensionVersion().toString(), + cliVersion, + clientId: this.getOrGenerateClientId(), + extensionVersion, operatingSystem: AnalyticsService.getOperatingSystem(), - clientId: this.getOrGenerateClientId() }; } public launchDebugger(request: string, platform: string): Promise { - if(this._analyticsEnabled) { + if (this._analyticsEnabled) { try { return this._gua.launchDebugger(request, platform); - } catch(e) {} + } catch (e) { + this._logger.log(`Analytics error: ${_.isString(e) ? e : e.message}`); + } } return Promise.resolve(); } public runRunCommand(platform: string): Promise { - if(this._analyticsEnabled) { + if (this._analyticsEnabled) { try { return this._gua.runRunCommand(platform); - } catch(e) { } + } catch (e) { + this._logger.log(`Analytics error: ${_.isString(e) ? e : e.message}`); + } } return Promise.resolve(); } - private static getOperatingSystem() : OperatingSystem { - switch(process.platform) { - case 'win32': - return OperatingSystem.Windows; - case 'darwin': - return OperatingSystem.OSX; - case 'linux': - case 'freebsd': - return OperatingSystem.Linux; - default: - return OperatingSystem.Other; - }; - } - - public initialize() : void { + public initialize(): void { const hasAnalyticsPromptShown = this._globalState.get(AnalyticsService.HAS_ANALYTICS_PROMPT_SHOWN_KEY); - if(!hasAnalyticsPromptShown) { + + if (!hasAnalyticsPromptShown) { vscode.window.showInformationMessage(AnalyticsService.ANALYTICS_PROMPT_MESSAGE, AnalyticsService.ANALYTICS_PROMPT_ACCEPT_ACTION, - AnalyticsService.ANALYTICS_PROMPT_DENY_ACTION + AnalyticsService.ANALYTICS_PROMPT_DENY_ACTION, ) - .then(result => this.onAnalyticsMessageConfirmation(result)); + .then((result) => this.onAnalyticsMessageConfirmation(result)); return; } @@ -87,7 +96,7 @@ export class AnalyticsService { private getOrGenerateClientId(): string { let clientId = this._globalState.get(AnalyticsService.CLIENT_ID_KEY); - if(!clientId) { + if (!clientId) { clientId = uuid.v4(); this._globalState.update(AnalyticsService.CLIENT_ID_KEY, clientId); } @@ -95,20 +104,20 @@ export class AnalyticsService { return clientId; } - private onAnalyticsMessageConfirmation(result: string) : void { + private onAnalyticsMessageConfirmation(result: string): void { const shouldEnableAnalytics = result === AnalyticsService.ANALYTICS_PROMPT_ACCEPT_ACTION ? true : false; this._globalState.update(AnalyticsService.HAS_ANALYTICS_PROMPT_SHOWN_KEY, true); - Services.workspaceConfigService().isAnalyticsEnabled = shouldEnableAnalytics; + services.workspaceConfigService.isAnalyticsEnabled = shouldEnableAnalytics; this.updateAnalyticsEnabled(); } private updateAnalyticsEnabled() { - this._analyticsEnabled = Services.workspaceConfigService().isAnalyticsEnabled; + this._analyticsEnabled = services.workspaceConfigService.isAnalyticsEnabled; - if(this._analyticsEnabled && !this._gua) { + if (this._analyticsEnabled && !this._gua) { this._gua = new GUAService('UA-111455-29', this._baseInfo); } } -} \ No newline at end of file +} diff --git a/src/analytics/guaService.ts b/src/analytics/guaService.ts index 6d50f49..8ecf4e4 100644 --- a/src/analytics/guaService.ts +++ b/src/analytics/guaService.ts @@ -1,5 +1,5 @@ import * as ua from 'universal-analytics'; -import { AnalyticsBaseInfo, OperatingSystem } from './analyticsBaseInfo'; +import { IAnalyticsBaseInfo, OperatingSystem } from './analyticsBaseInfo'; /** * Google Universal Analytics Service @@ -8,38 +8,42 @@ export class GUAService { private _visitor: any; private _getBasePayload: () => any; - constructor(trackingId: string, baseInfo: AnalyticsBaseInfo) { + constructor(trackingId: string, baseInfo: IAnalyticsBaseInfo) { this._visitor = ua(trackingId, baseInfo.clientId, { requestOptions: {}, strictCidFormat: false }); this._getBasePayload = () => { return { - cid: baseInfo.clientId, - dh: 'ns-vs-extension.org', cd5: baseInfo.cliVersion, cd6: OperatingSystem[baseInfo.operatingSystem], - cd7: baseInfo.extensionVersion + cd7: baseInfo.extensionVersion, + cid: baseInfo.clientId, + dh: 'ns-vs-extension.org', }; }; } public launchDebugger(request: string, platform: string): Promise { - let payload = this._getBasePayload(); + const payload = this._getBasePayload(); + payload.ec = 'vscode-extension-debug'; // event category payload.ea = `debug-${request}-on-${platform}`; // event action + return this.sendEvent(payload); } public runRunCommand(platform: string): Promise { - let payload = this._getBasePayload(); + const payload = this._getBasePayload(); + payload.ec = 'vscode-extension-command'; // event category payload.ea = `command-run-on-${platform}`; // event action + return this.sendEvent(payload); } private sendEvent(params): Promise { return new Promise((res, rej) => { - this._visitor.event(params, err => { + this._visitor.event(params, (err) => { return err ? rej(err) : res(); }); }); } -} \ No newline at end of file +} diff --git a/src/common/extensionProtocol.ts b/src/common/extensionProtocol.ts new file mode 100644 index 0000000..67bc1b7 --- /dev/null +++ b/src/common/extensionProtocol.ts @@ -0,0 +1,14 @@ +export interface IRequest { + id: string; + service: string; + method: string; + args: any[]; +} + +export interface IResponse { + requestId: string; + result: object; +} + +export const BEFORE_DEBUG_START = 'before-debug-start'; +export const NS_DEBUG_ADAPTER_MESSAGE = 'ns-debug-adapter-message'; diff --git a/src/common/logger.ts b/src/common/logger.ts index 49fde9d..215398e 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -1,102 +1,13 @@ -import * as fs from 'fs'; - -export enum LoggerMessageType { - Log, - Info, - Warning, - Error +enum LogLevel { + Verbose = 0, + Log = 1, + Warn = 2, + Error = 3, + Stop = 4, } -export interface LoggerMessageEventArgs { - message: string, - type: LoggerMessageType +interface ILogger { + log(msg: string, level?: LogLevel): void; } -export type LoggerHandler = ((args: LoggerMessageEventArgs) => void); -type TaggedLoggerHandler = { handler: LoggerHandler, tags: string[] }; - -/** - * The logger is a singleton. - */ -export class Logger { - private _handlers: TaggedLoggerHandler[]; - - constructor() { - this._handlers = []; - } - - private handleMessage(message: string, type: LoggerMessageType = LoggerMessageType.Log, tag: string = null) { - for (let handler of this._handlers) { - if (!handler.tags || handler.tags.length == 0 || handler.tags.indexOf(tag) > -1) { - handler.handler({ message: message, type: type }); - } - } - } - - public log(message: string, tag: string = null): void { - this.handleMessage(message, LoggerMessageType.Log, tag); - } - - public info(message: string, tag: string = null): void { - this.handleMessage(message, LoggerMessageType.Info, tag); - } - - public warn(message: string, tag: string = null): void { - this.handleMessage(message, LoggerMessageType.Warning, tag); - } - - public error(message: string, tag: string = null): void { - this.handleMessage(message, LoggerMessageType.Error, tag); - } - - public addHandler(handler: LoggerHandler, tags: string[] = null) { - tags = tags || []; - this._handlers.push({ handler: handler, tags: tags }); - } - - /** - * Removes all occurrence of this handler, ignoring the associated tags - */ - public removeHandler(handlerToRemove: LoggerHandler) { - let i = this._handlers.length; - while (i--) { - if (this._handlers[i].handler == handlerToRemove) { - this._handlers.splice(i, 1); - } - } - } -} - -export namespace Tags { - export const FrontendMessage: string = "LoggerTag.FrontendMessage"; -} - -export namespace Handlers { - export function stdStreamsHandler(args: LoggerMessageEventArgs) { - var message = args.message.replace(/\n$/, ""); - switch(args.type) { - case LoggerMessageType.Log: - console.log(message); - break; - case LoggerMessageType.Info: - console.info(message); - break; - case LoggerMessageType.Warning: - console.warn(message); - break; - case LoggerMessageType.Error: - console.error(message); - break; - } - }; - - export function createStreamHandler(stream: fs.WriteStream, encoding: string = 'utf8'): LoggerHandler { - let isStreamClosed = false; - stream.on('close', () => { isStreamClosed = true; }); - return (args: LoggerMessageEventArgs) => { - if (stream && !isStreamClosed) { - stream.write(args.message, encoding); - } - } - } -} \ No newline at end of file +export { ILogger, LogLevel }; diff --git a/src/common/utilities.ts b/src/common/utilities.ts index e581c68..dfde8c8 100644 --- a/src/common/utilities.ts +++ b/src/common/utilities.ts @@ -1,396 +1,26 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ +import { ChildProcess, exec } from 'child_process'; +import * as os from 'os'; -// TODO: Some functions can be moved to common. +export function killProcess(childProcess: ChildProcess): void { + switch (process.platform) { + case 'win32': + exec(`taskkill /pid ${childProcess.pid} /T /F`); + break; -import * as http from 'http'; -import * as os from 'os'; -import * as fs from 'fs'; -import * as url from 'url'; -import * as path from 'path'; -import {Version} from './version'; -import {ChildProcess, exec} from 'child_process'; + default: + childProcess.kill('SIGINT'); + break; + } +} export const enum Platform { - Windows, OSX, Linux + Windows, OSX, Linux, } export function getPlatform(): Platform { const platform = os.platform(); + return platform === 'darwin' ? Platform.OSX : platform === 'win32' ? Platform.Windows : Platform.Linux; } - -/** - * Node's fs.existsSync is deprecated, implement it in terms of statSync - */ -export function existsSync(path: string): boolean { - try { - fs.statSync(path); - return true; - } catch (e) { - // doesn't exist - return false; - } -} - -/** - * Returns a reversed version of arr. Doesn't modify the input. - */ -export function reversedArr(arr: any[]): any[] { - return arr.reduce((reversed: any[], x: any) => { - reversed.unshift(x); - return reversed; - }, []); -} - -export function promiseTimeout(p?: Promise, timeoutMs: number = 1000, timeoutMsg?: string): Promise { - if (timeoutMsg === undefined) { - timeoutMsg = `Promise timed out after ${timeoutMs}ms`; - } - - return new Promise((resolve, reject) => { - if (p) { - p.then(resolve, reject); - } - - setTimeout(() => { - if (p) { - reject(timeoutMsg); - } else { - resolve(); - } - }, timeoutMs); - }); -} - -export function retryAsync(fn: () => Promise, timeoutMs: number): Promise { - const startTime = Date.now(); - - function tryUntilTimeout(): Promise { - return fn().catch( - e => { - if (Date.now() - startTime < timeoutMs) { - return tryUntilTimeout(); - } else { - return errP(e); - } - }); - } - - return tryUntilTimeout(); -} - -function tryFindSourcePathInNSProject(nsProjectPath: string, additionalFileExtension: string, resorcePath: string) : string { - let guess: string = ""; - const pathParts = resorcePath.split(path.sep); - let appIndex = pathParts.indexOf("app"); - let isTnsModule = appIndex >= 0 && pathParts.length > appIndex + 1 && pathParts[appIndex + 1] === "tns_modules"; - //let isTnsModule: boolean = (pathParts.length >= 3 && pathParts[0] == '' && pathParts[1] == 'app' && pathParts[2] == 'tns_modules'); - if (isTnsModule) { - // the file is part of a module, so we search it in '{ns-app}/node_modules/' - let nsNodeModulesPath: string = path.join(nsProjectPath, 'node_modules'); - - let modulePath: string = path.join.apply(path, pathParts.slice(appIndex + 2)); - guess = path.join(nsNodeModulesPath, modulePath); - } - else { - guess = path.join(nsProjectPath, resorcePath); - } - - if (existsSync(guess)) { - return canonicalizeUrl(guess); - } - - let extension: string = path.extname(guess); - let platformSpecificPath: string = guess.substr(0, guess.length - extension.length) + '.' + additionalFileExtension + extension; - if (existsSync(platformSpecificPath)) { - return canonicalizeUrl(platformSpecificPath); - } - - return null; -} - -/** - * Maps a url from webkit to an absolute local path. - * If not given an absolute path (with file: prefix), searches the current working directory for a matching file. - * http://localhost/scripts/code.js => d:/app/scripts/code.js - * file:///d:/scripts/code.js => d:/scripts/code.js - */ -export function webkitUrlToClientPath(webRoot: string, additionalFileExtension: string, aUrl: string): string { - if (!aUrl) { - return ''; - } - - // If we don't have the client workingDirectory for some reason, don't try to map the url to a client path - if (!webRoot) { - return ''; - } - - aUrl = decodeURI(aUrl); - - // Search the filesystem under the webRoot for the file that best matches the given url - let pathName = url.parse(canonicalizeUrl(aUrl)).pathname; - if (!pathName || pathName === '/') { - return ''; - } - - // Dealing with the path portion of either a url or an absolute path to remote file. - // Need to force path.sep separator - pathName = pathName.replace(/\//g, path.sep); - let nsProjectFile = tryFindSourcePathInNSProject(webRoot, additionalFileExtension, pathName); - if(nsProjectFile) { - return nsProjectFile; - } - - let pathParts = pathName.split(path.sep); - while (pathParts.length > 0) { - const clientPath = path.join(webRoot, pathParts.join(path.sep)); - if (existsSync(clientPath)) { - return canonicalizeUrl(clientPath); - } - - pathParts.shift(); - } - - //check for {N} android internal files - pathParts = pathName.split(path.sep); - while (pathParts.length > 0) { - const clientPath = path.join(webRoot, "platforms/android/src/main/assets", pathParts.join(path.sep)); - if (existsSync(clientPath)) { - return canonicalizeUrl(clientPath); - } - - pathParts.shift(); - } - - return ''; -} - -/** - * Infers the device root of a given path. - * The device root is the parent directory of all {N} source files - * This implementation assumes that all files are all under one common root on the device - * Returns all the device parent directories of a source file until the file is found on the client by client path - */ -export function inferDeviceRoot(projectRoot: string, additionalFileExtension: string, aUrl: string): string { - if (!aUrl) { - return null; - } - - // If we don't have the projectRoot for some reason, don't try to map the url to a client path - if (!projectRoot) { - return null; - } - - aUrl = decodeURI(aUrl); - - // Search the filesystem under the webRoot for the file that best matches the given url - let pathName = url.parse(canonicalizeUrl(aUrl)).pathname; - if (!pathName || pathName === '/') { - return null; - } - - // Dealing with the path portion of either a url or an absolute path to remote file. - // Need to force path.sep separator - pathName = pathName.replace(/\//g, path.sep); - - let shiftedParts = []; - let pathParts = pathName.split(path.sep); - while (pathParts.length > 0) { - const clientPath = path.join(projectRoot, pathParts.join(path.sep)); - if (existsSync(clientPath)) { - //return canonicalizeUrl(clientPath); - return shiftedParts.join(path.sep).replace(/\\/g, "/"); - } - - let shifted = pathParts.shift(); - shiftedParts.push(shifted); - } - - //check for {N} android internal files - shiftedParts = []; - pathParts = pathName.split(path.sep); - while (pathParts.length > 0) { - const clientPath = path.join(projectRoot, "platforms/android/src/main/assets", pathParts.join(path.sep)); - if (existsSync(clientPath)) { - //return canonicalizeUrl(clientPath); - return shiftedParts.join(path.sep).replace(/\\/g, "/"); - } - - let shifted = pathParts.shift(); - shiftedParts.push(shifted); - } - - return null; -} - -/** - * Modify a url either from the client or the webkit target to a common format for comparing. - * The client can handle urls in this format too. - * file:///D:\\scripts\\code.js => d:/scripts/code.js - * file:///Users/me/project/code.js => /Users/me/project/code.js - * c:\\scripts\\code.js => c:/scripts/code.js - * http://site.com/scripts/code.js => (no change) - * http://site.com/ => http://site.com - */ -export function canonicalizeUrl(aUrl: string): string { - aUrl = aUrl.replace('file:///', ''); - aUrl = stripTrailingSlash(aUrl); - - aUrl = fixDriveLetterAndSlashes(aUrl); - if (aUrl[0] !== '/' && aUrl.indexOf(':') < 0 && getPlatform() === Platform.OSX) { - // Ensure osx path starts with /, it can be removed when file:/// was stripped. - // Don't add if the url still has a protocol - aUrl = '/' + aUrl; - } - - return aUrl; -} - -/** - * Ensure lower case drive letter and \ on Windows - */ -export function fixDriveLetterAndSlashes(aPath: string): string { - if (getPlatform() === Platform.Windows) { - if (aPath.match(/file:\/\/\/[A-Za-z]:/)) { - const prefixLen = 'file:///'.length; - aPath = - 'file:///' + - aPath[prefixLen].toLowerCase() + - aPath.substr(prefixLen + 1).replace(/\//g, path.sep); - } else if (aPath.match(/^[A-Za-z]:/)) { - // If this is Windows and the path starts with a drive letter, ensure lowercase. VS Code uses a lowercase drive letter - aPath = aPath[0].toLowerCase() + aPath.substr(1); - aPath = aPath.replace(/\//g, path.sep); - } - } - - return aPath; -} - -/** - * Remove a slash of any flavor from the end of the path - */ -export function stripTrailingSlash(aPath: string): string { - return aPath - .replace(/\/$/, '') - .replace(/\\$/, ''); -} - -export function remoteObjectToValue(object: WebKitProtocol.Runtime.RemoteObject, stringify = true): { value: string, variableHandleRef: string } { - let value = ''; - let variableHandleRef: string; - - if (object) { // just paranoia? - if (object && object.type === 'object') { - if (object.subtype === 'null') { - value = 'null'; - } else { - // If it's a non-null object, create a variable reference so the client can ask for its props - variableHandleRef = object.objectId; - value = object.description; - } - } else if (object && object.type === 'undefined') { - value = 'undefined'; - } else if (object.type === 'function') { - const firstBraceIdx = object.description.indexOf('{'); - if (firstBraceIdx >= 0) { - value = object.description.substring(0, firstBraceIdx) + '{ … }'; - } else { - const firstArrowIdx = object.description.indexOf('=>'); - value = firstArrowIdx >= 0 ? - object.description.substring(0, firstArrowIdx + 2) + ' …' : - object.description; - } - } else { - // The value is a primitive value, or something that has a description (not object, primitive, or undefined). And force to be string - if (typeof object.value === 'undefined') { - value = object.description; - } else { - value = stringify ? JSON.stringify(object.value) : object.value; - } - } - } - - return { value, variableHandleRef }; -} - -/** - * A helper for returning a rejected promise with an Error object. Avoids double-wrapping an Error, which could happen - * when passing on a failure from a Promise error handler. - * @param msg - Should be either a string or an Error - */ -export function errP(msg: any): Promise { - let e: Error; - if (!msg) { - e = new Error('Unknown error'); - } else if (msg.message) { - // msg is already an Error object - e = msg; - } else { - e = new Error(msg); - } - - return Promise.reject(e); -} - -/** - * Helper function to GET the contents of a url - */ -export function getURL(aUrl: string): Promise { - return new Promise((resolve, reject) => { - http.get(aUrl, response => { - let responseData = ''; - response.on('data', chunk => responseData += chunk); - response.on('end', () => { - // Sometimes the 'error' event is not fired. Double check here. - if (response.statusCode === 200) { - resolve(responseData); - } else { - reject(responseData); - } - }); - }).on('error', e => { - reject(e); - }); - }); -} - -/** - * Returns true if urlOrPath is like "http://localhost" and not like "c:/code/file.js" or "/code/file.js" - */ -export function isURL(urlOrPath: string): boolean { - return urlOrPath && !path.isAbsolute(urlOrPath) && !!url.parse(urlOrPath).protocol; -} - -/** - * Strip a string from the left side of a string - */ -export function lstrip(s: string, lStr: string): string { - return s.startsWith(lStr) ? - s.substr(lStr.length) : - s; -} - -export function getInstalledExtensionVersion(): Version { - return Version.parse(require('../../package.json').version); -} - -export function getMinSupportedCliVersion(): Version { - return Version.parse(require('../../package.json').minNativescriptCliVersion); -} - -export function killProcess(childProcess: ChildProcess) : void { - switch (process.platform) { - case "win32": - exec(`taskkill /pid ${childProcess.pid} /T /F`); - break; - - default: - childProcess.kill("SIGINT"); - break; - } -} \ No newline at end of file diff --git a/src/common/version.ts b/src/common/version.ts deleted file mode 100644 index 59ba47d..0000000 --- a/src/common/version.ts +++ /dev/null @@ -1,28 +0,0 @@ -export class Version { - private _version: number[]; - - public static parse(versionStr: string): Version { - if (versionStr === null) { - return null; - } - let version: number[] = versionStr.split('.').map((str, index, array) => parseInt(str)); - for(let i = version.length; i < 3; i++) { - version.push(0); - } - return new Version(version); - } - - constructor(version: number[]) { - this._version = version; - } - - public toString(): string { - return `${this._version[0]}.${this._version[1]}.${this._version[2]}`; - } - - public compareBySubminorTo(other: Version): number { - let v1 = this._version; - let v2 = other._version; - return (v1[0] - v2[0] != 0) ? (v1[0] - v2[0]) : (v1[1] - v2[1] != 0) ? v1[1] - v2[1] : v1[2] - v2[2]; - } -} diff --git a/src/common/workspaceConfigService.ts b/src/common/workspaceConfigService.ts index 862ef29..89f82af 100644 --- a/src/common/workspaceConfigService.ts +++ b/src/common/workspaceConfigService.ts @@ -12,4 +12,4 @@ export class WorkspaceConfigService { public get tnsPath(): string { return vscode.workspace.getConfiguration('nativescript').get('tnsPath') as string; } -} \ No newline at end of file +} diff --git a/src/custom-typings/debugProtocolExtensions.d.ts b/src/custom-typings/debugProtocolExtensions.d.ts deleted file mode 100644 index 0508b34..0000000 --- a/src/custom-typings/debugProtocolExtensions.d.ts +++ /dev/null @@ -1,123 +0,0 @@ -import {DebugProtocol} from 'vscode-debugprotocol'; - -declare module 'vscode-debugprotocol' { - namespace DebugProtocol { - - type RequestArguments = LaunchRequestArguments | AttachRequestArguments; - type RequestType = "launch" | "attach"; - type PlatformType = "android" | "ios"; - - interface IRequestArgs { - request: RequestType; - platform: PlatformType; - appRoot: string; - sourceMaps?: boolean; - diagnosticLogging?: boolean; - tnsArgs?: string[]; - tnsOutput?: string; - } - - interface ILaunchRequestArgs extends DebugProtocol.LaunchRequestArguments, IRequestArgs { - stopOnEntry?: boolean; - watch?: boolean; - } - - interface IAttachRequestArgs extends DebugProtocol.AttachRequestArguments, IRequestArgs { - } - - interface ISetBreakpointsArgs extends DebugProtocol.SetBreakpointsArguments { - /** DebugProtocol does not send cols, maybe it will someday, but this is used internally when a location is sourcemapped */ - cols?: number[]; - authoredPath?: string; - } - - interface IBreakpoint extends DebugProtocol.Breakpoint { - column?: number; - } - - /* - * The ResponseBody interfaces are copied from debugProtocol.d.ts which defines these inline in the Response interfaces. - * They should always match those interfaces, see the original for comments. - */ - interface ISetBreakpointsResponseBody { - breakpoints: IBreakpoint[]; - } - - interface ISourceResponseBody { - content: string; - } - - interface IThreadsResponseBody { - threads: DebugProtocol.Thread[]; - } - - interface IStackTraceResponseBody { - stackFrames: DebugProtocol.StackFrame[]; - totalFrames?: number; - } - - interface IScopesResponseBody { - scopes: DebugProtocol.Scope[]; - } - - interface IVariablesResponseBody { - variables: DebugProtocol.Variable[]; - } - - interface IEvaluateResponseBody { - result: string; - variablesReference: number; - } - - type PromiseOrNot = T | Promise; - - interface IDebugAdapter { - registerEventHandler(eventHandler: (event: DebugProtocol.Event) => void): void; - - initialize(args: DebugProtocol.InitializeRequestArguments): PromiseOrNot; - launch(args: ILaunchRequestArgs): PromiseOrNot; - configurationDone(args: DebugProtocol.ConfigurationDoneArguments): void; - disconnect(): PromiseOrNot; - attach(args: IAttachRequestArgs): PromiseOrNot; - setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): PromiseOrNot; - setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): PromiseOrNot; - - continue(): PromiseOrNot; - next(): PromiseOrNot; - stepIn(): PromiseOrNot; - stepOut(): PromiseOrNot; - pause(): PromiseOrNot; - - stackTrace(args: DebugProtocol.StackTraceArguments): PromiseOrNot; - scopes(args: DebugProtocol.ScopesArguments): PromiseOrNot; - variables(args: DebugProtocol.VariablesArguments): PromiseOrNot; - source(args: DebugProtocol.SourceArguments): PromiseOrNot; - threads(): PromiseOrNot; - evaluate(args: DebugProtocol.EvaluateArguments): PromiseOrNot; - } - - interface IDebugTransformer { - initialize?(args: DebugProtocol.InitializeRequestArguments, requestSeq?: number): PromiseOrNot; - launch?(args: ILaunchRequestArgs, requestSeq?: number): PromiseOrNot; - attach?(args: IAttachRequestArgs, requestSeq?: number): PromiseOrNot; - setBreakpoints?(args: DebugProtocol.SetBreakpointsArguments, requestSeq?: number): PromiseOrNot; - setExceptionBreakpoints?(args: DebugProtocol.SetExceptionBreakpointsArguments, requestSeq?: number): PromiseOrNot; - - stackTrace?(args: DebugProtocol.StackTraceArguments, requestSeq?: number): PromiseOrNot; - scopes?(args: DebugProtocol.ScopesArguments, requestSeq?: number): PromiseOrNot; - variables?(args: DebugProtocol.VariablesArguments, requestSeq?: number): PromiseOrNot; - source?(args: DebugProtocol.SourceArguments, requestSeq?: number): PromiseOrNot; - evaluate?(args: DebugProtocol.EvaluateArguments, requestSeq?: number): PromiseOrNot; - - setBreakpointsResponse?(response: ISetBreakpointsResponseBody, requestSeq?: number): PromiseOrNot; - stackTraceResponse?(response: IStackTraceResponseBody, requestSeq?: number): PromiseOrNot; - scopesResponse?(response: IScopesResponseBody, requestSeq?: number): PromiseOrNot; - variablesResponse?(response: IVariablesResponseBody, requestSeq?: number): PromiseOrNot; - sourceResponse?(response: ISourceResponseBody, requestSeq?: number): PromiseOrNot; - threadsResponse?(response: IThreadsResponseBody, requestSeq?: number): PromiseOrNot; - evaluateResponse?(response: IEvaluateResponseBody, requestSeq?: number): PromiseOrNot; - - scriptParsed?(event: DebugProtocol.Event); - } - } -} \ No newline at end of file diff --git a/src/custom-typings/webKitProtocol.d.ts b/src/custom-typings/webKitProtocol.d.ts deleted file mode 100644 index d856010..0000000 --- a/src/custom-typings/webKitProtocol.d.ts +++ /dev/null @@ -1,261 +0,0 @@ -declare namespace WebKitProtocol { - interface Notification { - method: string; - params: any; - } - - interface Request { - id: number; - method: string; - params?: any; - } - - interface Response { - id: number; - error?: any; - result?: any; - } - - namespace Debugger { - type ScriptId = string; - type BreakpointId = string; - - interface Script { - scriptId: ScriptId; - url: string; - - startLine?: number; - startColumn?: number; - endLine?: number; - endColumn?: number; - sourceMapURL?: string; - isContentScript?: boolean; - } - - interface CallFrame { - callFrameId: string; - functionName: string; - location: Location; - scopeChain: Scope[]; - this: any; - } - - interface Scope { - object: Runtime.RemoteObject; - type: string; - } - - interface PausedParams { - callFrames: CallFrame[]; - // 'exception' or 'other' - reason: string; - data: Runtime.RemoteObject; - hitBreakpoints: BreakpointId[]; - } - - interface BreakpointResolvedParams { - breakpointId: BreakpointId; - location: Location; - } - - interface Location { - scriptId: ScriptId; - lineNumber: number; - columnNumber?: number; - } - - interface BreakpointAction { - type: string; /* "log", "evaluate", "sound" or "probe" */ - data?: string; - id?: number; - } - - interface BreakpointOptions { - condition?: string; - actions?: BreakpointAction[]; - autoContinue?: boolean; - ignoreCount?: number; - } - - interface SetBreakpointParams { - location: Location; - options: BreakpointOptions; - } - - interface SetBreakpointByUrlParams { - url?: string; - urlRegex?: string; - lineNumber: number; - columnNumber: number; - options: BreakpointOptions; - } - - interface SetBreakpointResponse extends Response { - result: { - breakpointId: BreakpointId; - actualLocation: Location; - }; - } - - interface SetBreakpointByUrlResponse extends Response { - result: { - breakpointId: BreakpointId; - locations: Location[]; - }; - } - - interface RemoveBreakpointParams { - breakpointId: BreakpointId; - } - - interface EvaluateOnCallFrameParams { - callFrameId: string; - expression: string; - objectGroup: string; - returnByValue: boolean; - } - - interface ExceptionStackFrame extends Location { - functionName: string; - scriptId: ScriptId; - url: string; - } - - interface EvaluateOnCallFrameResponse extends Response { - result: { - result: Runtime.RemoteObject; - wasThrown: boolean; - exceptionDetails?: { - text: string; - url: string; - line: number; - column: number; - stackTrace: ExceptionStackFrame[]; - }; - }; - } - - interface SetPauseOnExceptionsParams { - state: string; - } - - interface GetScriptSourceParams { - scriptId: ScriptId; - } - - interface GetScriptSourceResponse extends Response { - result: { - scriptSource: string; - }; - } - } - - namespace Runtime { - interface GetPropertiesParams { - objectId: string; - ownProperties: boolean; - accessorPropertiesOnly: boolean; - } - - interface GetPropertiesResponse extends Response { - result: { - result: PropertyDescriptor[]; - }; - } - - interface PropertyDescriptor { - configurable: boolean; - enumerable: boolean; - get?: RemoteObject; - name: string; - set?: RemoteObject; - value?: RemoteObject; - wasThrown?: boolean; - writeable?: boolean; - } - - interface RemoteObject { - className?: string; - description?: string; - objectId?: string; - subtype?: string; - type: string; - value?: any; - preview?: { - type: string; - description: string; - lossless: boolean; - overflow: boolean; - properties: PropertyPreview[]; - }; - } - - interface PropertyPreview { - name: string; - type: string; - subtype?: string; - value: string; - } - - interface EvaluateParams { - expression: string; - objectGroup: string; - contextId: number; - returnByValue: boolean; - } - - interface EvaluateResponse extends Response { - result: { - result: Runtime.RemoteObject; - wasThrown: boolean; - }; - } - } - - namespace Page { - interface SetOverlayMessageRequest extends Request { - message: string; - } - } - - namespace Console { - interface CallFrame { - lineNumber: number; - columnNumber: number; - functionName: string; - scriptId: Debugger.ScriptId; - url: string; - } - - type StackTrace = CallFrame[]; - - interface MessageAddedParams { - message: Message; - } - - interface MessageRepeatCountUpdatedEventArgs { - count: number; - } - - interface Message { - line?: number; - column?: number; - - // 'debug', 'error', 'log', 'warning' - level: string; - - // 'assert', 'clear', 'dir', 'dirxml', 'endGroup', 'log', 'profile', 'profileEnd', - // 'startGroup', 'startGroupCollapsed', 'table', 'timing', 'trace' - type?: string; - - parameters?: Runtime.RemoteObject[]; - repeatCount?: string; - stackTrace?: StackTrace; - text: string; - url?: string; - source?: string; - timestamp?: number; - executionContextId?: number; - } - } -} diff --git a/src/debug-adapter/adapter/adapterProxy.ts b/src/debug-adapter/adapter/adapterProxy.ts deleted file mode 100644 index 84887ab..0000000 --- a/src/debug-adapter/adapter/adapterProxy.ts +++ /dev/null @@ -1,84 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -import * as utils from '../../common/utilities'; -import {Services} from '../../services/debugAdapterServices'; -import {DebugProtocol} from 'vscode-debugprotocol'; - -export type EventHandler = (event: DebugProtocol.Event) => void; - -export class AdapterProxy { - private static INTERNAL_EVENTS = ['scriptParsed', 'clearClientContext', 'clearTargetContext']; - - public constructor(private _requestTransformers: DebugProtocol.IDebugTransformer[], private _debugAdapter: DebugProtocol.IDebugAdapter, private _eventHandler: EventHandler) { - this._debugAdapter.registerEventHandler(event => this.onAdapterEvent(event)); - } - - public dispatchRequest(request: DebugProtocol.Request): Promise { - if (!(request.command in this._debugAdapter)) { - return utils.errP('unknowncommand'); - } - - return this.transformRequest(request) - // Pass the modified args to the adapter - .then(() => this._debugAdapter[request.command](request.arguments)) - - // Pass the body back through the transformers and ensure the body is returned - .then((body?) => { - return this.transformResponse(request, body) - .then(() => body); - }); - } - - /** - * Pass the request arguments through the transformers. They modify the object in place. - */ - private transformRequest(request: DebugProtocol.Request): Promise { - return this._requestTransformers - // If the transformer implements this command, give it a chance to modify the args. Otherwise skip it - .filter(transformer => request.command in transformer) - .reduce( - (p, transformer) => p.then(() => transformer[request.command](request.arguments, request.seq)), - Promise.resolve()); - } - - /** - * Pass the response body back through the transformers in reverse order. They modify the body in place. - */ - private transformResponse(request: DebugProtocol.Request, body: any): Promise { - if (!body) { - return Promise.resolve(); - } - - const bodyTransformMethodName = request.command + 'Response'; - const reversedTransformers = utils.reversedArr(this._requestTransformers); - return reversedTransformers - // If the transformer implements this command, give it a chance to modify the args. Otherwise skip it - .filter(transformer => bodyTransformMethodName in transformer) - .reduce( - (p, transformer) => p.then(() => transformer[bodyTransformMethodName](body, request.seq)), - Promise.resolve()); - } - - /** - * Pass the event back through the transformers in reverse. They modify the object in place. - */ - private onAdapterEvent(event: DebugProtocol.Event): void { - // try/catch because this method isn't promise-based like the rest of the class - try { - const reversedTransformers = utils.reversedArr(this._requestTransformers); - reversedTransformers - .filter(transformer => event.event in transformer) - .forEach( - transformer => transformer[event.event](event)); - - // Internal events should not be passed back through DebugProtocol - if (AdapterProxy.INTERNAL_EVENTS.indexOf(event.event) < 0) { - this._eventHandler(event); - } - } catch (e) { - Services.logger().error('Error handling adapter event: ' + (e ? e.stack : '')); - } - } -} diff --git a/src/debug-adapter/adapter/lineNumberTransformer.ts b/src/debug-adapter/adapter/lineNumberTransformer.ts deleted file mode 100644 index cf93b6c..0000000 --- a/src/debug-adapter/adapter/lineNumberTransformer.ts +++ /dev/null @@ -1,49 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -import {DebugProtocol} from 'vscode-debugprotocol'; - -/** - * Converts from 1 based lines on the client side to 0 based lines on the target side - */ -export class LineNumberTransformer implements DebugProtocol.IDebugTransformer { - private _targetLinesStartAt1: boolean; - private _clientLinesStartAt1: boolean; - - constructor(targetLinesStartAt1: boolean) { - this._targetLinesStartAt1 = targetLinesStartAt1; - } - - public initialize(args: DebugProtocol.InitializeRequestArguments): void { - this._clientLinesStartAt1 = args.linesStartAt1; - } - - public setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): void { - args.lines = args.lines.map(line => this.convertClientLineToTarget(line)); - } - - public setBreakpointsResponse(response: DebugProtocol.ISetBreakpointsResponseBody): void { - response.breakpoints.forEach(bp => bp.line = this.convertTargetLineToClient(bp.line)); - } - - public stackTraceResponse(response: DebugProtocol.IStackTraceResponseBody): void { - response.stackFrames.forEach(frame => frame.line = this.convertTargetLineToClient(frame.line)); - } - - private convertClientLineToTarget(line: number): number { - if (this._targetLinesStartAt1) { - return this._clientLinesStartAt1 ? line : line + 1; - } - - return this._clientLinesStartAt1 ? line - 1 : line; - } - - private convertTargetLineToClient(line: number): number { - if (this._targetLinesStartAt1) { - return this._clientLinesStartAt1 ? line : line - 1; - } - - return this._clientLinesStartAt1 ? line + 1 : line; - } -} diff --git a/src/debug-adapter/adapter/pathTransformer.ts b/src/debug-adapter/adapter/pathTransformer.ts deleted file mode 100644 index bedd6ff..0000000 --- a/src/debug-adapter/adapter/pathTransformer.ts +++ /dev/null @@ -1,138 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -import * as utils from '../../common/utilities'; -import {Services} from '../../services/debugAdapterServices'; -import {DebugProtocol} from 'vscode-debugprotocol'; - -interface IPendingBreakpoint { - resolve: () => void; - reject: (e: Error) => void; - args: DebugProtocol.ISetBreakpointsArgs; -} - -/** - * Converts a local path from Code to a path on the target. - */ -export class PathTransformer implements DebugProtocol.IDebugTransformer { - private _appRoot: string; - private _platform: string; - private _clientPathToWebkitUrl = new Map(); - private _webkitUrlToClientPath = new Map(); - private _pendingBreakpointsByPath = new Map(); - private inferedDeviceRoot :string = null; - - public launch(args: DebugProtocol.ILaunchRequestArgs): void { - this._appRoot = args.appRoot; - this._platform = args.platform; - this.inferedDeviceRoot = (this._platform === 'ios') ? 'file://' : this.inferedDeviceRoot; - } - - public attach(args: DebugProtocol.IAttachRequestArgs): void { - this._appRoot = args.appRoot; - this._platform = args.platform; - } - - public setBreakpoints(args: DebugProtocol.ISetBreakpointsArgs): Promise { - return new Promise((resolve, reject) => { - if (!args.source.path) { - resolve(); - return; - } - - if (utils.isURL(args.source.path)) { - // already a url, use as-is - Services.logger().log(`Paths.setBP: ${args.source.path} is already a URL`); - resolve(); - return; - } - - const url = utils.canonicalizeUrl(args.source.path); - if (this._clientPathToWebkitUrl.has(url)) { - args.source.path = this._clientPathToWebkitUrl.get(url); - Services.logger().log(`Paths.setBP: Resolved ${url} to ${args.source.path}`); - resolve(); - } - else if (this.inferedDeviceRoot) { - let inferedUrl = url.replace(this._appRoot, this.inferedDeviceRoot).replace(/\\/g, "/"); - inferedUrl = inferedUrl.replace("/node_modules/", "/app/tns_modules/"); - - //change platform specific paths - inferedUrl = inferedUrl.replace(`.${this._platform}.`, '.'); - - args.source.path = inferedUrl; - Services.logger().log(`Paths.setBP: Resolved (by infering) ${url} to ${args.source.path}`); - resolve(); - } - else { - Services.logger().log(`Paths.setBP: No target url cached for client path: ${url}, waiting for target script to be loaded.`); - args.source.path = url; - this._pendingBreakpointsByPath.set(args.source.path, { resolve, reject, args }); - } - }); - } - - public clearClientContext(): void { - this._pendingBreakpointsByPath = new Map(); - } - - public clearTargetContext(): void { - this._clientPathToWebkitUrl = new Map(); - this._webkitUrlToClientPath = new Map(); - } - - public scriptParsed(event: DebugProtocol.Event): void { - const webkitUrl: string = event.body.scriptUrl; - if (!this.inferedDeviceRoot && this._platform === "android") - { - this.inferedDeviceRoot = utils.inferDeviceRoot(this._appRoot, this._platform, webkitUrl); - if (this.inferedDeviceRoot) - { - Services.logger().log("\n\n\n ***Inferred device root: " + this.inferedDeviceRoot + "\n\n\n"); - - if (this.inferedDeviceRoot.indexOf("/data/user/0/") != -1) - { - this.inferedDeviceRoot = this.inferedDeviceRoot.replace("/data/user/0/", "/data/data/"); - } - } - } - - const clientPath = utils.webkitUrlToClientPath(this._appRoot, this._platform, webkitUrl); - - if (!clientPath) { - Services.logger().log(`Paths.scriptParsed: could not resolve ${webkitUrl} to a file in the workspace. webRoot: ${this._appRoot}`); - } else { - Services.logger().log(`Paths.scriptParsed: resolved ${webkitUrl} to ${clientPath}. webRoot: ${this._appRoot}`); - this._clientPathToWebkitUrl.set(clientPath, webkitUrl); - this._webkitUrlToClientPath.set(webkitUrl, clientPath); - - event.body.scriptUrl = clientPath; - } - - if (this._pendingBreakpointsByPath.has(event.body.scriptUrl)) { - Services.logger().log(`Paths.scriptParsed: Resolving pending breakpoints for ${event.body.scriptUrl}`); - const pendingBreakpoint = this._pendingBreakpointsByPath.get(event.body.scriptUrl); - this._pendingBreakpointsByPath.delete(event.body.scriptUrl); - this.setBreakpoints(pendingBreakpoint.args).then(pendingBreakpoint.resolve, pendingBreakpoint.reject); - } - } - - public stackTraceResponse(response: DebugProtocol.IStackTraceResponseBody): void { - response.stackFrames.forEach(frame => { - // Try to resolve the url to a path in the workspace. If it's not in the workspace, - // just use the script.url as-is. It will be resolved or cleared by the SourceMapTransformer. - if (frame.source && frame.source.path) { - const clientPath = this._webkitUrlToClientPath.has(frame.source.path) ? - this._webkitUrlToClientPath.get(frame.source.path) : - utils.webkitUrlToClientPath(this._appRoot, this._platform, frame.source.path); - // Incoming stackFrames have sourceReference and path set. If the path was resolved to a file in the workspace, - // clear the sourceReference since it's not needed. - if (clientPath) { - frame.source.path = clientPath; - frame.source.sourceReference = 0; - } - } - }); - } -} \ No newline at end of file diff --git a/src/debug-adapter/adapter/sourceMaps/pathUtilities.ts b/src/debug-adapter/adapter/sourceMaps/pathUtilities.ts deleted file mode 100644 index 98280cb..0000000 --- a/src/debug-adapter/adapter/sourceMaps/pathUtilities.ts +++ /dev/null @@ -1,103 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - - /* tslint:disable */ - -import * as Path from 'path'; -import * as URL from 'url'; -import {Services} from '../../../services/debugAdapterServices'; -import * as utils from '../../../common/utilities'; - -export function getPathRoot(p: string) { - if (p) { - if (p.length >= 3 && p[1] === ':' && p[2] === '\\' && ((p[0] >= 'a' && p[0] <= 'z') || (p[0] >= 'A' && p[0] <= 'Z'))) { - return p.substr(0, 3); - } - if (p.length > 0 && p[0] === '/') { - return '/'; - } - } - return null; -} - -export function makePathAbsolute(absPath: string, relPath: string): string { - return Path.resolve(Path.dirname(absPath), relPath); -} - -export function removeFirstSegment(path: string) { - const segments = path.split(Path.sep); - segments.shift(); - if (segments.length > 0) { - return segments.join(Path.sep); - } - return null; -} - -export function makeRelative(target: string, path: string) { - const t = target.split(Path.sep); - const p = path.split(Path.sep); - - let i = 0; - for (; i < Math.min(t.length, p.length) && t[i] === p[i]; i++) { - } - - let result = ''; - for (; i < p.length; i++) { - result = Path.join(result, p[i]); - } - return result; -} - -export function canonicalizeUrl(url: string): string { - let u = URL.parse(url); - let p = u.pathname; - - if (p.length >= 4 && p[0] === '/' && p[2] === ':' && p[3] === '/' && ((p[1] >= 'a' && p[1] <= 'z') || (p[1] >= 'A' && p[1] <= 'Z'))) { - return p.substr(1); - } - return p; -} - -/** - * Determine the absolute path to the sourceRoot. - */ -export function getAbsSourceRoot(sourceRoot: string, webRoot: string, generatedPath: string): string { - let absSourceRoot: string; - if (sourceRoot) { - if (sourceRoot.startsWith('file:///')) { - // sourceRoot points to a local path like "file:///c:/project/src" - absSourceRoot = canonicalizeUrl(sourceRoot); - } else if (Path.isAbsolute(sourceRoot)) { - // sourceRoot is like "/src", would be like http://localhost/src, resolve to a local path under webRoot - // note that C:/src (or /src as an absolute local path) is not a valid sourceroot - absSourceRoot = Path.join(webRoot, sourceRoot); - } else { - // sourceRoot is like "src" or "../src", relative to the script - if (Path.isAbsolute(generatedPath)) { - absSourceRoot = makePathAbsolute(generatedPath, sourceRoot); - } else { - // generatedPath is a URL so runtime script is not on disk, resolve the sourceRoot location on disk - const genDirname = Path.dirname(URL.parse(generatedPath).pathname); - absSourceRoot = Path.join(webRoot, genDirname, sourceRoot); - } - } - - Services.logger().log(`SourceMap: resolved sourceRoot ${sourceRoot} -> ${absSourceRoot}`); - } else { - if (Path.isAbsolute(generatedPath)) { - absSourceRoot = Path.dirname(generatedPath); - Services.logger().log(`SourceMap: no sourceRoot specified, using script dirname: ${absSourceRoot}`); - } else { - // runtime script is not on disk, resolve the sourceRoot location on disk - const scriptPathDirname = Path.dirname(URL.parse(generatedPath).pathname); - absSourceRoot = Path.join(webRoot, scriptPathDirname); - Services.logger().log(`SourceMap: no sourceRoot specified, using webRoot + script path dirname: ${absSourceRoot}`); - } - } - - absSourceRoot = utils.stripTrailingSlash(absSourceRoot); - absSourceRoot = utils.fixDriveLetterAndSlashes(absSourceRoot); - - return absSourceRoot; -} \ No newline at end of file diff --git a/src/debug-adapter/adapter/sourceMaps/sourceMapTransformer.ts b/src/debug-adapter/adapter/sourceMaps/sourceMapTransformer.ts deleted file mode 100644 index 6654eaf..0000000 --- a/src/debug-adapter/adapter/sourceMaps/sourceMapTransformer.ts +++ /dev/null @@ -1,252 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -import * as path from 'path'; -import * as fs from 'fs'; -import {Services} from '../../../services/debugAdapterServices'; -import {DebugProtocol} from 'vscode-debugprotocol'; -import {ISourceMaps, SourceMaps} from './sourceMaps'; -import * as utils from '../../../common/utilities'; - -interface IPendingBreakpoint { - resolve: () => void; - reject: (e: Error) => void; - args: DebugProtocol.ISetBreakpointsArgs; - requestSeq: number; -} - -/** - * If sourcemaps are enabled, converts from source files on the client side to runtime files on the target side - */ -export class SourceMapTransformer implements DebugProtocol.IDebugTransformer { - private _sourceMaps: ISourceMaps; - private _requestSeqToSetBreakpointsArgs: Map; - private _allRuntimeScriptPaths: Set; - private _pendingBreakpointsByPath = new Map(); - private _webRoot: string; - private _authoredPathsToMappedBPLines: Map; - private _authoredPathsToMappedBPCols: Map; - - public launch(args: DebugProtocol.ILaunchRequestArgs): void { - this.init(args); - } - - public attach(args: DebugProtocol.IAttachRequestArgs): void { - this.init(args); - } - - private init(args: DebugProtocol.IRequestArgs): void { - if (args.sourceMaps) { - this._webRoot = args.appRoot; - this._sourceMaps = new SourceMaps(this._webRoot); - this._requestSeqToSetBreakpointsArgs = new Map(); - this._allRuntimeScriptPaths = new Set(); - this._authoredPathsToMappedBPLines = new Map(); - this._authoredPathsToMappedBPCols = new Map(); - } - } - - public clearTargetContext(): void { - this._allRuntimeScriptPaths = new Set(); - } - - /** - * Apply sourcemapping to the setBreakpoints request path/lines - */ - public setBreakpoints(args: DebugProtocol.ISetBreakpointsArgs, requestSeq: number): Promise { - return new Promise((resolve, reject) => { - if (this._sourceMaps && args.source.path && path.extname(args.source.path) !== ".js") { - const argsPath = utils.fixDriveLetterAndSlashes(args.source.path); - const mappedPath = this._sourceMaps.MapPathFromSource(argsPath); - if (mappedPath) { - Services.logger().log(`SourceMaps.setBP: Mapped ${argsPath} to ${mappedPath}`); - args.authoredPath = argsPath; - args.source.path = mappedPath; - - // DebugProtocol doesn't send cols, but they need to be added from sourcemaps - const mappedCols = []; - const mappedLines = args.lines.map((line, i) => { - const mapped = this._sourceMaps.MapFromSource(argsPath, line, /*column=*/0); - if (mapped) { - Services.logger().log(`SourceMaps.setBP: Mapped ${argsPath}:${line}:0 to ${mappedPath}:${mapped.line}:${mapped.column}`); - mappedCols[i] = mapped.column; - return mapped.line; - } else { - Services.logger().log(`SourceMaps.setBP: Mapped ${argsPath} but not line ${line}, column 0`); - mappedCols[i] = 0; - return line; - } - }); - - this._authoredPathsToMappedBPLines.set(argsPath, mappedLines); - this._authoredPathsToMappedBPCols.set(argsPath, mappedCols); - - // Include BPs from other files that map to the same file. Ensure the current file's breakpoints go first - args.lines = mappedLines; - args.cols = mappedCols; - this._sourceMaps.AllMappedSources(mappedPath).forEach(sourcePath => { - if (sourcePath === argsPath) { - return; - } - - const sourceBPLines = this._authoredPathsToMappedBPLines.get(sourcePath); - const sourceBPCols = this._authoredPathsToMappedBPCols.get(sourcePath); - - if (sourceBPLines && sourceBPCols) { - // Don't modify the cached array - args.lines = args.lines.concat(sourceBPLines); - args.cols = args.cols.concat(sourceBPCols); - } - }); - } else if (this._allRuntimeScriptPaths.has(argsPath)) { - // It's a generated file which is loaded - Services.logger().log(`SourceMaps.setBP: SourceMaps are enabled but ${argsPath} is a runtime script`); - } else { - // Source (or generated) file which is not loaded, need to wait - Services.logger().log(`SourceMaps.setBP: ${argsPath} can't be resolved to a loaded script.`); - this._pendingBreakpointsByPath.set(argsPath, { resolve, reject, args, requestSeq }); - return; - } - - this._requestSeqToSetBreakpointsArgs.set(requestSeq, JSON.parse(JSON.stringify(args))); - resolve(); - } else { - resolve(); - } - }); - } - - /** - * Apply sourcemapping back to authored files from the response - */ - public setBreakpointsResponse(response: DebugProtocol.ISetBreakpointsResponseBody, requestSeq: number): void { - if (this._sourceMaps && this._requestSeqToSetBreakpointsArgs.has(requestSeq)) { - const args = this._requestSeqToSetBreakpointsArgs.get(requestSeq); - if (args.authoredPath) { - const sourceBPLines = this._authoredPathsToMappedBPLines.get(args.authoredPath); - if (sourceBPLines) { - // authoredPath is set, so the file was mapped to source. - // Remove breakpoints from files that map to the same file, and map back to source. - response.breakpoints = response.breakpoints.filter((_, i) => i < sourceBPLines.length); - response.breakpoints.forEach((bp, i) => { - const mapped = this._sourceMaps.MapToSource(args.source.path, args.lines[i], args.cols[i]); - if (mapped) { - Services.logger().log(`SourceMaps.setBP: Mapped ${args.source.path}:${bp.line}:${bp.column} to ${mapped.path}:${mapped.line}`); - bp.line = mapped.line; - } else { - Services.logger().log(`SourceMaps.setBP: Can't map ${args.source.path}:${bp.line}:${bp.column}, keeping the line number as-is.`); - } - - this._requestSeqToSetBreakpointsArgs.delete(requestSeq); - }); - } - } - } - - // Cleanup column, which is passed in here in case it's needed for sourcemaps, but isn't actually - // part of the DebugProtocol - response.breakpoints.forEach(bp => { - delete bp.column; - }); - } - - /** - * Apply sourcemapping to the stacktrace response - */ - public stackTraceResponse(response: DebugProtocol.IStackTraceResponseBody): void { - if (this._sourceMaps) { - response.stackFrames.forEach(stackFrame => { - const mapped = this._sourceMaps.MapToSource(stackFrame.source.path, stackFrame.line, stackFrame.column); - if (mapped && utils.existsSync(mapped.path)) { - // Script was mapped to a valid path - stackFrame.source.path = utils.canonicalizeUrl(mapped.path); - stackFrame.source.sourceReference = 0; - stackFrame.source.name = path.basename(mapped.path); - stackFrame.line = mapped.line; - stackFrame.column = mapped.column; - } else if (utils.existsSync(stackFrame.source.path)) { - // Script could not be mapped, but does exist on disk. Keep it and clear the sourceReference. - stackFrame.source.sourceReference = 0; - } else { - // Script could not be mapped and doesn't exist on disk. Clear the path, use sourceReference. - stackFrame.source.path = undefined; - } - }); - } else { - response.stackFrames.forEach(stackFrame => { - // PathTransformer needs to leave the frame in an unfinished state because it doesn't know whether sourcemaps are enabled - if (stackFrame.source.path && stackFrame.source.sourceReference) { - stackFrame.source.path = undefined; - } - }); - } - } - - public scriptParsed(event: DebugProtocol.Event): void { - if (this._sourceMaps) { - this._allRuntimeScriptPaths.add(event.body.scriptUrl); - - let sourceMapUrlValue = event.body.sourceMapURL; - - if (!sourceMapUrlValue) { - sourceMapUrlValue = this._sourceMaps.FindSourceMapUrlInFile(event.body.scriptUrl); - } - - if (!sourceMapUrlValue || sourceMapUrlValue === "") { - this.resolvePendingBreakpoints(event.body.scriptUrl); - return; - } - - this._sourceMaps.ProcessNewSourceMap(event.body.scriptUrl, sourceMapUrlValue).then(() => { - const sources = this._sourceMaps.AllMappedSources(event.body.scriptUrl); - if (sources) { - Services.logger().log(`SourceMaps.scriptParsed: ${event.body.scriptUrl} was just loaded and has mapped sources: ${JSON.stringify(sources)}`); - sources.forEach(this.resolvePendingBreakpoints, this); - } - }); - } - } - - // private getSourceMappingFile(filePathOrSourceMapValue: string): string { - - // let result = filePathOrSourceMapValue; - - // if (!fs.existsSync(filePathOrSourceMapValue)) { - // return result; - // } - - // let fileContents = fs.readFileSync(filePathOrSourceMapValue, 'utf8'); - - // var baseRegex = "\\s*[@#]\\s*sourceMappingURL\\s*=\\s*([^\\s]*)"; - - // // Matches /* ... */ comments - // var blockCommentRegex = new RegExp("/\\*" + baseRegex + "\\s*\\*/"); - - // // Matches // .... comments - // var commentRegex = new RegExp("//" + baseRegex + "($|\n|\r\n?)"); - - // let match = fileContents.match(commentRegex); - // if (!match) { - // match = fileContents.match(blockCommentRegex); - // } - - // if (match) { - // result = match[1]; - // } - - // return result; - // } - - private resolvePendingBreakpoints(sourcePath: string): void { - // If there's a setBreakpoints request waiting on this script, go through setBreakpoints again - if (this._pendingBreakpointsByPath.has(sourcePath)) { - Services.logger().log(`SourceMaps.scriptParsed: Resolving pending breakpoints for ${sourcePath}`); - const pendingBreakpoint = this._pendingBreakpointsByPath.get(sourcePath); - this._pendingBreakpointsByPath.delete(sourcePath); - - this.setBreakpoints(pendingBreakpoint.args, pendingBreakpoint.requestSeq) - .then(pendingBreakpoint.resolve, pendingBreakpoint.reject); - } - } -} diff --git a/src/debug-adapter/adapter/sourceMaps/sourceMaps.ts b/src/debug-adapter/adapter/sourceMaps/sourceMaps.ts deleted file mode 100644 index fdbccc4..0000000 --- a/src/debug-adapter/adapter/sourceMaps/sourceMaps.ts +++ /dev/null @@ -1,563 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - - /* tslint:disable */ - -import * as Path from 'path'; -import * as URL from 'url'; -import * as FS from 'fs'; -import {SourceMapConsumer} from 'source-map'; -import * as PathUtils from './pathUtilities'; -import * as utils from '../../../common/utilities'; -import {Services} from '../../../services/debugAdapterServices'; - - -export interface MappingResult { - path: string; - line: number; - column: number; -} - -export interface ISourceMaps { - /* - * Map source language path to generated path. - * Returns null if not found. - */ - MapPathFromSource(path: string): string; - - /* - * Map location in source language to location in generated code. - * line and column are 0 based. - */ - MapFromSource(path: string, line: number, column: number): MappingResult; - - /* - * Map location in generated code to location in source language. - * line and column are 0 based. - */ - MapToSource(path: string, line: number, column: number): MappingResult; - - /* - * Get all the sources that map to this generated file - */ - AllMappedSources(path: string): string[]; - - /** - * With a known sourceMapURL for a generated script, process create the SourceMap and cache for later - */ - ProcessNewSourceMap(path: string, sourceMapURL: string): Promise; - - FindSourceMapUrlInFile(generatedFilePath: string): string; -} - - -export class SourceMaps implements ISourceMaps { - - public static TRACE = false; - - private static SOURCE_MAPPING_MATCHER = new RegExp("//[#@] ?sourceMappingURL=(.+)$"); - - private _generatedToSourceMaps: { [id: string] : SourceMap; } = {}; // generated -> source file - private _sourceToGeneratedMaps: { [id: string] : SourceMap; } = {}; // source file -> generated - - /* Path to resolve / paths against */ - private _webRoot: string; - - public constructor(webRoot: string) { - this._webRoot = webRoot; - } - - public MapPathFromSource(pathToSource: string): string { - var map = this._findSourceToGeneratedMapping(pathToSource); - if (map) - return map.generatedPath(); - return null; - } - - public MapFromSource(pathToSource: string, line: number, column: number): MappingResult { - const map = this._findSourceToGeneratedMapping(pathToSource); - if (map) { - line += 1; // source map impl is 1 based - const mr = map.generatedPositionFor(pathToSource, line, column, Bias.LEAST_UPPER_BOUND); - if (typeof mr.line === 'number') { - if (SourceMaps.TRACE) console.error(`${Path.basename(pathToSource)} ${line}:${column} -> ${mr.line}:${mr.column}`); - return { path: map.generatedPath(), line: mr.line-1, column: mr.column}; - } - } - return null; - } - - public MapToSource(pathToGenerated: string, line: number, column: number): MappingResult { - const map = this._generatedToSourceMaps[pathToGenerated]; - if (map) { - line += 1; // source map impl is 1 based - const mr = map.originalPositionFor(line, column); - if (mr.source) { - if (SourceMaps.TRACE) console.error(`${Path.basename(pathToGenerated)} ${line}:${column} -> ${mr.line}:${mr.column}`); - return { path: mr.source, line: mr.line-1, column: mr.column}; - } - } - return null; - } - - public AllMappedSources(pathToGenerated: string): string[] { - const map = this._generatedToSourceMaps[pathToGenerated]; - return map ? map.sources : null; - } - - public ProcessNewSourceMap(pathToGenerated: string, sourceMapURL: string): Promise { - return this._findGeneratedToSourceMapping(pathToGenerated, sourceMapURL).then(() => { }); - } - - //---- private ----------------------------------------------------------------------- - - private _findSourceToGeneratedMapping(pathToSource: string): SourceMap { - - if (!pathToSource) { - return null; - } - - if (pathToSource in this._sourceToGeneratedMaps) { - return this._sourceToGeneratedMaps[pathToSource]; - } - - // a reverse lookup: in all source maps try to find pathToSource in the sources array - for (let key in this._generatedToSourceMaps) { - const m = this._generatedToSourceMaps[key]; - if (m.doesOriginateFrom(pathToSource)) { - this._sourceToGeneratedMaps[pathToSource] = m; - return m; - } - } - - //try finding a map file next to the source file - let generatedFilePath = null; - const pos = pathToSource.lastIndexOf('.'); - if (pos >= 0) { - generatedFilePath = pathToSource.substr(0, pos) + '.js'; - } - - if (FS.existsSync(generatedFilePath)) { - let parsedSourceMap = this.findGeneratedToSourceMappingSync(generatedFilePath); - if (parsedSourceMap) { - if (parsedSourceMap.doesOriginateFrom(pathToSource)) { - this._sourceToGeneratedMaps[pathToSource] = parsedSourceMap; - return parsedSourceMap; - } - } - } - - //try finding all js files in app root and parse their source maps - let files = this.walkPath(this._webRoot); - files.forEach(file => { - let parsedSourceMap = this.findGeneratedToSourceMappingSync(file); - if (parsedSourceMap) { - if (parsedSourceMap.doesOriginateFrom(pathToSource)) { - this._sourceToGeneratedMaps[pathToSource] = parsedSourceMap; - return parsedSourceMap; - } - } - }); - - - // let module_files = this.walkPath(Path.join(this._webRoot, "node_modules")); - // module_files.forEach(file => { - // let parsedSourceMap = this.findGeneratedToSourceMappingSync(file); - // if (parsedSourceMap) - // { - // if (parsedSourceMap.doesOriginateFrom(pathToSource)) - // { - // this._sourceToGeneratedMaps[pathToSource] = parsedSourceMap; - // return parsedSourceMap; - // } - // } - // }); - - return null; - // not found in existing maps - } - - /** - * try to find the 'sourceMappingURL' in the file with the given path. - * Returns null in case of errors. - */ - public FindSourceMapUrlInFile(generatedFilePath: string): string { - - try { - const contents = FS.readFileSync(generatedFilePath).toString(); - const lines = contents.split('\n'); - for (let line of lines) { - const matches = SourceMaps.SOURCE_MAPPING_MATCHER.exec(line); - if (matches && matches.length === 2) { - const uri = matches[1].trim(); - Services.logger().log(`_findSourceMapUrlInFile: source map url at end of generated file '${generatedFilePath}''`); - return uri; - } - } - } catch (e) { - // ignore exception - } - return null; - } - - private walkPath(path: string): string[] { - var results = []; - var list = FS.readdirSync(path); - list.forEach(file => { - file = Path.join(path, file); - var stat = FS.statSync(file); - if (stat && stat.isDirectory()) { - results = results.concat(this.walkPath(file)); - } - else { - results.push(file); - } - }); - - return results - } - - // /** - // * Loads source map from file system. - // * If no generatedPath is given, the 'file' attribute of the source map is used. - // */ - // private _loadSourceMap(map_path: string, generatedPath?: string): SourceMap { - - // if (map_path in this._allSourceMaps) { - // return this._allSourceMaps[map_path]; - // } - - // try { - // const mp = Path.join(map_path); - // const contents = FS.readFileSync(mp).toString(); - - // const map = new SourceMap(mp, generatedPath, contents); - // this._allSourceMaps[map_path] = map; - - // this._registerSourceMap(map); - - // Logger.log(`_loadSourceMap: successfully loaded source map '${map_path}'`); - - // return map; - // } - // catch (e) { - // Logger.log(`_loadSourceMap: loading source map '${map_path}' failed with exception: ${e}`); - // } - // return null; - // } - - // private _registerSourceMap(map: SourceMap) { - // const gp = map.generatedPath(); - // if (gp) { - // this._generatedToSourceMaps[gp] = map; - // } - // } - - /** - * pathToGenerated - an absolute local path or a URL. - * mapPath - a path relative to pathToGenerated. - */ - private _findGeneratedToSourceMapping(generatedFilePath: string, mapPath: string): Promise { - if (!generatedFilePath) { - return Promise.resolve(null); - } - - if (generatedFilePath in this._generatedToSourceMaps) { - return Promise.resolve(this._generatedToSourceMaps[generatedFilePath]); - } - - let parsedSourceMap = this.parseInlineSourceMap(mapPath, generatedFilePath); - if (parsedSourceMap) - { - return Promise.resolve(parsedSourceMap); - } - - // if path is relative make it absolute - if (!Path.isAbsolute(mapPath)) { - if (Path.isAbsolute(generatedFilePath)) { - // runtime script is on disk, so map should be too - mapPath = PathUtils.makePathAbsolute(generatedFilePath, mapPath); - } else { - // runtime script is not on disk, construct the full url for the source map - const scriptUrl = URL.parse(generatedFilePath); - mapPath = `${scriptUrl.protocol}//${scriptUrl.host}${Path.dirname(scriptUrl.pathname)}/${mapPath}`; - } - } - - return this._createSourceMap(mapPath, generatedFilePath).then(map => { - if (!map) { - const mapPathNextToSource = generatedFilePath + ".map"; - if (mapPathNextToSource !== mapPath) { - return this._createSourceMap(mapPathNextToSource, generatedFilePath); - } - } - - return map; - }).then(map => { - if (map) { - this._generatedToSourceMaps[generatedFilePath] = map; - } - - return map || null; - }); - } - - - /** - * generatedFilePath - an absolute local path to the generated file - * returns the SourceMap parsed from inlined value or from a map file available next to the generated file - */ - private findGeneratedToSourceMappingSync(generatedFilePath: string): SourceMap { - if (!generatedFilePath) { - return null; - } - - if (generatedFilePath in this._generatedToSourceMaps) { - return this._generatedToSourceMaps[generatedFilePath]; - } - - let sourceMapUrlValue = this.FindSourceMapUrlInFile(generatedFilePath); - if (!sourceMapUrlValue) - { - return null; - } - - let parsedSourceMap = this.parseInlineSourceMap(sourceMapUrlValue, generatedFilePath); - if (parsedSourceMap) { - return parsedSourceMap; - } - - if (!FS.existsSync(generatedFilePath)) { - Services.logger().log("findGeneratedToSourceMappingSync: can't find the sourceMapping for file: " + generatedFilePath); - return null; - } - - // if path is relative make it absolute - if (!Path.isAbsolute(sourceMapUrlValue)) { - if (Path.isAbsolute(generatedFilePath)) { - // runtime script is on disk, so map should be too - sourceMapUrlValue = PathUtils.makePathAbsolute(generatedFilePath, sourceMapUrlValue); - } else { - // runtime script is not on disk, construct the full url for the source map - // const scriptUrl = URL.parse(generatedFilePath); - // mapPath = `${scriptUrl.protocol}//${scriptUrl.host}${Path.dirname(scriptUrl.pathname)}/${mapPath}`; - - return null; - } - } - - let map = this._createSourceMapSync(sourceMapUrlValue, generatedFilePath); - if (!map) { - const mapPathNextToSource = generatedFilePath + ".map"; - if (mapPathNextToSource !== sourceMapUrlValue) { - map = this._createSourceMapSync(mapPathNextToSource, generatedFilePath); - } - } - - if (map) { - this._generatedToSourceMaps[generatedFilePath] = map; - return map; - } - - return null; - } - - private parseInlineSourceMap(sourceMapContents: string, generatedFilePath: string) : SourceMap - { - if (sourceMapContents.indexOf("data:application/json;base64,") >= 0) { - // sourcemap is inlined - const pos = sourceMapContents.indexOf(','); - const data = sourceMapContents.substr(pos+1); - try { - const buffer = new Buffer(data, 'base64'); - const json = buffer.toString(); - if (json) { - const map = new SourceMap(generatedFilePath, json, this._webRoot); - this._generatedToSourceMaps[generatedFilePath] = map; - return map; - } - } - catch (e) { - Services.logger().log(`can't parse inlince sourcemap. exception while processing data url (${e.stack})`); - } - } - - return null; - } - - private _createSourceMap(mapPath: string, pathToGenerated: string): Promise { - let contentsP: Promise; - if (utils.isURL(mapPath)) { - contentsP = utils.getURL(mapPath).catch(e => { - Services.logger().log(`SourceMaps.createSourceMap: Could not download map from ${mapPath}`); - return null; - }); - } else { - contentsP = new Promise((resolve, reject) => { - FS.readFile(mapPath, 'utf8', (err, data) => { - if (err) { - Services.logger().log(`SourceMaps.createSourceMap: Could not read map from ${mapPath}`); - resolve(null); - } else { - resolve(data); - } - }); - }); - } - - return contentsP.then(contents => { - if (contents) { - try { - // Throws for invalid contents JSON - return new SourceMap(pathToGenerated, contents, this._webRoot); - } catch (e) { - Services.logger().log(`SourceMaps.createSourceMap: exception while processing sourcemap: ${e.stack}`); - return null; - } - } else { - return null; - } - }); - } - - private _createSourceMapSync(mapPath: string, pathToGenerated: string): SourceMap { - let contents = FS.readFileSync(mapPath, 'utf8'); - try { - // Throws for invalid contents JSON - return new SourceMap(pathToGenerated, contents, this._webRoot); - } catch (e) { - Services.logger().log(`SourceMaps.createSourceMap: exception while processing sourcemap: ${e.stack}`); - return null; - } - } -} - -enum Bias { - GREATEST_LOWER_BOUND = 1, - LEAST_UPPER_BOUND = 2 -} - -class SourceMap { - private _generatedPath: string; // the generated file for this sourcemap - private _sources: string[]; // the sources of generated file (relative to sourceRoot) - private _absSourceRoot: string; // the common prefix for the source (can be a URL) - private _smc: SourceMapConsumer; // the source map - private _webRoot: string; // if the sourceRoot starts with /, it's resolved from this absolute path - private _sourcesAreURLs: boolean; // if sources are specified with file:/// - - /** - * pathToGenerated - an absolute local path or a URL - * json - sourcemap contents - * webRoot - an absolute path - */ - public constructor(generatedPath: string, json: string, webRoot: string) { - Services.logger().log(`SourceMap: creating SM for ${generatedPath}`) - this._generatedPath = generatedPath; - this._webRoot = webRoot; - - const sm = JSON.parse(json); - this._absSourceRoot = PathUtils.getAbsSourceRoot(sm.sourceRoot, this._webRoot, this._generatedPath); - - // Overwrite the sourcemap's sourceRoot with the version that's resolved to an absolute path, - // so the work above only has to be done once - if (this._absSourceRoot.startsWith('/')) { - // OSX paths - sm.sourceRoot = 'file://' + this._absSourceRoot; - } else { - // Windows paths - sm.sourceRoot = 'file:///' + this._absSourceRoot; - } - - sm.sources = sm.sources.map((sourcePath: string) => { - // special-case webpack:/// prefixed sources which is kind of meaningless - sourcePath = utils.lstrip(sourcePath, 'webpack:///'); - - // Force correct format for sanity - return utils.fixDriveLetterAndSlashes(sourcePath); - }); - - this._smc = new SourceMapConsumer(sm); - - // rewrite sources as absolute paths - this._sources = sm.sources.map((sourcePath: string) => { - if (sourcePath.startsWith('file:///')) { - // If one source is a URL, assume all are - this._sourcesAreURLs = true; - } - - sourcePath = utils.lstrip(sourcePath, 'webpack:///'); - sourcePath = PathUtils.canonicalizeUrl(sourcePath); - if (Path.isAbsolute(sourcePath)) { - return utils.fixDriveLetterAndSlashes(sourcePath); - } else { - return Path.join(this._absSourceRoot, sourcePath); - } - }); - } - - /* - * Return all mapped sources as absolute paths - */ - public get sources(): string[] { - return this._sources; - } - - /* - * the generated file of this source map. - */ - public generatedPath(): string { - return this._generatedPath; - } - - /* - * returns true if this source map originates from the given source. - */ - public doesOriginateFrom(absPath: string): boolean { - return this.sources.some(path => path === absPath); - } - - /* - * finds the nearest source location for the given location in the generated file. - */ - public originalPositionFor(line: number, column: number, bias: Bias = Bias.GREATEST_LOWER_BOUND): sourceMap.MappedPosition { - - const mp = this._smc.originalPositionFor({ - line: line, - column: column, - bias: bias - }); - - if (mp.source) { - mp.source = PathUtils.canonicalizeUrl(mp.source); - } - - return mp; - } - - /* - * finds the nearest location in the generated file for the given source location. - */ - public generatedPositionFor(src: string, line: number, column: number, bias = Bias.GREATEST_LOWER_BOUND): sourceMap.Position { - if (this._sourcesAreURLs) { - src = 'file:///' + src; - } else if (this._absSourceRoot) { - // make input path relative to sourceRoot - src = Path.relative(this._absSourceRoot, src); - - // source-maps use forward slashes unless the source is specified with file:/// - if (process.platform === 'win32') { - src = src.replace(/\\/g, '/'); - } - } - - const needle = { - source: src, - line: line, - column: column, - bias: bias - }; - - return this._smc.generatedPositionFor(needle); - } -} diff --git a/src/debug-adapter/connection/INSDebugConnection.ts b/src/debug-adapter/connection/INSDebugConnection.ts deleted file mode 100644 index 85d67c4..0000000 --- a/src/debug-adapter/connection/INSDebugConnection.ts +++ /dev/null @@ -1,34 +0,0 @@ -export interface INSDebugConnection { - - attach(target: number | string, url?: string): Promise - - enable() : Promise; - - on(eventName: string, handler: (msg: any) => void): void; - - close(): void; - - debugger_setBreakpointByUrl(url: string, lineNumber: number, columnNumber: number, condition: string, ignoreCount: number): Promise - - debugger_removeBreakpoint(breakpointId: string): Promise - - debugger_stepOver(): Promise; - - debugger_stepIn(): Promise; - - debugger_stepOut(): Promise; - - debugger_resume(): Promise; - - debugger_pause(): Promise; - - debugger_evaluateOnCallFrame(callFrameId: string, expression: string, objectGroup?, returnByValue?: boolean): Promise; - - debugger_setPauseOnExceptions(state: string): Promise; - - debugger_getScriptSource(scriptId: WebKitProtocol.Debugger.ScriptId): Promise; - - runtime_getProperties(objectId: string, ownProperties: boolean, accessorPropertiesOnly: boolean): Promise; - - runtime_evaluate(expression: string, objectGroup?: any, contextId?: number, returnByValue?: boolean): Promise; -} \ No newline at end of file diff --git a/src/debug-adapter/connection/androidConnection.ts b/src/debug-adapter/connection/androidConnection.ts deleted file mode 100644 index 8cbc254..0000000 --- a/src/debug-adapter/connection/androidConnection.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Services } from '../../services/debugAdapterServices'; -import { INSDebugConnection } from './INSDebugConnection'; -import { ChromeConnection } from 'vscode-chrome-debug-core'; -import Crdp from 'vscode-chrome-debug-core/lib/crdp/crdp'; - -export class AndroidConnection implements INSDebugConnection { - private _chromeConnection: ChromeConnection; - - constructor() { - this._chromeConnection = new ChromeConnection((address: string, port: number, targetFilter?: any, targetUrl?: string): Promise => Promise.resolve(`ws://${address}:${port}`)); - } - - private get api(): Crdp.CrdpClient { - return this._chromeConnection.api; - } - - public on(eventName: string, handler: (msg: any) => void): void { - let domainMethodPair = eventName.split("."); - if (domainMethodPair.length == 2) - { - let domain = domainMethodPair[0]; - let method = domainMethodPair[1]; - method = "on" + method.charAt(0).toUpperCase() + method.slice(1); - this.api[domain][method](handler); - } - else - { - ((this._chromeConnection))._socket.on(eventName, handler); - } - } - - public attach(port: number, url?: string): Promise { - Services.logger().log('Attempting to attach on port ' + port); - return this._chromeConnection.attach(url, port); - } - - public enable(): Promise { - return this.api.Debugger.enable(); - } - - public close(): void { - this._chromeConnection.close(); - } - - public debugger_setBreakpointByUrl(url: string, lineNumber: number, columnNumber: number, condition: string, ignoreCount: number): Promise { - return this.api.Debugger.setBreakpointByUrl({ urlRegex: url, lineNumber: lineNumber, columnNumber: columnNumber, condition }) - .then(response => - { - return - { - result: { - breakpointId: response.breakpointId.toString(), - locations: response.locations - }, - } - }); - } - - public debugger_removeBreakpoint(breakpointId: string): Promise { - return this.api.Debugger.removeBreakpoint({ breakpointId }).then(response => { - return {}; - }); - } - - public debugger_stepOver(): Promise { - return this.api.Debugger.stepOver().then(reponse => { - return {}; - }); - } - - public debugger_stepIn(): Promise { - return this.api.Debugger.stepInto().then(reponse => { - return {}; - }); - } - - public debugger_stepOut(): Promise { - return this.api.Debugger.stepOut().then(reponse => { - return {}; - }); - } - - public debugger_resume(): Promise { - return this.api.Debugger.resume().then(reponse => { - return {}; - }); - } - - public debugger_pause(): Promise { - return this.api.Debugger.pause().then(reponse => { - return {}; - }); - } - - public debugger_evaluateOnCallFrame(callFrameId: string, expression: string, objectGroup = 'dummyObjectGroup', returnByValue?: boolean): Promise { - return this.api.Debugger.evaluateOnCallFrame({ callFrameId, expression, silent: true, generatePreview: true }).then(response => { - return { - result: { - result: response.result, - wasThrown: false - } - } - }); - } - - public debugger_setPauseOnExceptions(args: string): Promise { - let state: 'all' | 'uncaught' | 'none'; - if (args.indexOf('all') >= 0) { - state = 'all'; - } else if (args.indexOf('uncaught') >= 0) { - state = 'uncaught'; - } else { - state = 'none'; - } - - return this.api.Debugger.setPauseOnExceptions({ state }) - .then(reponse => { - return {} ; - }); - } - - public debugger_getScriptSource(scriptId: WebKitProtocol.Debugger.ScriptId): Promise { - return this.api.Debugger.getScriptSource({ scriptId: scriptId }).then(response => - { - return - { - result: { - scriptSource: response.scriptSource - } - } - }); - } - - public runtime_getProperties(objectId: string, ownProperties: boolean, accessorPropertiesOnly: boolean): Promise { - return this.api.Runtime.getProperties({objectId, ownProperties, accessorPropertiesOnly, generatePreview: true}).then(response => { - return { - result: { - result: response.result - } - }}); - } - - public runtime_evaluate(expression: string, objectGroup = 'dummyObjectGroup', contextId?: number, returnByValue = false): Promise { - return this.api.Runtime.evaluate({expression, objectGroup, contextId, returnByValue}).then(response => { - return { - result: { - result: response.result - } - }}); - } -} diff --git a/src/debug-adapter/connection/iosConnection.ts b/src/debug-adapter/connection/iosConnection.ts deleted file mode 100644 index 76b92c2..0000000 --- a/src/debug-adapter/connection/iosConnection.ts +++ /dev/null @@ -1,265 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -import * as net from 'net'; -import * as stream from 'stream'; -import {EventEmitter} from 'events'; -import {INSDebugConnection} from './INSDebugConnection'; -import * as utils from '../../common/utilities'; -import {Services} from '../../services/debugAdapterServices'; - -interface IMessageWithId { - id: number; - method: string; - params?: any; -} - -export class PacketStream extends stream.Transform { - private buffer: Buffer; - private offset: number; - - constructor(opts?: stream.TransformOptions) { - super(opts); - } - - public _transform(packet: any, encoding: string, done: Function): void { - while (packet.length > 0) { - if (!this.buffer) { - // read length - let length = packet.readInt32BE(0); - this.buffer = new Buffer(length); - this.offset = 0; - packet = packet.slice(4); - } - - packet.copy(this.buffer, this.offset); - let copied = Math.min(this.buffer.length - this.offset, packet.length); - this.offset += copied; - packet = packet.slice(copied); - - if (this.offset === this.buffer.length) { - this.push(this.buffer); - this.buffer = undefined; - } - } - done(); - } -} - -/** - * Implements a Request/Response API on top of a Unix domain socket for messages that are marked with an `id` property. - * Emits `message.method` for messages that don't have `id`. - */ -class ResReqTcpSocket extends EventEmitter { - private _pendingRequests = new Map(); - private _unixSocketAttached: Promise; - - /** - * Attach to the given filePath - */ - public attach(filePath: string): Promise { - this._unixSocketAttached = new Promise((resolve, reject) => { - - let unixSocket: net.Socket; - try { - unixSocket = net.createConnection(filePath); - unixSocket.on('connect', () => { - resolve(unixSocket); - }); - - unixSocket.on('error', (e) => { - reject(e); - }); - - unixSocket.on('close', () => { - Services.logger().log('Unix socket closed'); - this.emit('close'); - }); - - let packetsStream = new PacketStream(); - unixSocket.pipe(packetsStream); - - packetsStream.on('data', (buffer: Buffer) => { - let packet = buffer.toString('utf16le'); - Services.logger().log('From target: ' + packet); - this.onMessage(JSON.parse(packet)); - }); - } catch (e) { - // invalid url e.g. - reject(e.message); - return; - } - }); - - return >this._unixSocketAttached; - } - - public close(): void { - if (this._unixSocketAttached) { - this._unixSocketAttached.then(socket => socket.destroy()); - } - } - - /** - * Send a message which must have an id. Ok to call immediately after attach. Messages will be queued until - * the websocket actually attaches. - */ - public sendMessage(message: IMessageWithId): Promise { - return new Promise((resolve, reject) => { - this._pendingRequests.set(message.id, resolve); - this._unixSocketAttached.then(socket => { - const msgStr = JSON.stringify(message); - Services.logger().log('To target: ' + msgStr); - let encoding = "utf16le"; - let length = Buffer.byteLength(msgStr, encoding); - let payload = new Buffer(length + 4); - payload.writeInt32BE(length, 0); - payload.write(msgStr, 4, length, encoding); - socket.write(payload); - }); - }); - } - - private onMessage(message: any): void { - if (message.id) { - if (this._pendingRequests.has(message.id)) { - // Resolve the pending request with this response - this._pendingRequests.get(message.id)(message); - this._pendingRequests.delete(message.id); - } else { - console.error(`Got a response with id ${message.id} for which there is no pending request, weird.`); - } - } else if (message.method) { - this.emit(message.method, message.params); - } - } -} - -/** - * Connects to a target supporting the webkit protocol and sends and receives messages - */ -export class IosConnection implements INSDebugConnection { - private _nextId = 1; - private _socket: ResReqTcpSocket; - - constructor() { - this._socket = new ResReqTcpSocket(); - } - - public on(eventName: string, eventHandler: (eventParams: any) => void): void { - this._socket.on(eventName, eventHandler); - } - - /** - * Attach the underlying Unix socket - */ - public attach(filePath: string): Promise { - Services.logger().log('Attempting to attach to path ' + filePath); - return utils.retryAsync(() => this._attach(filePath), 6000) - .then(() => { - Promise.all([ - this.sendMessage('Debugger.enable'), - this.sendMessage('Console.enable'), - this.sendMessage('Debugger.setBreakpointsActive', {active: true}) - ]); - }); - } - - public _attach(filePath: string): Promise { - return this._socket.attach(filePath); - } - - public enable() : Promise { - return Promise.resolve(); - } - - public close(): void { - this._socket.close(); - } - - public debugger_setBreakpoint(location: WebKitProtocol.Debugger.Location, condition?: string): Promise { - return >(this.sendMessage('Debugger.setBreakpoint', { - location, - options: { condition } - })); - } - - public debugger_setBreakpointByUrl(url: string, lineNumber: number, columnNumber: number, condition: string, ignoreCount: number): Promise { - return >(this.sendMessage('Debugger.setBreakpointByUrl', { - url, - lineNumber, - columnNumber: 0 /* a columnNumber different from 0 confuses the debugger */, - options: { - condition, - ignoreCount - } - })); - } - - public debugger_removeBreakpoint(breakpointId: string): Promise { - return this.sendMessage('Debugger.removeBreakpoint', { breakpointId }); - } - - public debugger_stepOver(): Promise { - return this.sendMessage('Debugger.stepOver'); - } - - public debugger_stepIn(): Promise { - return this.sendMessage('Debugger.stepInto'); - } - - public debugger_stepOut(): Promise { - return this.sendMessage('Debugger.stepOut'); - } - - public debugger_resume(): Promise { - return this.sendMessage('Debugger.resume'); - } - - public debugger_pause(): Promise { - return this.sendMessage('Debugger.pause'); - } - - public debugger_evaluateOnCallFrame(callFrameId: string, expression: string, objectGroup = 'dummyObjectGroup', returnByValue?: boolean): Promise { - return >(this.sendMessage('Debugger.evaluateOnCallFrame', { - callFrameId, - expression, - objectGroup, - returnByValue - })); - } - - public debugger_setPauseOnExceptions(state: string): Promise { - return this.sendMessage('Debugger.setPauseOnExceptions', { state }); - } - - public debugger_getScriptSource(scriptId: WebKitProtocol.Debugger.ScriptId): Promise { - return >(this.sendMessage('Debugger.getScriptSource', { scriptId })); - } - - public runtime_getProperties(objectId: string, ownProperties: boolean, accessorPropertiesOnly: boolean): Promise { - return >(this.sendMessage('Runtime.getProperties', { - objectId, - ownProperties, - accessorPropertiesOnly - })); - } - - public runtime_evaluate(expression: string, objectGroup = 'dummyObjectGroup', contextId?: number, returnByValue = false): Promise { - return >(this.sendMessage('Runtime.evaluate', { - expression, - objectGroup, - contextId, - returnByValue - })); - } - - private sendMessage(method: string, params?: any): Promise { - return this._socket.sendMessage({ - id: this._nextId++, - method: method, - params: params - }); - } -} diff --git a/src/debug-adapter/consoleHelper.ts b/src/debug-adapter/consoleHelper.ts deleted file mode 100644 index 1adfd4b..0000000 --- a/src/debug-adapter/consoleHelper.ts +++ /dev/null @@ -1,148 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -import * as url from 'url'; -import * as Utilities from '../common/utilities'; - -export function formatConsoleMessage(m: WebKitProtocol.Console.Message, isClientPath :boolean = false): { text: string, isError: boolean } { - let outputText: string; - if (m.type === 'log') { - outputText = resolveParams(m); - if (m.source === 'network') { - outputText += ` (${m.url})`; - } - else if (m.source === 'console-api' && m.url) { - let fileName = m.url; - if (!isClientPath) { - const fileName = url.parse(m.url).pathname; - } - - const output = `${fileName}:${m.line}:${m.column}`; - outputText += ` (${output})`; - return { text: outputText, isError: m.level === 'error' }; - } - } else if (m.type === 'assert') { - outputText = 'Assertion failed'; - if (m.parameters && m.parameters.length) { - outputText += ': ' + m.parameters.map(p => p.value).join(' '); - } - - outputText += '\n' + stackTraceToString(m.stackTrace); - } else if (m.type === 'startGroup' || m.type === 'startGroupCollapsed') { - outputText = '‹Start group›'; - if (m.text) { - // Or wherever the label is - outputText += ': ' + m.text; - } - } else if (m.type === 'endGroup') { - outputText = '‹End group›'; - } else if (m.type === 'trace') { - outputText = 'console.trace()\n' + stackTraceToString(m.stackTrace); - } else { - // Some types we have to ignore - outputText = 'Unimplemented console API: ' + m.type; - } - - return { text: outputText, isError: m.level === 'error' }; -} - -function resolveParams(m: WebKitProtocol.Console.Message): string { - if (!m.parameters || !m.parameters.length) { - return m.text; - } - - const textParam = m.parameters[0]; - let text = remoteObjectToString(textParam); - m.parameters.shift(); - - // Find all %s, %i, etc in the first parameter, which is always the main text. Strip % - let formatSpecifiers: string[] = []; - if (textParam.type === 'string') { - formatSpecifiers = textParam.value.match(/\%[sidfoOc]/g) || []; - formatSpecifiers = formatSpecifiers.map(spec => spec[1]); - } - - // Append all parameters, formatting properly if there's a format specifier - m.parameters.forEach((param, i) => { - let formatted: any; - if (formatSpecifiers[i] === 's') { - formatted = param.value; - } else if (['i', 'd'].indexOf(formatSpecifiers[i]) >= 0) { - formatted = Math.floor(+param.value); - } else if (formatSpecifiers[i] === 'f') { - formatted = +param.value; - } else if (['o', 'O', 'c'].indexOf(formatSpecifiers[i]) >= 0) { - // um - formatted = param.value; - } - - // If this param had a format specifier, search and replace it with the formatted param. - // Otherwise, append it to the end of the text - if (formatSpecifiers[i]) { - text = text.replace('%' + formatSpecifiers[i], formatted); - } else { - text += ' ' + remoteObjectToString(param); - } - }); - - return text; -} - -function remoteObjectToString(obj: WebKitProtocol.Runtime.RemoteObject): string { - const result = Utilities.remoteObjectToValue(obj, /*stringify=*/false); - if (result.variableHandleRef) { - // The DebugProtocol console API doesn't support returning a variable reference, so do our best to - // build a useful string out of this object. - if (obj.subtype === 'array') { - return arrayRemoteObjToString(obj); - } else if (obj.preview && obj.preview.properties) { - let props: string = obj.preview.properties - .map(prop => { - let propStr = prop.name + ': '; - if (prop.type === 'string') { - propStr += `"${prop.value}"`; - } else { - propStr += prop.value; - } - - return propStr; - }) - .join(', '); - - if (obj.preview.overflow) { - props += '…'; - } - - return `${obj.className} {${props}}`; - } - } else { - return result.value; - } -} - -function arrayRemoteObjToString(obj: WebKitProtocol.Runtime.RemoteObject): string { - if (obj.preview && obj.preview.properties) { - let props: string = obj.preview.properties - .map(prop => prop.value) - .join(', '); - - if (obj.preview.overflow) { - props += '…'; - } - - return `[${props}]`; - } else { - return obj.description; - } -} - -function stackTraceToString(stackTrace: WebKitProtocol.Console.StackTrace): string { - return stackTrace - .map(frame => { - const fnName = frame.functionName || (frame.url ? '(anonymous)' : '(eval)'); - const fileName = frame.url ? url.parse(frame.url).pathname : '(eval)'; - return ` ${fnName} @${fileName}:${frame.lineNumber}`; - }) - .join('\n'); -} diff --git a/src/debug-adapter/debugRequest.ts b/src/debug-adapter/debugRequest.ts deleted file mode 100644 index 67d1d2b..0000000 --- a/src/debug-adapter/debugRequest.ts +++ /dev/null @@ -1,56 +0,0 @@ -import {DebugProtocol} from 'vscode-debugprotocol'; -import {Project} from '../project/project'; -import {IosProject} from '../project/iosProject'; -import {AndroidProject} from '../project/androidProject'; -import {DebugAdapterServices as Services} from '../services/debugAdapterServices'; -import {NativeScriptCli} from '../project/nativeScriptCli'; - -export class DebugRequest { - private _requestArgs: DebugProtocol.IRequestArgs; - private _project: Project; - - constructor(requestArgs: DebugProtocol.IRequestArgs, cli: NativeScriptCli) { - this._requestArgs = requestArgs; - this._project = this.isIos ? new IosProject(this.args.appRoot, cli) : new AndroidProject(this.args.appRoot, cli); - } - - public get isLaunch(): boolean { - return this.args.request === "launch"; - } - - public get isAttach(): boolean { - return this.args.request === "attach"; - } - - public get isAndroid(): boolean { - return this.args.platform == "android"; - } - - public get isIos(): boolean { - return this.args.platform == "ios"; - } - - public get args(): DebugProtocol.IRequestArgs { - return this._requestArgs; - } - - public get launchArgs(): DebugProtocol.ILaunchRequestArgs { - return this.isLaunch ? this.args : null; - } - - public get attachArgs(): DebugProtocol.IAttachRequestArgs { - return this.isAttach ? this.args : null; - } - - public get project(): Project { - return this._project; - } - - public get iosProject(): IosProject { - return this.isIos ? this.project : null; - } - - public get androidProject(): AndroidProject { - return this.isAndroid ? this.project : null; - } -} diff --git a/src/debug-adapter/nativeScriptDebug.ts b/src/debug-adapter/nativeScriptDebug.ts new file mode 100644 index 0000000..c229d87 --- /dev/null +++ b/src/debug-adapter/nativeScriptDebug.ts @@ -0,0 +1,24 @@ +import * as os from 'os'; +import * as path from 'path'; +import { chromeConnection, ChromeDebugSession } from 'vscode-chrome-debug-core'; +import { AndroidProject } from '../project/androidProject'; +import { IosProject } from '../project/iosProject'; +import { NativeScriptCli } from '../project/nativeScriptCli'; +import { nativeScriptDebugAdapterGenerator } from './nativeScriptDebugAdapter'; +import { NativeScriptPathTransformer } from './nativeScriptPathTransformer'; +import { NativeScriptTargetDiscovery } from './nativeScriptTargetDiscovery'; + +class NSAndroidConnection extends chromeConnection.ChromeConnection { + constructor() { + super(new NativeScriptTargetDiscovery()); + } +} + +ChromeDebugSession.run(ChromeDebugSession.getSession( + { + adapter: nativeScriptDebugAdapterGenerator(IosProject, AndroidProject, NativeScriptCli), + chromeConnection: NSAndroidConnection, + extensionName: 'nativescript-extension', + logFilePath: path.join(os.tmpdir(), 'nativescript-extension.txt'), + pathTransformer: NativeScriptPathTransformer, + })); diff --git a/src/debug-adapter/nativeScriptDebugAdapter.ts b/src/debug-adapter/nativeScriptDebugAdapter.ts new file mode 100644 index 0000000..6cc1196 --- /dev/null +++ b/src/debug-adapter/nativeScriptDebugAdapter.ts @@ -0,0 +1,209 @@ +import { ChildProcess } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { ChromeDebugAdapter, logger } from 'vscode-chrome-debug-core'; +import { Event, OutputEvent, TerminatedEvent } from 'vscode-debugadapter'; +import * as extProtocol from '../common/extensionProtocol'; +import * as utils from '../common/utilities'; +import { AndroidProject } from '../project/androidProject'; +import { IosProject } from '../project/iosProject'; +import { NativeScriptCli } from '../project/nativeScriptCli'; +import { IDebugResult } from '../project/project'; + +export function nativeScriptDebugAdapterGenerator(iosProject: typeof IosProject, + androidProject: typeof AndroidProject, + nativeScriptCli: typeof NativeScriptCli) { + return class NativeScriptDebugAdapter extends ChromeDebugAdapter { + private _tnsProcess: ChildProcess; + private _idCounter = 0; + private _pendingRequests: object = {}; + + public attach(args: any): Promise { + return this.processRequestAndAttach(args); + } + + public launch(args: any): Promise { + return this.processRequestAndAttach(args); + } + + public disconnect(args: any): void { + super.disconnect(args); + + if (this._tnsProcess) { + this._tnsProcess.stdout.removeAllListeners(); + this._tnsProcess.stderr.removeAllListeners(); + this._tnsProcess.removeAllListeners(); + utils.killProcess(this._tnsProcess); + } + } + + public onExtensionResponse(response) { + this._pendingRequests[response.requestId](response.result); + delete this._pendingRequests[response.requestId]; + } + + private async processRequestAndAttach(args: any) { + const transformedArgs = await this.processRequest(args); + + (this.pathTransformer as any).setTargetPlatform(args.platform); + (ChromeDebugAdapter as any).SET_BREAKPOINTS_TIMEOUT = 20000; + + return super.attach(transformedArgs); + } + + private async processRequest(args: any): Promise { + args = this.translateArgs(args); + + this._session.sendEvent(new Event(extProtocol.BEFORE_DEBUG_START)); + + const tnsPath = await this.callRemoteMethod('workspaceConfigService', 'tnsPath'); + const cli = new nativeScriptCli(tnsPath, logger); + + const project = args.platform === 'ios' ? + new iosProject(args.appRoot, cli) : + new androidProject(args.appRoot, cli); + + this.callRemoteMethod('analyticsService', 'launchDebugger', args.request, args.platform); + + // Run CLI Command + const version = project.cli.executeGetVersion(); + + this.log(`[NSDebugAdapter] Using tns CLI v${version} on path '${project.cli.path}'\n`); + this.log('[NSDebugAdapter] Running tns command...\n'); + let cliCommand: IDebugResult; + + if (args.request === 'launch') { + let tnsArgs = args.tnsArgs; + + // For iOS the TeamID is required if there's more than one. + // Therefore if not set, show selection to the user. + if (args.platform && args.platform.toLowerCase() === 'ios') { + const teamId = this.getTeamId(path.join(args.appRoot, 'app'), tnsArgs); + + if (!teamId) { + const selectedTeam = await this.callRemoteMethod<{ id: string, name: string }>('iOSTeamService', 'selectTeam'); + + if (selectedTeam) { + // add the selected by the user Team Id + tnsArgs = (tnsArgs || []).concat(['--teamId', selectedTeam.id]); + this.log(`[NSDebugAdapter] Using iOS Team ID '${selectedTeam.id}', you can change this in the workspace settings.\n`); + } + } + } + + cliCommand = project.debug({ stopOnEntry: args.stopOnEntry, watch: args.watch }, tnsArgs); + } else if (args.request === 'attach') { + cliCommand = project.attach(args.tnsArgs); + } + + if (cliCommand.tnsProcess) { + this._tnsProcess = cliCommand.tnsProcess; + cliCommand.tnsProcess.stdout.on('data', (data) => { this.log(data.toString()); }); + cliCommand.tnsProcess.stderr.on('data', (data) => { this.log(data.toString()); }); + + cliCommand.tnsProcess.on('close', (code, signal) => { + this.log(`[NSDebugAdapter] The tns command finished its execution with code ${code}.\n`); + + // Sometimes we execute "tns debug android --start" and the process finishes + // which is totally fine. If there's an error we need to Terminate the session. + if (code !== 0) { + this.log(`The tns command finished its execution with code ${code}`); + this._session.sendEvent(new TerminatedEvent()); + } + }); + } + + this.log('[NSDebugAdapter] Watching the tns CLI output to receive a connection token\n'); + + return new Promise ((res, rej) => { + cliCommand.tnsOutputEventEmitter.on('readyForConnection', (connectionToken: string | number) => { + this.log(`[NSDebugAdapter] Ready to attach to application on ${connectionToken}\n`); + args.port = connectionToken; + + res(args); + }); + }); + } + + private translateArgs(args): any { + if (args.diagnosticLogging) { + args.trace = args.diagnosticLogging; + } + + if (args.appRoot) { + args.webRoot = args.appRoot; + } + + return args; + } + + private log(text: string): void { + this._session.sendEvent(new OutputEvent(text)); + } + + private getTeamId(appRoot: string, tnsArgs?: string[]): string { + // try to get the TeamId from the TnsArgs + if (tnsArgs) { + const teamIdArgIndex = tnsArgs.indexOf('--teamId'); + + if (teamIdArgIndex > 0 && teamIdArgIndex + 1 < tnsArgs.length) { + return tnsArgs[ teamIdArgIndex + 1 ]; + } + } + + // try to get the TeamId from the buildxcconfig or teamid file + const teamIdFromConfig = this.readTeamId(appRoot); + + if (teamIdFromConfig) { + return teamIdFromConfig; + } + + // we should get the Teams from the machine and ask the user if they are more than 1 + return null; + } + + private readXCConfig(appRoot: string, flag: string): string { + const xcconfigFile = path.join(appRoot, 'App_Resources/iOS/build.xcconfig'); + + if (fs.existsSync(xcconfigFile)) { + const text = fs.readFileSync(xcconfigFile, { encoding: 'utf8'}); + let teamId: string; + + text.split(/\r?\n/).forEach((line) => { + line = line.replace(/\/(\/)[^\n]*$/, ''); + if (line.indexOf(flag) >= 0) { + teamId = line.split('=')[1].trim(); + if (teamId[teamId.length - 1] === ';') { + teamId = teamId.slice(0, -1); + } + } + }); + if (teamId) { + return teamId; + } + } + + const fileName = path.join(appRoot, 'teamid'); + + if (fs.existsSync(fileName)) { + return fs.readFileSync(fileName, { encoding: 'utf8' }); + } + + return null; + } + + private readTeamId(appRoot): string { + return this.readXCConfig(appRoot, 'DEVELOPMENT_TEAM'); + } + + private callRemoteMethod(service: string, method: string, ...args: any[]): Promise { + const request: extProtocol.IRequest = { id: `req${++this._idCounter}`, service, method, args }; + + return new Promise((res, rej) => { + this._pendingRequests[request.id] = res; + + this._session.sendEvent(new Event(extProtocol.NS_DEBUG_ADAPTER_MESSAGE, request)); + }); + } + }; +} diff --git a/src/debug-adapter/nativeScriptPathTransformer.ts b/src/debug-adapter/nativeScriptPathTransformer.ts new file mode 100644 index 0000000..49237f3 --- /dev/null +++ b/src/debug-adapter/nativeScriptPathTransformer.ts @@ -0,0 +1,63 @@ +import * as fs from 'fs'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { UrlPathTransformer } from 'vscode-chrome-debug-core'; + +export class NativeScriptPathTransformer extends UrlPathTransformer { + private filePatterns = { + android: new RegExp('^(file:)?/*data/(data|user/\\d+)/.*?/files/(.*)$', 'i'), + ios: new RegExp('^(file:)?/*(.*)$', 'i'), + }; + + private targetPlatform: string; + + public setTargetPlatform(targetPlatform: string) { + this.targetPlatform = targetPlatform.toLowerCase(); + } + + protected async targetUrlToClientPath(webRoot: string, scriptUrl: string): Promise { + if (!scriptUrl) { + return; + } + + if (_.startsWith(scriptUrl, 'mdha:')) { + scriptUrl = _.trimStart(scriptUrl, 'mdha:'); + } + + if (path.isAbsolute(scriptUrl) && fs.existsSync(scriptUrl)) { + return Promise.resolve(scriptUrl); + } + + const filePattern = this.filePatterns[this.targetPlatform]; + + const matches = filePattern.exec(scriptUrl); + + let relativePath = scriptUrl; + + if (matches) { + relativePath = this.targetPlatform === 'android' ? matches[3] : matches[2]; + } + + const nodePath = path.join('..', 'node_modules'); + + relativePath = relativePath.replace('tns_modules', nodePath); + + const absolutePath = path.resolve(path.join(webRoot, relativePath)); + + if (fs.existsSync(absolutePath)) { + return Promise.resolve(absolutePath); + } + + const fileExtension = path.extname(absolutePath); + + if (fileExtension) { + const platformSpecificPath = absolutePath.replace(fileExtension, `.${this.targetPlatform}${fileExtension}`); + + if (fs.existsSync(platformSpecificPath)) { + return Promise.resolve(platformSpecificPath); + } + } + + return Promise.resolve(scriptUrl); + } +} diff --git a/src/debug-adapter/nativeScriptTargetDiscovery.ts b/src/debug-adapter/nativeScriptTargetDiscovery.ts new file mode 100644 index 0000000..7dfa821 --- /dev/null +++ b/src/debug-adapter/nativeScriptTargetDiscovery.ts @@ -0,0 +1,27 @@ +import * as uuid from 'uuid'; +import { chromeConnection, chromeTargetDiscoveryStrategy, logger, telemetry } from 'vscode-chrome-debug-core'; + +export class NativeScriptTargetDiscovery extends chromeTargetDiscoveryStrategy.ChromeTargetDiscovery { + constructor() { + super(logger, new telemetry.TelemetryReporter()); + } + + public getTarget(address: string, port: number, targetFilter?: any, targetUrl?: string): Promise { + return Promise.resolve({ + description: 'NS Debug Target', + devtoolsFrontendUrl: `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=${address}:${port}`, + id: uuid.v4(), + title: 'NS Debug Target', + type: 'node', + webSocketDebuggerUrl: `ws://${address}:${port}`, + }); + } + + public async getAllTargets(address: string, + port: number, targetFilter?: chromeConnection.ITargetFilter, + targetUrl?: string): Promise { + const target = await this.getTarget(address, port); + + return Promise.resolve([ target ]); + } +} diff --git a/src/debug-adapter/webKitDebug.ts b/src/debug-adapter/webKitDebug.ts deleted file mode 100644 index 1a250a3..0000000 --- a/src/debug-adapter/webKitDebug.ts +++ /dev/null @@ -1,7 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -import {WebKitDebugSession} from './webKitDebugSession'; - -WebKitDebugSession.run(WebKitDebugSession); diff --git a/src/debug-adapter/webKitDebugAdapter.ts b/src/debug-adapter/webKitDebugAdapter.ts deleted file mode 100644 index b7c0737..0000000 --- a/src/debug-adapter/webKitDebugAdapter.ts +++ /dev/null @@ -1,765 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ -import * as os from 'os'; -import * as fs from 'fs'; -import * as path from 'path'; -import {ChildProcess} from 'child_process'; -import {Handles, StoppedEvent, InitializedEvent, TerminatedEvent, OutputEvent} from 'vscode-debugadapter'; -import {DebugProtocol} from 'vscode-debugprotocol'; -import {INSDebugConnection} from './connection/INSDebugConnection'; -import {IosConnection} from './connection/iosConnection'; -import {AndroidConnection} from './connection/androidConnection'; -import {DebugResult} from '../project/project'; -import * as utils from '../common/utilities'; -import {formatConsoleMessage} from './consoleHelper'; -import {Services} from '../services/debugAdapterServices'; -import {LoggerHandler, Handlers, Tags} from '../common/logger'; -import {DebugRequest} from './debugRequest'; - -interface IScopeVarHandle { - objectId: string; - thisObj?: WebKitProtocol.Runtime.RemoteObject; -} - -export class WebKitDebugAdapter implements DebugProtocol.IDebugAdapter { - private static THREAD_ID = 1; - private static EXCEPTION_VALUE_ID = 'EXCEPTION_VALUE_ID'; - - private _initArgs: DebugProtocol.InitializeRequestArguments; - - private _variableHandles: Handles; - private _currentStack: WebKitProtocol.Debugger.CallFrame[]; - private _committedBreakpointsByUrl: Map; - private _exceptionValueObject: WebKitProtocol.Runtime.RemoteObject; - private _expectingResumedEvent: boolean; - private _scriptsById: Map; - private _setBreakpointsRequestQ: Promise; - private _webKitConnection: INSDebugConnection; - private _eventHandler: (event: DebugProtocol.Event) => void; - private _lastOutputEvent: OutputEvent; - private _loggerFrontendHandler: LoggerHandler = args => this.fireEvent(new OutputEvent(`${args.message}`, args.type.toString())); - private _request: DebugRequest; - private _tnsProcess: ChildProcess; - - public constructor() { - this._variableHandles = new Handles(); - - // Messages tagged with a special tag are sent to the frontend through the debugging protocol - Services.logger().addHandler(this._loggerFrontendHandler, [Tags.FrontendMessage]); - Services.logger().log(`OS: ${os.platform()} ${os.arch()}`); - Services.logger().log('Node version: ' + process.version); - Services.logger().log('Adapter version: ' + utils.getInstalledExtensionVersion().toString()); - - this.clearEverything(); - } - - private get paused(): boolean { - return !!this._currentStack; - } - - private clearTargetContext(): void { - this._scriptsById = new Map(); - this._committedBreakpointsByUrl = new Map(); - this._setBreakpointsRequestQ = Promise.resolve(); - this._lastOutputEvent = null; - this.fireEvent({ seq: 0, type: 'event', event: 'clearTargetContext'}); - } - - private clearClientContext(): void { - this.fireEvent({ seq: 0, type: 'event', event: 'clearClientContext'}); - } - - public registerEventHandler(eventHandler: (event: DebugProtocol.Event) => void): void { - this._eventHandler = eventHandler; - } - - public initialize(args: DebugProtocol.InitializeRequestArguments): DebugProtocol.Capabilities | Promise { - // Cache to log if diagnostic logging is enabled later - this._initArgs = args; - return { - supportsConfigurationDoneRequest: true, - supportsFunctionBreakpoints: false, - supportsConditionalBreakpoints: true, - supportsEvaluateForHovers: false, - supportsHitConditionalBreakpoints: true, // TODO: Not working on Android - exceptionBreakpointFilters: [{ - label: 'All Exceptions', - filter: 'all', - default: false - }, - { - label: 'Uncaught Exceptions', - filter: 'uncaught', - default: true - }], - supportsStepBack: false, - supportsSetVariable: false, // TODO: Check if can be enabled - supportsRestartFrame: false, // TODO: Check if can be enabled - supportsGotoTargetsRequest: false, // TODO: Check if can be enabled - supportsStepInTargetsRequest: false, // TODO: Check if can be enabled - supportsCompletionsRequest: false, // TODO: Check if can be enabled - supportsModulesRequest: false, // TODO: Check if can be enabled - additionalModuleColumns: undefined, // TODO: Check if can be enabled - supportedChecksumAlgorithms: undefined // TODO: Check if can be enabled - } - } - - public configurationDone(args: DebugProtocol.ConfigurationDoneArguments): void { - - } - - public launch(args: DebugProtocol.ILaunchRequestArgs): Promise { - return this.processRequest(args); - } - - public attach(args: DebugProtocol.IAttachRequestArgs): Promise { - return this.processRequest(args); - } - - private configureLoggingForRequest(args: DebugProtocol.IRequestArgs): void { - if (args.diagnosticLogging) { - // The logger frontend handler is initially configured to handle messages with LoggerTagFrontendMessage tag only. - // We remove the handler and add it again for all messages. - Services.logger().removeHandler(this._loggerFrontendHandler); - Services.logger().addHandler(this._loggerFrontendHandler); - } - if (args.tnsOutput) { - Services.logger().addHandler(Handlers.createStreamHandler(fs.createWriteStream(args.tnsOutput))); - } - Services.logger().log(`initialize(${JSON.stringify(this._initArgs) })\n`); - Services.logger().log(`${args.request}(${JSON.stringify(args)})\n`); - } - - private async processRequest(args: DebugProtocol.IRequestArgs) { - // Initialize the request - this.configureLoggingForRequest(args); - Services.appRoot = args.appRoot; - return Services.extensionClient().getInitSettings().then(async settings => { - Services.cliPath = settings.tnsPath || Services.cliPath; - this._request = new DebugRequest(args, Services.cli()); - Services.extensionClient().analyticsLaunchDebugger({ request: args.request, platform: args.platform }); - - // Run CLI Command - Services.logger().log(`[NSDebugAdapter] Using tns CLI v${this._request.project.cli.version.version} on path '${this._request.project.cli.path}'\n`, Tags.FrontendMessage); - Services.logger().log('[NSDebugAdapter] Running tns command...\n', Tags.FrontendMessage); - let cliCommand: DebugResult; - if (this._request.isLaunch) { - let tnsArgs = this._request.args.tnsArgs; - - // For iOS the TeamID is required if there's more than one. - // Therefore if not set, show selection to the user. - if(args.platform && args.platform.toLowerCase() === 'ios') { - let teamId = this.getTeamId(path.join(Services.appRoot, 'app'), this._request.args.tnsArgs); - if(!teamId) { - let selectedTeam = (await Services.extensionClient().selectTeam()); - if(selectedTeam) { - // add the selected by the user Team Id - tnsArgs = (tnsArgs || []).concat(['--teamId', selectedTeam.id]); - Services.logger().log(`[NSDebugAdapter] Using iOS Team ID '${selectedTeam.id}', you can change this in the workspace settings.\n`, Tags.FrontendMessage); - } - } - } - - cliCommand = this._request.project.debug({ stopOnEntry: this._request.launchArgs.stopOnEntry, watch: this._request.launchArgs.watch }, tnsArgs); - } - else if (this._request.isAttach) { - cliCommand = this._request.project.attach(this._request.args.tnsArgs); - } - - if (cliCommand.tnsProcess) { - this._tnsProcess = cliCommand.tnsProcess; - cliCommand.tnsOutputEventEmitter.on("tsCompilationError", () => { - this.fireEvent(new TerminatedEvent()); - }); - cliCommand.tnsProcess.stdout.on('data', data => { Services.logger().log(data.toString(), Tags.FrontendMessage); }); - cliCommand.tnsProcess.stderr.on('data', data => { Services.logger().error(data.toString(), Tags.FrontendMessage); }); - cliCommand.tnsProcess.on('close', (code, signal) => { - Services.logger().error(`[NSDebugAdapter] The tns command finished its execution with code ${code}.\n`, Tags.FrontendMessage); - - // Sometimes we execute "tns debug android --start" and the process finishes - // which is totally fine. If there's an error we need to Terminate the session. - if(code > 0) { - this.fireEvent(new TerminatedEvent()); - } - }); - } - - let promiseResolve = null; - let promise: Promise = new Promise((res, rej) => { promiseResolve = res; }); - Services.logger().log('[NSDebugAdapter] Watching the tns CLI output to receive a connection token\n', Tags.FrontendMessage); - // Attach to the running application - cliCommand.tnsOutputEventEmitter.on('readyForConnection', (connectionToken: string | number) => { - Services.logger().log(`[NSDebugAdapter] Ready to attach to application on ${connectionToken}\n`, Tags.FrontendMessage); - let connection: INSDebugConnection = this._request.isAndroid ? new AndroidConnection() : new IosConnection(); - - connection.attach(connectionToken, 'localhost').then(() => { - Services.logger().log(`[NSDebugAdapter] Connection to target application established on ${connectionToken}\n`, Tags.FrontendMessage); - this.setConnection(connection); - return connection.enable(); - }).then(() => { - Services.logger().log(`[NSDebugAdapter] Connection to target application successfully enabled\n`, Tags.FrontendMessage); - this.fireEvent(new InitializedEvent()); - promiseResolve(); - }).then(() => {}); - }); - - return promise; - }); - } - - private setConnection(connection: INSDebugConnection) : INSDebugConnection { - if (this._webKitConnection) { - this._webKitConnection.close(); - } - this._webKitConnection = connection; - connection.on('Debugger.paused', params => this.onDebuggerPaused(params)); - connection.on('Debugger.resumed', () => this.onDebuggerResumed()); - connection.on('Debugger.scriptParsed', params => this.onScriptParsed(params)); - connection.on('Debugger.globalObjectCleared', () => this.onGlobalObjectCleared()); - connection.on('Debugger.breakpointResolved', params => this.onBreakpointResolved(params)); - connection.on('Console.messageAdded', params => this.onConsoleMessage(params)); - connection.on('Console.messageRepeatCountUpdated', params => this.onMessageRepeatCountUpdated(params)); - connection.on('Inspector.detached', () => this.terminateSession()); - connection.on('close', () => this.terminateSession()); - connection.on('error', (error) => { - Services.logger().log(error.toString()); - this.terminateSession(); - }); - connection.on('connect', () => this.onConnected()) - return connection; - } - - private onConnected(): void { - Services.logger().log("Debugger connected"); - } - - private fireEvent(event: DebugProtocol.Event): void { - if (this._eventHandler) { - this._eventHandler(event); - } - } - - private terminateSession(): void { - this.clearEverything(); - // In case of a sync request the session is not terminated when the backend is detached - if (!this._request.isLaunch || !this._request.launchArgs.watch) { - Services.logger().log("[NSDebugAdapter] Terminating debug session"); - this.fireEvent(new TerminatedEvent()); - } - } - - private clearEverything(): void { - this.clearClientContext(); - this.clearTargetContext(); - } - - /** - * e.g. the target navigated - */ - private onGlobalObjectCleared(): void { - this.clearTargetContext(); - } - - private onDebuggerPaused(notification: WebKitProtocol.Debugger.PausedParams): void { - this._currentStack = notification.callFrames; - - // We can tell when we've broken on an exception. Otherwise if hitBreakpoints is set, assume we hit a - // breakpoint. If not set, assume it was a step. We can't tell the difference between step and 'break on anything'. - let reason: string; - let exceptionText: string; - if (notification.reason === 'exception') { - reason = 'exception'; - if (notification.data && this._currentStack.length) { - // Insert a scope to wrap the exception object. exceptionText is unused by Code at the moment. - const remoteObjValue = utils.remoteObjectToValue(notification.data, false); - let scopeObject: WebKitProtocol.Runtime.RemoteObject; - - if (remoteObjValue.variableHandleRef) { - // If the remote object is an object (probably an Error), treat the object like a scope. - exceptionText = notification.data.description; - scopeObject = notification.data; - } else { - // If it's a value, use a special flag and save the value for later. - exceptionText = notification.data.value; - scopeObject = { objectId: WebKitDebugAdapter.EXCEPTION_VALUE_ID }; - this._exceptionValueObject = notification.data; - } - - this._currentStack[0].scopeChain.unshift({ type: 'Exception', object: scopeObject }); - } - } else if (notification.reason == "PauseOnNextStatement") { - reason = 'pause'; - } else if (notification.reason == "Breakpoint") { - reason = 'breakpoint'; - } else { - reason = 'step'; - } - - this.fireEvent(new StoppedEvent(reason, /*threadId=*/WebKitDebugAdapter.THREAD_ID, exceptionText)); - } - - private onDebuggerResumed(): void { - this._currentStack = null; - - if (!this._expectingResumedEvent) { - // This is a private undocumented event provided by VS Code to support the 'continue' button on a paused Chrome page - let resumedEvent: DebugProtocol.Event = { seq: 0, type: 'event', event: 'continued', body: { threadId: WebKitDebugAdapter.THREAD_ID }}; - this.fireEvent(resumedEvent); - } else { - this._expectingResumedEvent = false; - } - } - - private onScriptParsed(script: WebKitProtocol.Debugger.Script): void { - this._scriptsById.set(script.scriptId, script); - - if (this.scriptIsNotAnonymous(script)) { - this.fireEvent({ seq: 0, type: 'event', event: 'scriptParsed', body: { scriptUrl: script.url, sourceMapURL: script.sourceMapURL }}); - } - } - - private onBreakpointResolved(params: WebKitProtocol.Debugger.BreakpointResolvedParams): void { - const script = this._scriptsById.get(params.location.scriptId); - if (!script) { - // Breakpoint resolved for a script we don't know about - return; - } - - const committedBps = this._committedBreakpointsByUrl.get(script.url) || []; - committedBps.push(params.breakpointId); - this._committedBreakpointsByUrl.set(script.url, committedBps); - } - - private onConsoleMessage(params: WebKitProtocol.Console.MessageAddedParams): void { - let localMessage = params.message; - let isClientPath = false; - if (localMessage.url) - { - const clientPath = utils.webkitUrlToClientPath(this._request.args.appRoot, this._request.args.platform, localMessage.url); - if (clientPath !== '') { - localMessage.url = clientPath; - isClientPath = true; - } - } - - const formattedMessage = formatConsoleMessage(localMessage, isClientPath); - if (formattedMessage) { - let outputEvent: OutputEvent = new OutputEvent(formattedMessage.text + '\n', formattedMessage.isError ? 'stderr' : 'stdout'); - this._lastOutputEvent = outputEvent; - this.fireEvent(outputEvent); - } - } - - public onMessageRepeatCountUpdated(params: WebKitProtocol.Console.MessageRepeatCountUpdatedEventArgs) { - if (this._lastOutputEvent) { - this.fireEvent(this._lastOutputEvent); - } - } - - public disconnect(): Promise { - this.clearEverything(); - if (this._tnsProcess) { - utils.killProcess(this._tnsProcess); - this._tnsProcess = null; - } - if (this._webKitConnection) { - Services.logger().log("Closing debug connection"); - this._webKitConnection.close(); - this._webKitConnection = null; - } - - return Promise.resolve(); - } - - public setBreakpoints(args: DebugProtocol.ISetBreakpointsArgs): Promise { - let targetScriptUrl: string; - if (args.source.path) { - targetScriptUrl = args.source.path; - } else if (args.source.sourceReference) { - const targetScript = this._scriptsById.get(sourceReferenceToScriptId(args.source.sourceReference)); - if (targetScript) { - targetScriptUrl = targetScript.url; - } - } - - if (targetScriptUrl) { - // DebugProtocol sends all current breakpoints for the script. Clear all scripts for the breakpoint then add all of them - const setBreakpointsPFailOnError = this._setBreakpointsRequestQ - .then(() => this._clearAllBreakpoints(targetScriptUrl)) - .then(() => this._addBreakpoints(targetScriptUrl, args)) - .then(responses => ({ breakpoints: this._webkitBreakpointResponsesToODPBreakpoints(targetScriptUrl, responses, args.lines) })); - - const inDebug = typeof (global).v8debug === 'object'; - const setBreakpointsPTimeout = utils.promiseTimeout(setBreakpointsPFailOnError, /*timeoutMs*/inDebug ? 2000000 : 8000, 'Set breakpoints request timed out'); - - // Do just one setBreakpointsRequest at a time to avoid interleaving breakpoint removed/breakpoint added requests to Chrome. - // Swallow errors in the promise queue chain so it doesn't get blocked, but return the failing promise for error handling. - this._setBreakpointsRequestQ = setBreakpointsPTimeout.catch(() => undefined); - return setBreakpointsPTimeout; - } else { - return utils.errP(`Can't find script for breakpoint request`); - } - } - - private _clearAllBreakpoints(url: string): Promise { - if (!this._committedBreakpointsByUrl.has(url)) { - return Promise.resolve(); - } - - // Remove breakpoints one at a time. Seems like it would be ok to send the removes all at once, - // but there is a chrome bug where when removing 5+ or so breakpoints at once, it gets into a weird - // state where later adds on the same line will fail with 'breakpoint already exists' even though it - // does not break there. - return this._committedBreakpointsByUrl.get(url).reduce((p, bpId) => { - return p.then(() => this._webKitConnection.debugger_removeBreakpoint(bpId)).then(() => { }); - }, Promise.resolve()).then(() => { - this._committedBreakpointsByUrl.set(url, null); - }); - } - - private _addBreakpoints(url: string, breakpoints: DebugProtocol.ISetBreakpointsArgs): Promise { - // Call setBreakpoint for all breakpoints in the script simultaneously - const responsePs = breakpoints.breakpoints - .map((b, i) => this._webKitConnection.debugger_setBreakpointByUrl(url, breakpoints.lines[i], breakpoints.cols ? breakpoints.cols[i] : 0, b.condition, parseInt(b.hitCondition) || 0)); - - // Join all setBreakpoint requests to a single promise - return Promise.all(responsePs); - } - - private _webkitBreakpointResponsesToODPBreakpoints(url: string, responses: WebKitProtocol.Debugger.SetBreakpointByUrlResponse[], requestLines: number[]): DebugProtocol.IBreakpoint[] { - // Don't cache errored responses - const committedBpIds = responses - .filter(response => !response.error) - .map(response => response.result.breakpointId); - - // Cache successfully set breakpoint ids from webkit in committedBreakpoints set - this._committedBreakpointsByUrl.set(url, committedBpIds); - - // Map committed breakpoints to DebugProtocol response breakpoints - return responses - .map((response, i) => { - // The output list needs to be the same length as the input list, so map errors to - // unverified breakpoints. - if (response.error || !response.result.locations.length) { - return { - verified: !response.error, - line: requestLines[i], - column: 0 - }; - } - - return { - verified: true, - line: response.result.locations[0].lineNumber, - column: response.result.locations[0].columnNumber - }; - }); - } - - public setExceptionBreakpoints(args: DebugProtocol.SetExceptionBreakpointsArguments): Promise { - let state: string; - if (args.filters.indexOf('all') >= 0) { - state = 'all'; - } else if (args.filters.indexOf('uncaught') >= 0) { - state = 'uncaught'; - } else { - state = 'none'; - } - - return this._webKitConnection.debugger_setPauseOnExceptions(state) - .then(() => { }); - } - - public continue(): Promise { - this._expectingResumedEvent = true; - return this._webKitConnection.debugger_resume() - .then(() => { }); - } - - public next(): Promise { - this._expectingResumedEvent = true; - return this._webKitConnection.debugger_stepOver() - .then(() => { }); - } - - public stepIn(): Promise { - this._expectingResumedEvent = true; - return this._webKitConnection.debugger_stepIn() - .then(() => { }); - } - - public stepOut(): Promise { - this._expectingResumedEvent = true; - return this._webKitConnection.debugger_stepOut() - .then(() => { }); - } - - public pause(): Promise { - return this._webKitConnection.debugger_pause() - .then(() => { }); - } - - public stackTrace(args: DebugProtocol.StackTraceArguments): DebugProtocol.IStackTraceResponseBody { - // Only process at the requested number of frames, if 'levels' is specified - let stack = this._currentStack; - if (args.levels) { - stack = this._currentStack.filter((_, i) => args.startFrame <= i && i < args.startFrame + args.levels); - } - - const stackFrames: DebugProtocol.StackFrame[] = stack - .map((callFrame: WebKitProtocol.Debugger.CallFrame, i: number) => { - const sourceReference = scriptIdToSourceReference(callFrame.location.scriptId); - const scriptId = callFrame.location.scriptId; - const script = this._scriptsById.get(scriptId); - - let source: DebugProtocol.Source; - if (this.scriptIsNotUnknown(scriptId)) { - // We have received Debugger.scriptParsed event for the script. - if (this.scriptIsNotAnonymous(script)) { - /** - * We have received non-empty url with the Debugger.scriptParsed event. - * We set the url value to the path property. Later on, the PathTransformer will attempt to resolve it to a script in the app root folder. - * In case it fails to resolve it, we also set the sourceReference field in order to allow the client to send source request to retrieve the source. - * If the PathTransformer resolves the url successfully, it will change the value of sourceReference to 0. - */ - source = { - name: path.basename(script.url), - path: script.url, - sourceReference: scriptIdToSourceReference(script.scriptId) // will be 0'd out by PathTransformer if not needed - }; - } - else { - /** - * We have received Debugger.scriptParsed event with empty url value. - * Sending only the sourceId will make the client to send source request to retrieve the source of the script. - */ - source = { - name: 'anonymous source', - sourceReference: sourceReference - }; - } - } - else { - /** - * Unknown script. No Debugger.scriptParsed event received for the script. - * - * Some 'internal scripts' are intentionally referenced by id equal to 0. Others have id > 0 but no Debugger.scriptParsed event is sent when parsed. - * In both cases we can't get its source code. If we send back a zero sourceReference the VS Code client will not send source request. - * The most we can do is to include a dummy stack frame with no source associated and without specifing the sourceReference. - */ - source = { - name: 'unknown source', - origin: 'internal module', - sourceReference: 0 - }; - } - - // If the frame doesn't have a function name, it's either an anonymous function - // or eval script. If its source has a name, it's probably an anonymous function. - const frameName = callFrame.functionName || (script && script.url ? '(anonymous function)' : '(eval code)'); - return { - id: args.startFrame + i, - name: frameName, - source: source, - line: callFrame.location.lineNumber, - column: callFrame.location.columnNumber - }; - }); - - return { stackFrames: stackFrames, totalFrames: this._currentStack.length }; - } - - public scopes(args: DebugProtocol.ScopesArguments): DebugProtocol.IScopesResponseBody { - const scopes = this._currentStack[args.frameId].scopeChain.map((scope: WebKitProtocol.Debugger.Scope, i: number) => { - const scopeHandle: IScopeVarHandle = { objectId: scope.object.objectId }; - if (i === 0) { - // The first scope should include 'this'. Keep the RemoteObject reference for use by the variables request - scopeHandle.thisObj = this._currentStack[args.frameId]['this']; - } - - return { - name: scope.type, - variablesReference: this._variableHandles.create(scopeHandle), - expensive: scope.type === 'global' - }; - }); - - return { scopes }; - } - - public variables(args: DebugProtocol.VariablesArguments): Promise { - const handle = this._variableHandles.get(args.variablesReference); - if (handle.objectId === WebKitDebugAdapter.EXCEPTION_VALUE_ID) { - // If this is the special marker for an exception value, create a fake property descriptor so the usual route can be used - const excValuePropDescriptor: WebKitProtocol.Runtime.PropertyDescriptor = { name: 'exception', value: this._exceptionValueObject }; - return Promise.resolve({ variables: [this.propertyDescriptorToVariable(excValuePropDescriptor)] }); - } else if (handle != null) { - return Promise.all([ - // Need to make two requests to get all properties - this._webKitConnection.runtime_getProperties(handle.objectId, /*ownProperties=*/false, /*accessorPropertiesOnly=*/true), - this._webKitConnection.runtime_getProperties(handle.objectId, /*ownProperties=*/true, /*accessorPropertiesOnly=*/false) - ]).then(getPropsResponses => { - // Sometimes duplicates will be returned - merge all property descriptors returned - const propsByName = new Map(); - getPropsResponses.forEach(response => { - if (!response.error) { - response.result.result.forEach(propDesc => - propsByName.set(propDesc.name, propDesc)); - } - }); - - // Convert WebKitProtocol prop descriptors to DebugProtocol vars, sort the result - const variables: DebugProtocol.Variable[] = []; - propsByName.forEach(propDesc => variables.push(this.propertyDescriptorToVariable(propDesc))); - variables.sort((var1, var2) => var1.name.localeCompare(var2.name)); - - // If this is a scope that should have the 'this', prop, insert it at the top of the list - if (handle.thisObj) { - variables.unshift(this.propertyDescriptorToVariable({ name: 'this', value: handle.thisObj })); - } - - return { variables }; - }); - } else { - return Promise.resolve(null); - } - } - - public source(args: DebugProtocol.SourceArguments): Promise { - return this._webKitConnection.debugger_getScriptSource(sourceReferenceToScriptId(args.sourceReference)).then(webkitResponse => { - if (webkitResponse.error) { - throw new Error(webkitResponse.error.message); - } - return { content: webkitResponse.result.scriptSource }; - }); - } - - public threads(): DebugProtocol.IThreadsResponseBody { - return { - threads: [ - { - id: WebKitDebugAdapter.THREAD_ID, - name: 'Thread ' + WebKitDebugAdapter.THREAD_ID - } - ] - }; - } - - public evaluate(args: DebugProtocol.EvaluateArguments): Promise { - let evalPromise: Promise; - if (this.paused) { - const callFrame = this._currentStack[args.frameId]; - if (!this.scriptIsNotUnknown(callFrame.location.scriptId)) { - // The iOS debugger backend hangs and stops responding after receiving evaluate request on call frame which has unknown source. - throw new Error('-'); // The message will be printed in the VS Code UI - } - evalPromise = this._webKitConnection.debugger_evaluateOnCallFrame(callFrame.callFrameId, args.expression); - } else { - evalPromise = this._webKitConnection.runtime_evaluate(args.expression); - } - - return evalPromise.then(evalResponse => { - if (evalResponse.result.wasThrown) { - const errorMessage = evalResponse.result.exceptionDetails ? evalResponse.result.exceptionDetails.text : 'Error'; - return utils.errP(errorMessage); - } - - const { value, variablesReference } = this.remoteObjectToValue(evalResponse.result.result); - return { result: value, variablesReference }; - }); - } - - private propertyDescriptorToVariable(propDesc: WebKitProtocol.Runtime.PropertyDescriptor): DebugProtocol.Variable { - if (propDesc.get || propDesc.set) { - // A property doesn't have a value here, and we shouldn't evaluate the getter because it may have side effects. - // Node adapter shows 'undefined', Chrome can eval the getter on demand. - return { name: propDesc.name, value: 'property', variablesReference: 0 }; - } else { - const { value, variablesReference } = this.remoteObjectToValue(propDesc.value); - return { name: propDesc.name, value, variablesReference }; - } - } - - /** - * Run the object through Utilities.remoteObjectToValue, and if it returns a variableHandle reference, - * use it with this instance's variableHandles to create a variable handle. - */ - private remoteObjectToValue(object: WebKitProtocol.Runtime.RemoteObject): { value: string, variablesReference: number } { - const { value, variableHandleRef } = utils.remoteObjectToValue(object); - const result = { value, variablesReference: 0 }; - if (variableHandleRef) { - result.variablesReference = this._variableHandles.create({ objectId: variableHandleRef }); - } - - return result; - } - - // Returns true if the script has url supplied in Debugger.scriptParsed event - private scriptIsNotAnonymous(script: WebKitProtocol.Debugger.Script): boolean { - return script && !!script.url; - } - - // Returns true if Debugger.scriptParsed event is received for the provided script id - private scriptIsNotUnknown(scriptId: WebKitProtocol.Debugger.ScriptId): boolean { - return !!this._scriptsById.get(scriptId); - } - - private getTeamId(appRoot: string, tnsArgs?: string[]): string { - // try to get the TeamId from the TnsArgs - if(tnsArgs) { - const teamIdArgIndex = tnsArgs.indexOf('--teamId'); - if(teamIdArgIndex > 0 && teamIdArgIndex + 1 < tnsArgs.length) { - return tnsArgs[ teamIdArgIndex + 1 ]; - } - } - - // try to get the TeamId from the buildxcconfig or teamid file - const teamIdFromConfig = this.readTeamId(appRoot); - if(teamIdFromConfig) { - return teamIdFromConfig; - } - - // we should get the Teams from the machine and ask the user if they are more than 1 - return null; - } - - private readXCConfig(appRoot: string, flag: string): string { - let xcconfigFile = path.join(appRoot, "App_Resources/iOS/build.xcconfig"); - if (fs.existsSync(xcconfigFile)) { - let text = fs.readFileSync(xcconfigFile, { encoding: 'utf8'}); - let teamId: string; - text.split(/\r?\n/).forEach((line) => { - line = line.replace(/\/(\/)[^\n]*$/, ""); - if (line.indexOf(flag) >= 0) { - teamId = line.split("=")[1].trim(); - if (teamId[teamId.length - 1] === ';') { - teamId = teamId.slice(0, -1); - } - } - }); - if (teamId) { - return teamId; - } - } - - let fileName = path.join(appRoot, "teamid"); - if (fs.existsSync(fileName)) { - return fs.readFileSync(fileName, { encoding: 'utf8' }); - } - - return null; - } - - private readTeamId(appRoot): string { - return this.readXCConfig(appRoot, "DEVELOPMENT_TEAM"); - } -} - -function scriptIdToSourceReference(scriptId: WebKitProtocol.Debugger.ScriptId): number { - return parseInt(scriptId, 10); -} - -function sourceReferenceToScriptId(sourceReference: number): WebKitProtocol.Debugger.ScriptId { - return '' + sourceReference; -} diff --git a/src/debug-adapter/webKitDebugSession.ts b/src/debug-adapter/webKitDebugSession.ts deleted file mode 100644 index 1cc8a7a..0000000 --- a/src/debug-adapter/webKitDebugSession.ts +++ /dev/null @@ -1,114 +0,0 @@ -import {OutputEvent, DebugSession, ErrorDestination} from 'vscode-debugadapter'; -import {DebugProtocol} from 'vscode-debugprotocol'; - -import {WebKitDebugAdapter} from './webKitDebugAdapter'; -import {LoggerHandler, Handlers, Tags} from '../common/logger'; -import {Services} from '../services/debugAdapterServices'; - -import {AdapterProxy} from './adapter/adapterProxy'; -import {LineNumberTransformer} from './adapter/lineNumberTransformer'; -import {PathTransformer} from './adapter/pathTransformer'; -import {SourceMapTransformer} from './adapter/sourceMaps/sourceMapTransformer'; - -export class WebKitDebugSession extends DebugSession { - private _adapterProxy: AdapterProxy; - - public constructor(targetLinesStartAt1: boolean, isServer: boolean = false) { - super(targetLinesStartAt1, isServer); - - // Logging on the std streams is only allowed when running in server mode, because otherwise it goes through - // the same channel that Code uses to communicate with the adapter, which can cause communication issues. - if (isServer) { - Services.logger().addHandler(Handlers.stdStreamsHandler); - } - - process.removeAllListeners('unhandledRejection'); - process.addListener('unhandledRejection', reason => { - Services.logger().log(`******** ERROR! Unhandled promise rejection: ${reason}`); - }); - - this._adapterProxy = new AdapterProxy( - [ - new LineNumberTransformer(targetLinesStartAt1), - new SourceMapTransformer(), - new PathTransformer() - ], - new WebKitDebugAdapter(), - event => this.sendEvent(event)); - } - - /** - * Overload sendEvent to log - */ - public sendEvent(event: DebugProtocol.Event): void { - if (event.event !== 'output') { - // Don't create an infinite loop... - Services.logger().log(`To client: ${JSON.stringify(event) }`); - } - - super.sendEvent(event); - } - - /** - * Overload sendResponse to log - */ - public sendResponse(response: DebugProtocol.Response): void { - Services.logger().log(`To client: ${JSON.stringify(response) }`); - super.sendResponse(response); - } - - /** - * Takes a response and a promise to the response body. If the promise is successful, assigns the response body and sends the response. - * If the promise fails, sets the appropriate response parameters and sends the response. - */ - private sendResponseAsync(request: DebugProtocol.Request, response: DebugProtocol.Response, responseP: Promise): void { - responseP.then( - (body?) => { - response.body = body; - this.sendResponse(response); - }, - e => { - let eStr = e ? e.message : 'Unknown error'; - if (typeof e === "string" || e instanceof String) - { - eStr = e; - } - - - if (eStr === 'Error: unknowncommand') { - this.sendErrorResponse(response, 1014, '[NSDebugAdapter] Unrecognized request: ' + request.command, null, ErrorDestination.Telemetry); - return; - } - - if (request.command === 'evaluate') { - // Errors from evaluate show up in the console or watches pane. Doesn't seem right - // as it's not really a failed request. So it doesn't need the tag and worth special casing. - response.message = eStr; - } else { - // These errors show up in the message bar at the top (or nowhere), sometimes not obvious that they - // come from the adapter - response.message = '[NSDebugAdapter] ' + eStr; - Services.logger().error('Error: ' + eStr, Tags.FrontendMessage); - } - - response.success = false; - this.sendResponse(response); - }); - } - - /** - * Overload dispatchRequest to dispatch to the adapter proxy instead of debugSession's methods for each request. - */ - protected dispatchRequest(request: DebugProtocol.Request): void { - const response = { seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true }; - try { - Services.logger().log(`From client: ${request.command}(${JSON.stringify(request.arguments) })`); - this.sendResponseAsync( - request, - response, - this._adapterProxy.dispatchRequest(request)); - } catch (e) { - this.sendErrorResponse(response, 1104, 'Exception while processing request (exception: {_exception})', { _exception: e.message }, ErrorDestination.Telemetry); - } - } -} \ No newline at end of file diff --git a/src/ipc/extensionClient.ts b/src/ipc/extensionClient.ts deleted file mode 100644 index 7e7e3bc..0000000 --- a/src/ipc/extensionClient.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as extProtocol from './extensionProtocol'; -import {Services} from "../services/debugAdapterServices"; -import {getSocketId} from "./sockedId"; - -const ipc = require('node-ipc'); - -export class ExtensionClient { - private _appRoot: string; - private _idCounter = 0; - private _pendingRequests: Object; - private _socketId: string; - - private _ipcClientInitialized: Promise; - - constructor(appRoot: string) { - this._appRoot = appRoot; - this._idCounter = 0; - this._pendingRequests = {}; - - this._socketId = getSocketId(); - - ipc.config.id = 'debug-adapter-' + process.pid; - ipc.config.retry = 1500; - ipc.config.maxRetries = 5; - - this._ipcClientInitialized = new Promise((res, rej) => { - ipc.connectTo( - this._socketId, - () => { - ipc.of[this._socketId].on('connect', () => { - res(); - }); - - ipc.of[this._socketId].on('error', error => { - Services.logger().log(`[ExtensionClient] error: ${JSON.stringify(error)}\n`); - }); - - ipc.of[this._socketId].on('extension-protocol-message', (response: extProtocol.Response) => { - (<(result: Object) => void>this._pendingRequests[response.requestId])(response.result); - }); - } - ); - }); - } - - private callRemoteMethod(method: string, args?: Object): Promise { - let request: extProtocol.Request = {id: 'req' + (++this._idCounter), method: method, args: args}; - return new Promise((res, rej) => { - this._pendingRequests[request.id] = res; - ipc.of[this._socketId].emit('extension-protocol-message', request); - }); - } - - public getInitSettings(): Promise { - return >(this.callRemoteMethod('getInitSettings')); - } - - public analyticsLaunchDebugger(args: extProtocol.AnalyticsLaunchDebuggerArgs): Promise { - return this.callRemoteMethod('analyticsLaunchDebugger', args); - } - - public runRunCommand(args: extProtocol.AnalyticsRunRunCommandArgs): Promise { - return this.callRemoteMethod('runRunCommand', args); - } - - public selectTeam(): Promise<{ id: string, name: string }> { - return >(this.callRemoteMethod('selectTeam')); - } -} \ No newline at end of file diff --git a/src/ipc/extensionProtocol.ts b/src/ipc/extensionProtocol.ts deleted file mode 100644 index c3740eb..0000000 --- a/src/ipc/extensionProtocol.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface Request { - id: string; - method: string; - args: Object; -} - -export interface Response { - requestId: string; - result: Object; -} - -export interface AnalyticsLaunchDebuggerArgs { - request: string; - platform: string; -} - -export interface AnalyticsRunRunCommandArgs { - platform: string; -} - -export interface InitSettingsResult { - tnsPath: string; -} \ No newline at end of file diff --git a/src/ipc/extensionServer.ts b/src/ipc/extensionServer.ts deleted file mode 100644 index 366feca..0000000 --- a/src/ipc/extensionServer.ts +++ /dev/null @@ -1,138 +0,0 @@ -import * as path from 'path'; -import * as fs from 'fs'; -import * as vscode from 'vscode'; -import {QuickPickItem} from 'vscode'; -import * as extProtocol from './extensionProtocol'; -import {Services} from '../services/extensionHostServices'; -import {getSocketId} from "./sockedId"; - -let ipc = require('node-ipc'); - -export class ExtensionServer { - private _isRunning: boolean; - - constructor() { - this._isRunning = false; - } - - public start() { - if (!this._isRunning) { - ipc.config.id = getSocketId(); - ipc.serve( - () => { - ipc.server.on('extension-protocol-message', (data: extProtocol.Request, socket) => { - return (>this[data.method].call(this, data.args)).then(result => { - let response: extProtocol.Response = {requestId: data.id, result: result}; - return ipc.server.emit(socket, 'extension-protocol-message', response); - }); - }); - }); - ipc.server.start(); - this._isRunning = true; - } - return this._isRunning; - } - - public stop() { - if (this._isRunning) { - ipc.server.stop(); - this._isRunning = false; - } - } - - public isRunning() { - return this._isRunning; - } - - public getInitSettings(): Promise { - let tnsPath = Services.workspaceConfigService().tnsPath; - return Promise.resolve({tnsPath: tnsPath}); - } - - public analyticsLaunchDebugger(args: extProtocol.AnalyticsLaunchDebuggerArgs): Promise { - return Services.analyticsService().launchDebugger(args.request, args.platform); - } - - public runRunCommand(args: extProtocol.AnalyticsRunRunCommandArgs): Promise { - return Services.analyticsService().runRunCommand(args.platform); - } - - public selectTeam(): Promise<{ id: string, name: string }> { - return new Promise((resolve, reject) => { - const workspaceTeamId = vscode.workspace.getConfiguration().get("nativescript.iosTeamId"); - - if (workspaceTeamId) { - resolve({ - id: workspaceTeamId, - name: undefined // irrelevant - }); - return; - } - - let developmentTeams = this.getDevelopmentTeams(); - if (developmentTeams.length > 1) { - let quickPickItems: Array = developmentTeams.map((team) => { - return { - label: team.name, - description: team.id - }; - }); - vscode.window.showQuickPick( - quickPickItems, { - placeHolder: "Select your development team" - }) - .then((val: QuickPickItem) => { - vscode.workspace.getConfiguration().update("nativescript.iosTeamId", val.description); - resolve({ - id: val.description, - name: val.label - }) - }); - } else { - resolve(); - } - }); - } - - private getDevelopmentTeams(): Array<{ id: string, name: string }> { - try { - let dir = path.join(process.env.HOME, "Library/MobileDevice/Provisioning Profiles/"); - let files = fs.readdirSync(dir); - let teamIds: any = {}; - for (let file of files) { - let filePath = path.join(dir, file); - let data = fs.readFileSync(filePath, {encoding: "utf8"}); - let teamId = this.getProvisioningProfileValue("TeamIdentifier", data); - let teamName = this.getProvisioningProfileValue("TeamName", data); - if (teamId) { - teamIds[teamId] = teamName; - } - } - - let teamIdsArray = new Array<{ id: string, name: string }>(); - for (let teamId in teamIds) { - teamIdsArray.push({id: teamId, name: teamIds[teamId]}); - } - - return teamIdsArray; - } catch (e) { - // no matter what happens, don't break - return new Array<{ id: string, name: string }>(); - } - } - - private getProvisioningProfileValue(name: string, text: string): string { - let findStr = "" + name + ""; - let index = text.indexOf(findStr); - if (index > 0) { - index = text.indexOf("", index + findStr.length); - if (index > 0) { - index += "".length; - let endIndex = text.indexOf("", index); - let result = text.substring(index, endIndex); - return result; - } - } - return null; - } -} \ No newline at end of file diff --git a/src/ipc/sockedId.ts b/src/ipc/sockedId.ts deleted file mode 100644 index 54328e3..0000000 --- a/src/ipc/sockedId.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function getSocketId(): string { - // let's KISS for now - I doubt users will want to simultaneously debug 2 apps anyway - return 'vs-ns-ext'; -} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 26c869d..a4e8b25 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,90 +1,129 @@ +import * as semver from 'semver'; import * as vscode from 'vscode'; -import {CliVersion} from './project/nativeScriptCli'; -import {Services} from './services/extensionHostServices'; -import {Project} from './project/project'; -import {IosProject} from './project/iosProject'; -import {AndroidProject} from './project/androidProject'; +import * as extProtocol from './common/extensionProtocol'; import * as utils from './common/utilities'; +import { AndroidProject } from './project/androidProject'; +import { IosProject } from './project/iosProject'; +import { Project } from './project/project'; +import { ChannelLogger } from './services/channelLogger'; +import { services } from './services/extensionHostServices'; // this method is called when the extension is activated export function activate(context: vscode.ExtensionContext) { - Services.globalState = context.globalState; - Services.cliPath = Services.workspaceConfigService().tnsPath || Services.cliPath; - Services.extensionServer().start(); - Services.analyticsService().initialize(); - - // Check if NativeScript CLI is installed globally and if it is compatible with the extension version - let cliVersion = Services.cli().version; - if (!cliVersion.isCompatible) { - vscode.window.showErrorMessage(cliVersion.errorMessage); + services.globalState = context.globalState; + services.cliPath = services.workspaceConfigService.tnsPath || services.cliPath; + + const channel = vscode.window.createOutputChannel('NativeScript Extension'); + + services.logger = new ChannelLogger(channel); + + const packageJSON = vscode.extensions.getExtension('Telerik.nativescript').packageJSON; + const cliVersion = services.cli().executeGetVersion(); + + if (!cliVersion) { + // tslint:disable-next-line:max-line-length + vscode.window.showErrorMessage("NativeScript CLI not found. Use 'nativescript.tnsPath' workspace setting to explicitly set the absolute path to the NativeScript CLI."); + + return; + } + + if (!semver.gte(cliVersion, packageJSON.minNativescriptCliVersion)) { + // tslint:disable-next-line:max-line-length + vscode.window.showErrorMessage(`The existing NativeScript extension is compatible with NativeScript CLI v${packageJSON.minNativescriptCliVersion} or greater. + The currently installed NativeScript CLI is v${cliVersion}.You can update the NativeScript CLI by executing 'npm install -g nativescript'.`); + + return; } - let channel = createInfoChannel(cliVersion.version.toString()); - let showOutputChannelCommand = vscode.commands.registerCommand('nativescript.showOutputChannel', () => { + services.cliVersion = cliVersion; + services.extensionVersion = packageJSON.version; + + logExtensionInfo(cliVersion, packageJSON); + + services.analyticsService.initialize(); + + const showOutputChannelCommand = vscode.commands.registerCommand('nativescript.showOutputChannel', () => { channel.show(); }); - let runCommand = (project: Project) => { + const beforeBuildDisposables = new Array(); + const runCommand = (project: Project) => { if (vscode.workspace.rootPath === undefined) { vscode.window.showErrorMessage('No workspace opened.'); + return; } // Show output channel - let runChannel: vscode.OutputChannel = vscode.window.createOutputChannel(`Run on ${project.platformName()}`); + const runChannel: vscode.OutputChannel = vscode.window.createOutputChannel(`Run on ${project.platformName()}`); runChannel.clear(); runChannel.show(vscode.ViewColumn.Two); - Services.analyticsService().runRunCommand(project.platformName()); + services.analyticsService.runRunCommand(project.platformName()); - let tnsProcess = project.run(); - tnsProcess.on('error', err => { + const tnsProcess = project.run(); + tnsProcess.on('error', (err) => { vscode.window.showErrorMessage('Unexpected error executing NativeScript Run command.'); }); - tnsProcess.stderr.on('data', data => { + tnsProcess.stderr.on('data', (data) => { runChannel.append(data.toString()); }); - tnsProcess.stdout.on('data', data => { + tnsProcess.stdout.on('data', (data) => { runChannel.append(data.toString()); }); - tnsProcess.on('exit', exitCode => { + tnsProcess.on('exit', (exitCode) => { tnsProcess.stdout.removeAllListeners('data'); tnsProcess.stderr.removeAllListeners('data'); }); - tnsProcess.on('close', exitCode => { + tnsProcess.on('close', (exitCode) => { runChannel.hide(); }); - context.subscriptions.push({ - dispose: () => utils.killProcess(tnsProcess) - }); + const disposable = { + dispose: () => utils.killProcess(tnsProcess), + }; + + context.subscriptions.push(disposable); + beforeBuildDisposables.push(disposable); }; - let runIosCommand = vscode.commands.registerCommand('nativescript.runIos', () => { - return runCommand(new IosProject(vscode.workspace.rootPath, Services.cli())); + const runIosCommand = vscode.commands.registerCommand('nativescript.runIos', () => { + return runCommand(new IosProject(vscode.workspace.rootPath, services.cli())); }); - let runAndroidCommand = vscode.commands.registerCommand('nativescript.runAndroid', () => { - return runCommand(new AndroidProject(vscode.workspace.rootPath, Services.cli())); + const runAndroidCommand = vscode.commands.registerCommand('nativescript.runAndroid', () => { + return runCommand(new AndroidProject(vscode.workspace.rootPath, services.cli())); }); + context.subscriptions.push(vscode.debug.onDidReceiveDebugSessionCustomEvent((event) => { + if (event.event === extProtocol.BEFORE_DEBUG_START) { + beforeBuildDisposables.forEach((disposable) => disposable.dispose()); + } + + if (event.event === extProtocol.NS_DEBUG_ADAPTER_MESSAGE) { + const request = event.body as extProtocol.IRequest; + const service = services[request.service]; + const method = service[request.method]; + const response = typeof method === 'function' ? service[request.method].call(service, ...request.args) : method; + + if (response.then) { + response.then((result) => event.session.customRequest('onExtensionResponse', { requestId: request.id, result })); + + return; + } + + event.session.customRequest('onExtensionResponse', { requestId: request.id, result: response }); + } + })); + context.subscriptions.push(runIosCommand); context.subscriptions.push(runAndroidCommand); context.subscriptions.push(showOutputChannelCommand); } -function createInfoChannel(cliVersion: string): vscode.OutputChannel { - let channel = vscode.window.createOutputChannel("NativeScript Extension"); - const packageJSON = vscode.extensions.getExtension("Telerik.nativescript").packageJSON; - - packageJSON.version && channel.appendLine(`Version: ${packageJSON.version}`); - packageJSON.buildVersion && channel.appendLine(`Build version: ${packageJSON.buildVersion}`); - packageJSON.commitId && channel.appendLine(`Commit id: ${packageJSON.commitId}`); - channel.appendLine(`NativeScript CLI: ${cliVersion}`); - - return channel; +function logExtensionInfo(cliVersion: string, packageJSON: any): void { + packageJSON.version && services.logger.log(`Version: ${packageJSON.version}`); + packageJSON.buildVersion && services.logger.log(`Build version: ${packageJSON.buildVersion}`); + packageJSON.commitId && services.logger.log(`Commit id: ${packageJSON.commitId}`); + services.logger.log(`NativeScript CLI: ${cliVersion}`); } - -export function deactivate() { - Services.extensionServer().stop(); -} \ No newline at end of file diff --git a/src/project/androidProject.ts b/src/project/androidProject.ts index 510fc03..c975e65 100644 --- a/src/project/androidProject.ts +++ b/src/project/androidProject.ts @@ -1,12 +1,9 @@ -import {ChildProcess} from 'child_process'; +import { ChildProcess } from 'child_process'; +import { EventEmitter } from 'events'; import * as stream from 'stream'; -import {EventEmitter} from 'events'; -import {Project, DebugResult} from './project'; +import { NativeScriptCli } from './nativeScriptCli'; +import { IDebugResult, Project } from './project'; import * as scanner from './streamScanner'; -import {Version} from '../common/version'; -import {NativeScriptCli} from './nativeScriptCli'; - -export type GetDebugPortResult = { tnsProcess: ChildProcess, debugPort: Promise }; export class AndroidProject extends Project { @@ -15,45 +12,52 @@ export class AndroidProject extends Project { } public platformName(): string { - return "android"; + return 'android'; } - public attach(tnsArgs?: string[]): DebugResult { - let args: string[] = ["--start"]; + public attach(tnsArgs?: string[]): IDebugResult { + let args: string[] = ['--start']; + args = args.concat(tnsArgs); - let debugProcess : ChildProcess = super.executeDebugCommand(args); - let tnsOutputEventEmitter = new EventEmitter(); + const debugProcess: ChildProcess = super.executeDebugCommand(args); + const tnsOutputEventEmitter = new EventEmitter(); + this.configureReadyEvent(debugProcess.stdout, tnsOutputEventEmitter, true); - return { tnsProcess: debugProcess, tnsOutputEventEmitter: tnsOutputEventEmitter }; + + return { tnsProcess: debugProcess, tnsOutputEventEmitter }; } - public debug(options: { stopOnEntry: boolean, watch: boolean }, tnsArgs?: string[]): DebugResult { + public debug(options: { stopOnEntry: boolean, watch: boolean }, tnsArgs?: string[]): IDebugResult { let args: string[] = []; - args.push(options.watch ? "--watch" : "--no-watch"); - if (options.stopOnEntry) { args.push("--debug-brk"); } + + args.push(options.watch ? '--watch' : '--no-watch'); + if (options.stopOnEntry) { args.push('--debug-brk'); } args = args.concat(tnsArgs); - let debugProcess : ChildProcess = super.executeDebugCommand(args); - let tnsOutputEventEmitter: EventEmitter = new EventEmitter(); + const debugProcess: ChildProcess = super.executeDebugCommand(args); + const tnsOutputEventEmitter: EventEmitter = new EventEmitter(); + this.configureReadyEvent(debugProcess.stdout, tnsOutputEventEmitter, false); - return { tnsProcess: debugProcess, tnsOutputEventEmitter: tnsOutputEventEmitter }; + + return { tnsProcess: debugProcess, tnsOutputEventEmitter }; } protected configureReadyEvent(readableStream: stream.Readable, eventEmitter: EventEmitter, attach?: boolean): void { super.configureReadyEvent(readableStream, eventEmitter); let debugPort = null; - new scanner.StringMatchingScanner(readableStream).onEveryMatch(new RegExp("device: .* debug port: [0-9]+"), (match: scanner.MatchFound) => { - //device: {device-name} debug port: {debug-port} - debugPort = parseInt((match.matches[0]).match("(?:debug port: )([\\d]{5})")[1]); + + new scanner.StringMatchingScanner(readableStream).onEveryMatch(new RegExp('device: .* debug port: [0-9]+'), (match: scanner.IMatchFound) => { + // device: {device-name} debug port: {debug-port} + debugPort = parseInt((match.matches[0] as string).match('(?:debug port: )([\\d]{5})')[1], 10); if (attach) { // wait a little before trying to connect, this gives a chance for adb to be able to connect to the debug socket setTimeout(() => { eventEmitter.emit('readyForConnection', debugPort); }, 1000); } }); if (!attach) { - new scanner.StringMatchingScanner(readableStream).onEveryMatch('# NativeScript Debugger started #', (match: scanner.MatchFound) => { + new scanner.StringMatchingScanner(readableStream).onEveryMatch('# NativeScript Debugger started #', (match: scanner.IMatchFound) => { // wait a little before trying to connect, this gives a chance for adb to be able to connect to the debug socket setTimeout(() => { eventEmitter.emit('readyForConnection', debugPort); }, 1000); }); diff --git a/src/project/iosProject.ts b/src/project/iosProject.ts index b37ea39..9cf77f3 100644 --- a/src/project/iosProject.ts +++ b/src/project/iosProject.ts @@ -1,12 +1,9 @@ -import {ChildProcess} from 'child_process'; +import { ChildProcess } from 'child_process'; +import { EventEmitter } from 'events'; import * as stream from 'stream'; -import {EventEmitter} from 'events'; -import {Project, DebugResult} from './project'; +import { NativeScriptCli } from './nativeScriptCli'; +import { IDebugResult, Project } from './project'; import * as scanner from './streamScanner'; -import {Version} from '../common/version'; -import {NativeScriptCli} from './nativeScriptCli'; -import {Services} from '../services/debugAdapterServices'; -import {Tags} from '../common/logger'; export class IosProject extends Project { @@ -19,39 +16,46 @@ export class IosProject extends Project { } public platformName(): string { - return "ios"; + return 'ios'; } - public attach(tnsArgs?: string[]): DebugResult { - let args: string[] = ["--start"]; + public attach(tnsArgs?: string[]): IDebugResult { + let args: string[] = ['--start']; + args = args.concat(tnsArgs); - let debugProcess : ChildProcess = super.executeDebugCommand(args); - let tnsOutputEventEmitter: EventEmitter = new EventEmitter(); + const debugProcess: ChildProcess = super.executeDebugCommand(args); + const tnsOutputEventEmitter: EventEmitter = new EventEmitter(); + this.configureReadyEvent(debugProcess.stdout, tnsOutputEventEmitter); - return { tnsProcess: debugProcess, tnsOutputEventEmitter: tnsOutputEventEmitter }; + + return { tnsProcess: debugProcess, tnsOutputEventEmitter }; } - public debug(options: { stopOnEntry: boolean, watch: boolean }, tnsArgs?: string[]): DebugResult { + public debug(options: { stopOnEntry: boolean, watch: boolean }, tnsArgs?: string[]): IDebugResult { let args: string[] = []; - args.push(options.watch ? "--watch" : "--no-watch"); - if (options.stopOnEntry) { args.push("--debug-brk"); } + + args.push(options.watch ? '--watch' : '--no-watch'); + if (options.stopOnEntry) { args.push('--debug-brk'); } args = args.concat(tnsArgs); - let debugProcess : ChildProcess = super.executeDebugCommand(args); - let tnsOutputEventEmitter: EventEmitter = new EventEmitter(); + const debugProcess: ChildProcess = super.executeDebugCommand(args); + const tnsOutputEventEmitter: EventEmitter = new EventEmitter(); + this.configureReadyEvent(debugProcess.stdout, tnsOutputEventEmitter); - return { tnsProcess: debugProcess, tnsOutputEventEmitter: tnsOutputEventEmitter }; + + return { tnsProcess: debugProcess, tnsOutputEventEmitter }; } protected configureReadyEvent(readableStream: stream.Readable, eventEmitter: EventEmitter): void { super.configureReadyEvent(readableStream, eventEmitter); - let socketPathPrefix = 'socket-file-location: '; - let streamScanner = new scanner.StringMatchingScanner(readableStream); - streamScanner.onEveryMatch(new RegExp(socketPathPrefix + '.*\.sock'), (match: scanner.MatchFound) => { - let socketPath = (match.matches[0]).substr(socketPathPrefix.length); - eventEmitter.emit('readyForConnection', socketPath); + const streamScanner = new scanner.StringMatchingScanner(readableStream); + + streamScanner.onEveryMatch(new RegExp('Opened localhost (.*)'), (match: scanner.IMatchFound) => { + const port = parseInt(match.matches[1] as string, 10); + + setTimeout(() => { eventEmitter.emit('readyForConnection', port); }, 1000); }); } diff --git a/src/project/nativeScriptCli.ts b/src/project/nativeScriptCli.ts index a7669b0..7083fa4 100644 --- a/src/project/nativeScriptCli.ts +++ b/src/project/nativeScriptCli.ts @@ -1,55 +1,15 @@ -import {spawn, execSync, ChildProcess} from 'child_process'; -import {Version} from '../common/version'; -import {Logger, Tags} from '../common/logger'; +import { ChildProcess, execSync, spawn } from 'child_process'; +import { ILogger } from '../common/logger'; import * as utils from '../common/utilities'; -import * as os from 'os'; - -export enum CliVersionState { - NotExisting, - OlderThanSupported, - Compatible -} - -export class CliVersion { - private _cliVersion: Version = undefined; - private _minExpectedCliVersion: Version = undefined; - private _cliVersionState: CliVersionState; - private _cliVersionErrorMessage: string; - - constructor(cliVersion: Version, minExpectedCliVersion: Version) { - this._cliVersion = cliVersion; - this._minExpectedCliVersion = minExpectedCliVersion; - - // Calculate CLI version state and CLI version error message - this._cliVersionState = CliVersionState.Compatible; - if (minExpectedCliVersion) { - if (this._cliVersion === null) { - this._cliVersionState = CliVersionState.NotExisting; - this._cliVersionErrorMessage = "NativeScript CLI not found, please run 'npm -g install nativescript' to install it."; - } - else if (this._cliVersion.compareBySubminorTo(minExpectedCliVersion) < 0) { - this._cliVersionState = CliVersionState.OlderThanSupported; - this._cliVersionErrorMessage = `The existing NativeScript extension is compatible with NativeScript CLI v${this._minExpectedCliVersion} or greater. The currently installed NativeScript CLI is v${this._cliVersion}. You can update the NativeScript CLI by executing 'npm install -g nativescript'.`; - } - } - } - - public get version() { return this._cliVersion; } - - public get state() { return this._cliVersionState; } - - public get isCompatible() { return this._cliVersionState == CliVersionState.Compatible; } - - public get errorMessage() { return this._cliVersionErrorMessage; } -} export class NativeScriptCli { + private static CLI_OUTPUT_VERSION_REGEXP = /^(?:\d+\.){2}\d+.*?$/m; + private _path: string; private _shellPath: string; - private _cliVersion: CliVersion; - private _logger: Logger; + private _logger: ILogger; - constructor(cliPath: string, logger: Logger) { + constructor(cliPath: string, logger: ILogger) { this._path = cliPath; this._logger = logger; @@ -58,47 +18,54 @@ export class NativeScriptCli { // always default to cmd on Windows // workaround for issue #121 https://github.com/NativeScript/nativescript-vscode-extension/issues/121 if (utils.getPlatform() === utils.Platform.Windows) { - this._shellPath = "cmd.exe"; - } - - let versionStr = null; - try { - versionStr = this.executeSync(["--version"], undefined); - } - catch(e) { - this._logger.log(e, Tags.FrontendMessage); - throw new Error("NativeScript CLI not found. Use 'nativescript.tnsPath' workspace setting to explicitly set the absolute path to the NativeScript CLI."); - } - let cliVersion: Version = versionStr ? Version.parse(versionStr) : null; - this._cliVersion = new CliVersion(cliVersion, utils.getMinSupportedCliVersion()); - if (!this._cliVersion.isCompatible) { - throw new Error(this._cliVersion.errorMessage); + this._shellPath = 'cmd.exe'; } } public get path(): string { return this._path; } - public get version(): CliVersion { - return this._cliVersion; + public executeGetVersion(): string { + try { + const versionOutput = this.executeSync(['--version'], undefined); + + return this.getVersionFromCLIOutput(versionOutput); + } catch (e) { + this._logger.log(e); + + const errorMessage = `NativeScript CLI not found. Use 'nativescript.tnsPath' workspace setting + to explicitly set the absolute path to the NativeScript CLI.`; + + throw new Error(errorMessage); + } } public executeSync(args: string[], cwd: string): string { - args.unshift("--analyticsClient", "VSCode"); - let command: string = `${this._path} ${args.join(' ')}`; - this._logger.log(`[NativeScriptCli] execute: ${command}\n`, Tags.FrontendMessage); + args.unshift('--analyticsClient', 'VSCode'); + const command: string = `${this._path} ${args.join(' ')}`; - return execSync(command, { encoding: "utf8", cwd: cwd, shell: this._shellPath}).toString().trim(); + this._logger.log(`[NativeScriptCli] execute: ${command}`); + + return execSync(command, { encoding: 'utf8', cwd, shell: this._shellPath}).toString().trim(); } public execute(args: string[], cwd: string): ChildProcess { - args.unshift("--analyticsClient", "VSCode"); - let command: string = `${this._path} ${args.join(' ')}`; - this._logger.log(`[NativeScriptCli] execute: ${command}\n`, Tags.FrontendMessage); + args.unshift('--analyticsClient', 'VSCode'); + const command: string = `${this._path} ${args.join(' ')}`; + + this._logger.log(`[NativeScriptCli] execute: ${command}`); + + const options = { cwd, shell: this._shellPath }; + const child: ChildProcess = spawn(this._path, args, options); - let options = { cwd: cwd, shell: this._shellPath }; - let child: ChildProcess = spawn(this._path, args, options); child.stdout.setEncoding('utf8'); child.stderr.setEncoding('utf8'); + return child; } + + private getVersionFromCLIOutput(commandOutput: string): string { + const matches = commandOutput.match(NativeScriptCli.CLI_OUTPUT_VERSION_REGEXP); + + return matches && matches[0]; + } } diff --git a/src/project/project.ts b/src/project/project.ts index 4783ec5..7e96475 100644 --- a/src/project/project.ts +++ b/src/project/project.ts @@ -1,11 +1,10 @@ -import {ChildProcess} from 'child_process'; -import {EventEmitter} from 'events'; -import {Version} from '../common/version'; -import {NativeScriptCli} from './nativeScriptCli'; +import { ChildProcess } from 'child_process'; +import { EventEmitter } from 'events'; import * as stream from 'stream'; +import { NativeScriptCli } from './nativeScriptCli'; import * as scanner from './streamScanner'; -export type DebugResult = { tnsProcess: ChildProcess, tnsOutputEventEmitter: EventEmitter }; +export interface IDebugResult { tnsProcess: ChildProcess; tnsOutputEventEmitter: EventEmitter; } export abstract class Project { private _appRoot: string; @@ -26,21 +25,21 @@ export abstract class Project { return this.executeRunCommand(tnsArgs); } - public abstract attach(tnsArgs?: string[]): DebugResult; + public abstract attach(tnsArgs?: string[]): IDebugResult; - public abstract debug(options: { stopOnEntry: boolean, watch: boolean }, tnsArgs?: string[]): DebugResult; + public abstract debug(options: { stopOnEntry: boolean, watch: boolean }, tnsArgs?: string[]): IDebugResult; protected configureReadyEvent(readableStream: stream.Readable, eventEmitter: EventEmitter): void { - new scanner.StringMatchingScanner(readableStream).onEveryMatch("TypeScript compiler failed", () => { + new scanner.StringMatchingScanner(readableStream).onEveryMatch('TypeScript compiler failed', () => { eventEmitter.emit('tsCompilationError'); }); } protected executeRunCommand(args: string[]): ChildProcess { - return this.cli.execute(["run", this.platformName()].concat(args), this._appRoot); + return this._cli.execute(['run', this.platformName()].concat(args), this._appRoot); } protected executeDebugCommand(args: string[]): ChildProcess { - return this.cli.execute(["debug", this.platformName(), "--no-client"].concat(args), this._appRoot); + return this._cli.execute(['debug', this.platformName()].concat(args), this._appRoot); } } diff --git a/src/project/streamScanner.ts b/src/project/streamScanner.ts index 3cc167d..ae64080 100644 --- a/src/project/streamScanner.ts +++ b/src/project/streamScanner.ts @@ -1,17 +1,17 @@ -import * as stream from 'stream'; +import { Readable } from 'stream'; export class StreamScanner { - private _stream: stream.Readable; - private _scanCallback: (data: string, stop: () => void) => void; + private _stream: Readable; + private _scanCallback: (data: string, stop: () => void) => void; - constructor(stream: stream.Readable, scanCallback: (data: string, stop: () => void) => void) { + constructor(stream: Readable, scanCallback: (data: string, stop: () => void) => void) { this._stream = stream; this._scanCallback = scanCallback; - this._stream.on("data", this.scan.bind(this)); - } + this._stream.on('data', this.scan.bind(this)); + } public stop() { - this._stream.removeListener("data", this.scan); + this._stream.removeListener('data', this.scan); } private scan(data: string | Buffer): void { @@ -19,74 +19,80 @@ export class StreamScanner { } } -export type MatchFound = { - chunk: string, - matches: RegExpMatchArray | number[] -}; +export interface IMatchFound { + chunk: string; + matches: RegExpMatchArray | number[]; +} -type MatchMeta = { - promise: Promise, - resolve: (match: MatchFound) => void, - reject: (err: Error) => void, - test: string | RegExp -}; +interface IMatchMeta { + promise: Promise; + resolve: (match: IMatchFound) => void; + reject: (err: Error) => void; + test: string | RegExp; +} export class StringMatchingScanner extends StreamScanner { - private _metas: MatchMeta[]; + private metas: IMatchMeta[]; - constructor(stream: stream.Readable) { + constructor(stream: Readable) { super(stream, (data: string, stop: () => void) => { - this._metas.forEach((meta, metaIndex) => { + this.metas.forEach((meta, metaIndex) => { if (meta.test instanceof RegExp) { - let result: RegExpMatchArray = data.match(meta.test); + const result: RegExpMatchArray = data.match(meta.test as RegExp); + if (result && result.length > 0) { this.matchFound(metaIndex, { chunk: data, matches: result }); } - } - else if (typeof meta.test === 'string') { - let result: number[] = []; // matches indices - let dataIndex = -1; - while((dataIndex = data.indexOf(meta.test, dataIndex + 1)) > -1) { + } else if (typeof meta.test === 'string') { + const result: number[] = []; // matches indices + let dataIndex = data.indexOf(meta.test as string, 0); + + while (dataIndex > -1) { result.push(dataIndex); + dataIndex = data.indexOf(meta.test as string, dataIndex + 1); } if (result.length > 0) { this.matchFound(metaIndex, { chunk: data, matches: result }); } - } - else { - throw new TypeError("Invalid type"); + } else { + throw new TypeError('Invalid type'); } }); }); - this._metas = []; + + this.metas = []; } - public onEveryMatch(test: string | RegExp, handler: (result: MatchFound) => void) { - let handlerWrapper = (result: MatchFound) => { + public onEveryMatch(test: string | RegExp, handler: (result: IMatchFound) => void) { + const handlerWrapper = (result: IMatchFound) => { handler(result); this.nextMatch(test).then(handlerWrapper); }; + this.nextMatch(test).then(handlerWrapper); } - public nextMatch(test: string | RegExp): Promise { - let meta: MatchMeta = { - test: test, - resolve: null, + public nextMatch(test: string | RegExp): Promise { + const meta: IMatchMeta = { + promise: null, reject: null, - promise: null + resolve: null, + test, }; - meta.promise = new Promise((resolve, reject) => { + + meta.promise = new Promise((resolve, reject) => { meta.resolve = resolve; meta.reject = reject; }); - this._metas.push(meta); + this.metas.push(meta); + return meta.promise; } - private matchFound(matchMetaIndex: number, matchResult: MatchFound) { - let meta: MatchMeta = this._metas[matchMetaIndex]; - this._metas.splice(matchMetaIndex, 1); // remove the meta + private matchFound(matchMetaIndex: number, matchResult: IMatchFound) { + const meta: IMatchMeta = this.metas[matchMetaIndex]; + + this.metas.splice(matchMetaIndex, 1); // remove the meta meta.resolve(matchResult); } } diff --git a/src/services/channelLogger.ts b/src/services/channelLogger.ts new file mode 100644 index 0000000..e72ae65 --- /dev/null +++ b/src/services/channelLogger.ts @@ -0,0 +1,17 @@ +import { OutputChannel } from 'vscode'; +import { ILogger, LogLevel } from '../common/logger'; + +export class ChannelLogger implements ILogger { + private minLogLevel: LogLevel = LogLevel.Log; + private channel: OutputChannel; + + constructor(channel: OutputChannel) { + this.channel = channel; + } + + public log(msg: string, level: LogLevel = LogLevel.Log): void { + if (level >= this.minLogLevel) { + this.channel.appendLine(msg); + } + } +} diff --git a/src/services/debugAdapterServices.ts b/src/services/debugAdapterServices.ts deleted file mode 100644 index 8cec0ea..0000000 --- a/src/services/debugAdapterServices.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {Services as BaseServices} from './services'; -import {ExtensionClient} from '../ipc/extensionClient'; -import {Logger} from '../common/logger'; - -export class DebugAdapterServices extends BaseServices { - private _extensionClient: ExtensionClient; - private _appRoot: string; - - public get appRoot(): string { return this._appRoot; } - - public set appRoot(appRoot: string) { this._appRoot = appRoot; } - - public extensionClient(): ExtensionClient { - if (!this._extensionClient && !this._appRoot) { - throw new Error("appRoot has no value."); - } - this._extensionClient = this._extensionClient || new ExtensionClient(this._appRoot); - return this._extensionClient; - } -} - -export let Services = new DebugAdapterServices(); \ No newline at end of file diff --git a/src/services/extensionHostServices.ts b/src/services/extensionHostServices.ts index 855c5b2..b8210d8 100644 --- a/src/services/extensionHostServices.ts +++ b/src/services/extensionHostServices.ts @@ -1,34 +1,40 @@ import * as vscode from 'vscode'; -import {Services as BaseServices} from './services'; -import {ExtensionServer} from '../ipc/extensionServer'; -import {AnalyticsService} from '../analytics/analyticsService'; -import {WorkspaceConfigService} from '../common/workspaceConfigService'; +import { AnalyticsService } from '../analytics/analyticsService'; +import { WorkspaceConfigService } from '../common/workspaceConfigService'; +import { iOSTeamService } from './iOSTeamService'; +import { Services as BaseServices } from './services'; export class ExtensionHostServices extends BaseServices { + public cliVersion: string; + public extensionVersion: string; + private _globalState: vscode.Memento; private _workspaceConfigService: WorkspaceConfigService; - private _extensionServer: ExtensionServer; + private _iOSTeamService: iOSTeamService; private _analyticsService: AnalyticsService; public get globalState(): vscode.Memento { return this._globalState; } public set globalState(globalState: vscode.Memento) { this._globalState = globalState; } - public workspaceConfigService(): WorkspaceConfigService { + public get workspaceConfigService(): WorkspaceConfigService { this._workspaceConfigService = this._workspaceConfigService || new WorkspaceConfigService(); + return this._workspaceConfigService; } - public extensionServer(): ExtensionServer { - this._extensionServer = this._extensionServer || new ExtensionServer(); - return this._extensionServer; + public get iOSTeamService(): iOSTeamService { + this._iOSTeamService = this._iOSTeamService || new iOSTeamService(); + + return this._iOSTeamService; } - public analyticsService(): AnalyticsService { - this._analyticsService = this._analyticsService || new AnalyticsService(this.globalState); + public get analyticsService(): AnalyticsService { + this._analyticsService = this._analyticsService || new AnalyticsService(this.globalState, this.cliVersion, this.extensionVersion, this._logger); + return this._analyticsService; } } -export let Services = new ExtensionHostServices(); \ No newline at end of file +export let services = new ExtensionHostServices(); diff --git a/src/services/iOSTeamService.ts b/src/services/iOSTeamService.ts new file mode 100644 index 0000000..6996c37 --- /dev/null +++ b/src/services/iOSTeamService.ts @@ -0,0 +1,94 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; + +// tslint:disable-next-line:class-name +export class iOSTeamService { + public selectTeam(): Promise<{ id: string, name: string }> { + return new Promise((resolve, reject) => { + const workspaceTeamId = vscode.workspace.getConfiguration().get('nativescript.iosTeamId'); + + if (workspaceTeamId) { + resolve({ + id: workspaceTeamId, + name: undefined, // irrelevant + }); + + return; + } + + const developmentTeams = this.getDevelopmentTeams(); + + if (developmentTeams.length > 1) { + const quickPickItems: vscode.QuickPickItem[] = developmentTeams.map((team) => { + return { + description: team.id, + label: team.name, + }; + }); + + vscode.window.showQuickPick( + quickPickItems, { + placeHolder: 'Select your development team', + }) + .then((val: vscode.QuickPickItem) => { + vscode.workspace.getConfiguration().update('nativescript.iosTeamId', val.description); + resolve({ + id: val.description, + name: val.label, + }); + }); + } else { + resolve(); + } + }); + } + + private getDevelopmentTeams(): Array<{ id: string, name: string }> { + try { + const dir = path.join(process.env.HOME, 'Library/MobileDevice/Provisioning Profiles/'); + const files = fs.readdirSync(dir); + const teamIds: any = {}; + + for (const file of files) { + const filePath = path.join(dir, file); + const data = fs.readFileSync(filePath, {encoding: 'utf8'}); + const teamId = this.getProvisioningProfileValue('TeamIdentifier', data); + const teamName = this.getProvisioningProfileValue('TeamName', data); + + if (teamId) { + teamIds[teamId] = teamName; + } + } + + const teamIdsArray = new Array<{ id: string, name: string }>(); + + for (const teamId in teamIds) { + teamIdsArray.push({id: teamId, name: teamIds[teamId]}); + } + + return teamIdsArray; + } catch (e) { + // no matter what happens, don't break + return new Array<{ id: string, name: string }>(); + } + } + + private getProvisioningProfileValue(name: string, text: string): string { + const findStr = '' + name + ''; + let index = text.indexOf(findStr); + + if (index > 0) { + index = text.indexOf('', index + findStr.length); + if (index > 0) { + index += ''.length; + const endIndex = text.indexOf('', index); + const result = text.substring(index, endIndex); + + return result; + } + } + + return null; + } +} diff --git a/src/services/services.ts b/src/services/services.ts index 56b684e..417c3bd 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1,23 +1,23 @@ -import {Logger} from '../common/logger'; -import {NativeScriptCli} from '../project/nativeScriptCli'; +import { ILogger } from '../common/logger'; +import { NativeScriptCli } from '../project/nativeScriptCli'; export class Services { protected _cliPath: string; - protected _logger: Logger; + protected _logger: ILogger; protected _cli: NativeScriptCli; public get cliPath(): string { return this._cliPath; } public set cliPath(cliPath: string) { this._cliPath = cliPath; } - public logger(): Logger { - this._logger = this._logger || new Logger(); - return this._logger; - } + public get logger(): ILogger { return this._logger; } + + public set logger(logger: ILogger) { this._logger = logger; } public cli(): NativeScriptCli { - this._cli = this._cli || new NativeScriptCli(this._cliPath, this.logger()); + this._cli = this._cli || new NativeScriptCli(this._cliPath, this.logger); + return this._cli; } } diff --git a/src/tests/.vscode/launch.json b/src/tests/.vscode/launch.json deleted file mode 100644 index 9eb9aa5..0000000 --- a/src/tests/.vscode/launch.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "version": "0.1.0", - "configurations": [ - { - "name": "run tests on mac", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/../../node_modules/mocha/bin/_mocha", - "runtimeArgs": [ "--nolazy" ], - "args": [ - "--opts", "${workspaceRoot}/config/mocha.opts", - "--config", "${workspaceRoot}/config/mac.json", - "${workspaceRoot}/../../out/tests/" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}/../../out", - "cwd": "${workspaceRoot}/../" - }, - { - "name": "run tests on win", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/../../node_modules/mocha/bin/_mocha", - "runtimeArgs": [ "--nolazy" ], - "args": [ - "--opts", "${workspaceRoot}/config/mocha.opts", - "--config", "${workspaceRoot}/config/win.json", - "${workspaceRoot}/../../out/tests/" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}/../../out", - "cwd": "${workspaceRoot}/../" - }, - { - "name": "run tests (custom)", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/../../node_modules/mocha/bin/_mocha", - "runtimeArgs": [ "--nolazy" ], - "args": [ - "--opts", "${workspaceRoot}/config/mocha.opts", - "--config", "${workspaceRoot}/config/custom.json", - "${workspaceRoot}/../../out/tests/" - ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "${workspaceRoot}/../../out", - "cwd": "${workspaceRoot}/../" - } - ] -} \ No newline at end of file diff --git a/src/tests/.vscode/tasks.json b/src/tests/.vscode/tasks.json deleted file mode 100644 index 0d71c21..0000000 --- a/src/tests/.vscode/tasks.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": "0.1.0", - "windows": { - "command": "..\\..\\node_modules\\.bin\\tsc" - }, - "command": "../../node_modules/.bin/tsc", - "isShellCommand": true, - "args": ["-p", "../"], - "problemMatcher": "$tsc" -} \ No newline at end of file diff --git a/src/tests/adapter.test.ts b/src/tests/adapter.test.ts deleted file mode 100644 index 9a4123d..0000000 --- a/src/tests/adapter.test.ts +++ /dev/null @@ -1,439 +0,0 @@ -import * as path from 'path'; -import * as assert from 'assert'; -import {DebugProtocol} from 'vscode-debugprotocol'; -import {NsDebugClient} from './nsDebugClient'; -import {Scenario} from './scenario'; -import {TestsConfig, TestsContext} from './testsContext'; - -describe('The adapter', () => { - - let dc: NsDebugClient; - let context: TestsContext = new TestsContext(); - let config: TestsConfig = context.getTestsConfig(); - - console.log(`Tests Configuration: ${JSON.stringify(config)}`); - - function waitFor(miliseconds) { - return new Promise(r => setTimeout(r, miliseconds)); - } - - function iosOrAndroid(platform, iosValue, androidValue) { - return platform == 'ios' ? iosValue : androidValue; - } - - before(function() { - if (!config.skipSuitePrepare) { - context.prepare(); - } - - dc = new NsDebugClient('node', context.getDebugAdapterMainPath(), 'nativescript'); - // dc.setTimeout(0); // No timeout. Useful when debugging the debug adapter. - }); - - beforeEach(function(done) { - dc.start(config.port || undefined).then(_ => done(), done); - }); - - afterEach(function(done) { - dc.removeAllListeners('stopped'); - dc.removeAllListeners('initialized'); - dc.stop().then(_ => waitFor(10000)).then(_ => done(), done); // Stop the DebugClient - }); - - it('should produce error on unknown request', done => { - dc.send('illegal_request').then(() => { - done(new Error('doesn\'t produce error error on unknown request')); - }, err => { done() }); - }); - - it('should return supported features', () => { - return dc.initializeRequest().then(response => { - assert.equal(response.body.supportsFunctionBreakpoints, false); - assert.equal(response.body.supportsConfigurationDoneRequest, true); - }); - }); - - it.skip('should produce error for invalid \'pathFormat\'', done => { - dc.initializeRequest({ - adapterID: 'mock', - linesStartAt1: true, - columnsStartAt1: true, - pathFormat: 'url' - }).then(response => { - done(new Error('does not report error on invalid \'pathFormat\' attribute')); - }).catch(err => { - done(); // error expected - }); - }); - - // Test cases are generated for all active platforms - config.platforms.forEach(platform => { - let meta = `on ${platform} ${config.emulator ? 'emulator' : 'device'}`; - let initialStopReason = iosOrAndroid(platform, 'pause', 'step'); - let breakpointStopReason = iosOrAndroid(platform, 'breakpoint', 'step'); - let exceptionStopReason = iosOrAndroid(platform, 'exception', 'step'); - - it(`${meta} should stop on the first line after launch`, () => { - let appRoot = context.getAppPath('JsApp'); - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.start(); - return scenario.client.assertStoppedLocation(initialStopReason, { line: 1 }); - }); - - it(`${meta} should disconnect`, () => { - let appRoot = context.getAppPath('JsApp'); - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - return scenario.start().then(() => { - return scenario.client.disconnectRequest(); - }); - }); - - it(`${meta} should stop on debugger statement`, () => { - let appRoot = context.getAppPath('DebuggerStatement'); - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - // continue after first stop - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.start(); - return scenario.client.assertNthStoppedLocation(2, 'step', { line: 4 }); - }); - - it(`${meta} should stop on breakpoint`, () => { - let appRoot = context.getAppPath('JsApp'); - let bpPath = path.join(appRoot, 'app', 'main-page.js'); - let bpLine = 5; - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.beforeConfigurationDoneRequest = scenario.beforeConfigurationDoneRequest.then(() => { - return scenario.client.assertSetBreakpoints(bpPath, [{ line: bpLine }]); - }); - - scenario.start(); - return scenario.client.assertNthStoppedLocation(2, breakpointStopReason, { path: bpPath, line: bpLine, column: 4 }); - }); - - it.skip(`${meta} should stop on breakpoint in file with spaces in its name`, () => { - let appRoot = context.getAppPath('TestApp1'); - let bpPath = path.join(appRoot, 'app', 'file with space in name.js'); - let bpLine = 5; - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.beforeConfigurationDoneRequest = scenario.beforeConfigurationDoneRequest.then(() => { - return scenario.client.assertSetBreakpoints(bpPath, [{ line: bpLine }]); - }); - - scenario.start(); - return scenario.client.assertNthStoppedLocation(2, breakpointStopReason, { path: bpPath, line: bpLine, column: 0 }); - }); - - it(`${meta} should stop on conditional breakpoint when the condition is true`, () => { - let appRoot = context.getAppPath('TestApp1'); - let bpPath = path.join(appRoot, 'app', 'conditionalBreakpoint.js'); - let bpLine = 2; - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.beforeConfigurationDoneRequest = scenario.beforeConfigurationDoneRequest.then(() => { - return scenario.client.assertSetBreakpoints(bpPath, [{ line: bpLine, condition: 'a === 3' }]); - }); - - scenario.start(); - return scenario.client.assertNthStoppedLocation(2, breakpointStopReason, { path: bpPath, line: bpLine, column: 0 }) - .then(response => { - const frame = response.body.stackFrames[0]; - return scenario.client.evaluateRequest({ context: 'watch', frameId: frame.id, expression: 'a' }).then(response => { - assert.equal(response.body.result, 3, 'a !== 3'); - return response; - }); - }); - }); - - it(`${meta} should stop on typescript breakpoint`, () => { - let appRoot = context.getAppPath('TsApp'); - let bpPath = path.join(appRoot, 'app', 'main-page.ts'); - let bpLine = 9; - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.beforeConfigurationDoneRequest = scenario.beforeConfigurationDoneRequest.then(() => { - return scenario.client.assertSetBreakpoints(bpPath, [{ line: bpLine }]); - }); - - scenario.start(); - return scenario.client.assertNthStoppedLocation(2, breakpointStopReason, { path: bpPath, line: bpLine, column: 4 }); - }); - - it(`${meta} should stop on typescript even if breakpoint was set in JavaScript`, () => { - let appRoot = context.getAppPath('TsApp'); - let jsPath = path.join(appRoot, 'app', 'main-page.js'); - let jsLine = 7; - let tsPath = path.join(appRoot, 'app', 'main-page.ts'); - let tsLine = 9; - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.beforeConfigurationDoneRequest = scenario.beforeConfigurationDoneRequest.then(() => { - return scenario.client.assertSetBreakpoints(jsPath, [{ line: jsLine }]); - }); - - scenario.start(); - return scenario.client.assertNthStoppedLocation(2, breakpointStopReason, { path: tsPath, line: tsLine, column: 4 }); - }); - - it(`${meta} should stop on caught error`, () => { - let appRoot = context.getAppPath('TestApp2'); - let breakpointColumn = iosOrAndroid(platform, 35, 4); - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.beforeConfigurationDoneRequest = scenario.beforeConfigurationDoneRequest.then(() => { - return scenario.client.setExceptionBreakpointsRequest({ filters: ['all'] }); - }); - - scenario.start(); - return scenario.client.assertNthStoppedLocation(2, exceptionStopReason, { path: path.join(appRoot, 'app', 'app.js'), line: 3, column: breakpointColumn }); - }); - - it.skip(`${meta} should stop on uncaught error`, () => { - let appRoot = context.getAppPath('TestApp2'); - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.beforeConfigurationDoneRequest = scenario.beforeConfigurationDoneRequest.then(() => { - return scenario.client.setExceptionBreakpointsRequest({ filters: ['uncaught'] }); - }); - - scenario.start(); - return scenario.client.assertNthStoppedLocation(2, exceptionStopReason, { path: path.join(appRoot, 'app', 'app.js'), line: 9, column: 24 }); - }); - - it(`${meta} should receive output event when console.log is called`, () => { - let appRoot = context.getAppPath('TestApp1'); - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.afterLaunchRequest = scenario.afterLaunchRequest.then(() => { - return scenario.client.onNextTime('output').then(e => { - let event = e as DebugProtocol.OutputEvent; - assert.equal(event.body.category, 'stdout', 'message category mismatch'); - assert.equal(event.body.output.startsWith('console.log called'), true, 'message mismatch'); - }); - }); - scenario.start(); - return scenario.afterLaunchRequest; - }); - - it(`${meta} should receive 2 output events when console.log is called twice with the same message`, () => { - let appRoot = context.getAppPath('TestApp1'); - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.afterLaunchRequest = scenario.afterLaunchRequest.then(() => { - let assertOutputEvent = e => { - let event = e as DebugProtocol.OutputEvent; - assert.equal(event.body.category, 'stdout', 'message category mismatch'); - assert.equal(event.body.output.startsWith('console.log called'), true, 'message mismatch'); - }; - - return Promise.all([ - scenario.client.onNthTime(1, 'output').then(assertOutputEvent), - scenario.client.onNthTime(2, 'output').then(assertOutputEvent)]); - }); - - scenario.start(); - return scenario.afterLaunchRequest; - }); - - it(`${meta} should step over`, () => { - let appRoot = context.getAppPath('JsApp'); - let filePath = path.join(appRoot, 'app', 'main-view-model.js'); - let bpLine = 12; - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.beforeConfigurationDoneRequest = scenario.beforeConfigurationDoneRequest.then(() => { - return scenario.client.assertSetBreakpoints(filePath, [{ line: bpLine }]); - }); - - scenario.start(); - - return Promise.all([ - scenario.client.assertNthStoppedLocation(2, breakpointStopReason, { path: filePath, line: bpLine, column: 4 }).then(() => { - return scenario.client.nextRequest({ threadId: 1 }); - }), - scenario.client.assertNthStoppedLocation(3, 'step', { path: filePath, line: bpLine + 1, column: 4 }).then(() => { - return scenario.client.nextRequest({ threadId: 1 }); - }), - scenario.client.assertNthStoppedLocation(4, 'step', { path: filePath, line: bpLine + 2, column: 4 }) - ]); - }); - - it(`${meta} should step in`, () => { - let appRoot = context.getAppPath('JsApp'); - let filePath = path.join(appRoot, 'app', 'main-view-model.js'); - let bpLine = 14; - - let firstStepExpected = iosOrAndroid(platform, { path: filePath, line: 3, column: 19 }, { path: filePath, line: 4, column: 4 }); - let secondStepExpected = iosOrAndroid(platform, { path: filePath, line: 4, column: 4 }, { path: filePath, line: 7, column: 8 }); - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.beforeConfigurationDoneRequest = scenario.beforeConfigurationDoneRequest.then(() => { - return scenario.client.assertSetBreakpoints(filePath, [{ line: bpLine }]); - }); - - scenario.start(); - - return Promise.all([ - scenario.client.assertNthStoppedLocation(2, breakpointStopReason, { path: filePath, line: bpLine, column: 4 }).then(() => { - return scenario.client.stepInRequest({ threadId: 1 }); - }), - scenario.client.assertNthStoppedLocation(3, 'step', firstStepExpected).then(() => { - return scenario.client.stepInRequest({ threadId: 1 }); - }), - scenario.client.assertNthStoppedLocation(4, 'step', secondStepExpected) - ]); - }); - - it(`${meta} should pause`, () => { - let appRoot = context.getAppPath('JsApp'); - let filePath = path.join(appRoot, 'app', 'main-view-model.js'); - let bpLine = 14; - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - - return scenario.start().then(() => { - return scenario.client.pauseRequest({ threadId: 1 }); - }); - }); - - it(`${meta} should evaluate expression when stopped on breakpoint`, () => { - let appRoot = context.getAppPath('JsApp'); - let filePath = path.join(appRoot, 'app', 'main-view-model.js'); - let bpLine = 12; - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.beforeConfigurationDoneRequest = scenario.beforeConfigurationDoneRequest.then(() => { - return scenario.client.assertSetBreakpoints(filePath, [{ line: bpLine }]); - }); - - scenario.start(); - - return scenario.client.onNthTime(2, 'stopped').then(() => { - return scenario.client.stackTraceRequest({ threadId: 1, levels: 20 }).then((response) => { - return scenario.client.evaluateRequest({ - expression: 'getMessage(-5)', - context: 'watch', - frameId: response.body.stackFrames[0].id - }) - .then((response) => { - assert.equal(response.body.result, '"Hoorraaay! You unlocked the NativeScript clicker achievement!"', 'result mismatch'); - }); - }); - }); - }); - - it(`${meta} should return all threads`, () => { - let appRoot = context.getAppPath('JsApp'); - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - - return scenario.start().then(() => { - scenario.client.threadsRequest().then(response => { - assert.deepEqual(response.body.threads, [{ id: 1, name: 'Thread 1' }]); - }); - }); - }); - - it(`${meta} should return stack trace`, () => { - let appRoot = context.getAppPath('JsApp'); - let filePath = path.join(appRoot, 'app', 'main-view-model.js'); - let bpLine = 13; - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - scenario.beforeConfigurationDoneRequest = scenario.beforeConfigurationDoneRequest.then(() => { - return scenario.client.assertSetBreakpoints(filePath, [{ line: bpLine }]); - }); - - scenario.start(); - - return scenario.client.onNthTime(2, 'stopped').then(e => { - return scenario.client.stackTraceRequest({ threadId: e.body.threadId }).then(response => { - let stackFrames = response.body.stackFrames; - let firstFrame = stackFrames[0]; - let lastFrame = stackFrames[stackFrames.length - 1]; - let expectedStackFramesCount = iosOrAndroid(platform, 22, 10); - assert.equal(stackFrames.length, expectedStackFramesCount, 'wrong stack frames count'); - assert.equal(firstFrame.name, 'createViewModel', 'wrong top frame name'); - assert.equal(firstFrame.source.path, filePath, 'wrong top frame path'); - assert.equal(lastFrame.name, 'promiseReactionJob', 'wrong last frame name'); - }); - }); - }); - - it(`${meta} should attach`, () => { - let appRoot = context.getAppPath('JsApp'); - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - scenario.client.onNextTime('stopped').then(e => scenario.client.continueRequest({ threadId: 1 })); - - return scenario.start().then(() => { - return scenario.client.disconnectRequest().then(() => { - return scenario.client.attachRequest(Scenario.getDefaultAttachArgs(platform, appRoot, config.emulator)); - }); - }); - }); - - // iOS specifc tests - if (platform == 'ios') { - it(`${meta} should not hang on evaluating watch expression on call frame with unknown source`, () => { - let appRoot = context.getAppPath('JsApp'); - - let scenario = new Scenario(dc); - scenario.launchRequestArgs = Scenario.getDefaultLaunchArgs(platform, appRoot, config.emulator); - - - return Promise.all([ - scenario.start(), - scenario.client.onNextTime('stopped').then(e => { - return scenario.client.stackTraceRequest({ threadId: e.body.threadId }).then(response => { - let callFrame = response.body.stackFrames.filter(callFrame => !callFrame.source.path && !callFrame.source.sourceReference)[0]; - return scenario.client.evaluateRequest({ expression: 'Math.random()', frameId: callFrame.id, context: 'watch' }).then(response => { - assert.fail(undefined, undefined, 'Evaluate request should fail', undefined); - }, response => { - assert.equal(response.message, '-', 'error message mismatch'); - }); - }); - }) - ]); - }); - } - }); -}); \ No newline at end of file diff --git a/src/tests/config/mac.json b/src/tests/config/mac.json deleted file mode 100644 index 45dc5e1..0000000 --- a/src/tests/config/mac.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "platforms": ["android", "ios"], - "emulator": false, - "port": 4712, - "skipSuitePrepare": false -} \ No newline at end of file diff --git a/src/tests/config/mocha.opts b/src/tests/config/mocha.opts index ed83f01..0ae6543 100644 --- a/src/tests/config/mocha.opts +++ b/src/tests/config/mocha.opts @@ -1,4 +1,7 @@ --colors --ui tdd --timeout 60000 ---reporter spec \ No newline at end of file +--reporter spec +--exit + +./out/tests/**/*.tests.js \ No newline at end of file diff --git a/src/tests/config/win.json b/src/tests/config/win.json deleted file mode 100644 index b6e0393..0000000 --- a/src/tests/config/win.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "platforms": ["android"], - "emulator": false, - "port": 4712, - "skipSuitePrepare": false -} \ No newline at end of file diff --git a/src/tests/nativeScriptDebugAdapter.tests.ts b/src/tests/nativeScriptDebugAdapter.tests.ts new file mode 100644 index 0000000..858e142 --- /dev/null +++ b/src/tests/nativeScriptDebugAdapter.tests.ts @@ -0,0 +1,204 @@ +import { EventEmitter } from 'events'; +import * as _ from 'lodash'; +import * as sinon from 'sinon'; +import { ChromeDebugAdapter } from 'vscode-chrome-debug-core'; +import { Event } from 'vscode-debugadapter'; +import * as extProtocol from '../common/extensionProtocol'; +import { nativeScriptDebugAdapterGenerator } from '../debug-adapter/nativeScriptDebugAdapter'; + +const customMessagesResponses = { + workspaceConfigService: { + tnsPath: 'tnsPathMock', + }, +}; + +const defaultArgsMock: any = { + appRoot: 'appRootMock', + diagnosticLogging: true, + platform: 'android', + request: 'attach', + stopOnEntry: true, + tnsArgs: [ 'mockArgs'], + watch: true, +}; + +const mockConstructor = (mockObject: any): any => { + return function() { + return mockObject; + }; +}; + +describe('NativeScriptDebugAdapter', () => { + let nativeScriptDebugAdapter: any; + let chromeSessionMock: any; + let chromeConnectionMock: any; + let projectMock: any; + let nativeScriptCliMock: any; + let cliCommandMock: any; + let pathTransformerMock: any; + + beforeEach(() => { + chromeSessionMock = { + sendEvent: (e: Event) => { + const request = (e as any).body as extProtocol.IRequest; + + if (e.event === extProtocol.NS_DEBUG_ADAPTER_MESSAGE) { + const result = customMessagesResponses[request.service] && customMessagesResponses[request.service][request.method]; + + (nativeScriptDebugAdapter as any).onExtensionResponse({ requestId: request.id, result }); + } + }, + }; + + chromeConnectionMock = { + attach: () => Promise.resolve({}), + }; + + cliCommandMock = { + tnsOutputEventEmitter: { + on: (event, callback) => { + callback(); + }, + }, + }; + + pathTransformerMock = { + attach: () => ({}), + clearTargetContext: () => ({}), + setTargetPlatform: () => ({}), + }; + + projectMock = { + attach: () => cliCommandMock, + debug: () => cliCommandMock, + }; + + nativeScriptCliMock = { + executeGetVersion: () => 'cliVersionMock', + }; + + const projectClass: any = function(appRoot, cli) { + return _.merge({ + appRoot, + cli, + }, projectMock); + }; + + const nativeScriptDebugAdapterClass = nativeScriptDebugAdapterGenerator(projectClass, projectClass, mockConstructor(nativeScriptCliMock)); + + nativeScriptDebugAdapter = new nativeScriptDebugAdapterClass( + { chromeConnection: mockConstructor(chromeConnectionMock), pathTransformer: mockConstructor(pathTransformerMock) }, + chromeSessionMock, + ); + + ChromeDebugAdapter.prototype.attach = () => Promise.resolve(); + }); + + const platforms = [ 'android', 'ios' ]; + const launchMethods = [ 'launch', 'attach' ]; + + platforms.forEach((platform) => { + launchMethods.forEach((method) => { + const argsMock = _.merge({}, defaultArgsMock, { platform, request: method }); + + it(`${method} for ${platform} should raise debug start event`, async () => { + const spy = sinon.spy(chromeSessionMock, 'sendEvent'); + + await nativeScriptDebugAdapter[method](argsMock); + + sinon.assert.calledWith(spy, sinon.match({ event: extProtocol.BEFORE_DEBUG_START })); + }); + + it(`${method} for ${platform} should call analyticsService`, async () => { + const spy = sinon.spy(chromeSessionMock, 'sendEvent'); + + await nativeScriptDebugAdapter[method](argsMock); + + sinon.assert.calledWith(spy, sinon.match({ + body: { + args: [method, platform], + method: 'launchDebugger', + service: 'analyticsService', + }, + event: extProtocol.NS_DEBUG_ADAPTER_MESSAGE, + })); + }); + + it(`${method} for ${platform} should call project setTargetPlatform`, async () => { + const spy = sinon.spy(pathTransformerMock, 'setTargetPlatform'); + + await nativeScriptDebugAdapter[method](argsMock); + + sinon.assert.calledWith(spy, argsMock.platform); + }); + + it(`${method} for ${platform} should set debug port`, async () => { + const port = 1234; + + sinon.stub(cliCommandMock.tnsOutputEventEmitter, 'on').callsFake((event, callback) => callback(port)); + const spy = sinon.spy(ChromeDebugAdapter.prototype, 'attach'); + + await nativeScriptDebugAdapter[method](argsMock); + + sinon.assert.calledWith(spy, sinon.match({ + port, + })); + }); + + it(`${method} for ${platform} should translate args to chrome debug args`, async () => { + const spy = sinon.spy(ChromeDebugAdapter.prototype, 'attach'); + + await nativeScriptDebugAdapter[method](argsMock); + + sinon.assert.calledWith(spy, sinon.match({ + trace: true, + webRoot: 'appRootMock', + })); + }); + + it(`${method} for ${platform} - after process exit should send Terminate event`, async () => { + const spy = sinon.spy(chromeSessionMock, 'sendEvent'); + const fakeEmitter = { + on: () => ({}), + }; + + cliCommandMock.tnsProcess = new EventEmitter(); + cliCommandMock.tnsProcess.stderr = fakeEmitter; + cliCommandMock.tnsProcess.stdout = fakeEmitter; + + await nativeScriptDebugAdapter.attach(argsMock); + cliCommandMock.tnsProcess.emit('close', -1); + + sinon.assert.calledWith(spy, sinon.match({ + event: 'terminated', + })); + }); + }); + }); + + it('attach should call project attach method with correct args', async () => { + const attachSpy = sinon.spy(projectMock, 'attach'); + const debugSpy = sinon.spy(projectMock, 'debug'); + + const argsMock = _.merge({}, defaultArgsMock, { request: 'attach' }); + + await nativeScriptDebugAdapter.attach(argsMock); + + sinon.assert.calledOnce(attachSpy); + sinon.assert.calledWith(attachSpy, argsMock.tnsArgs); + sinon.assert.notCalled(debugSpy); + }); + + it('launch should call project debug method with correct args', async () => { + const attachSpy = sinon.spy(projectMock, 'attach'); + const debugSpy = sinon.spy(projectMock, 'debug'); + + const argsMock = _.merge({}, defaultArgsMock, { request: 'launch' }); + + await nativeScriptDebugAdapter.launch(argsMock); + + sinon.assert.calledOnce(debugSpy); + sinon.assert.calledWith(debugSpy, { stopOnEntry: argsMock.stopOnEntry, watch: argsMock.watch }, argsMock.tnsArgs); + sinon.assert.notCalled(attachSpy); + }); +}); diff --git a/src/tests/nativeScriptPathTransformer.tests.ts b/src/tests/nativeScriptPathTransformer.tests.ts new file mode 100644 index 0000000..691bd1b --- /dev/null +++ b/src/tests/nativeScriptPathTransformer.tests.ts @@ -0,0 +1,37 @@ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { NativeScriptPathTransformer } from '../debug-adapter/nativeScriptPathTransformer'; +import * as tests from './pathTransformData'; + +describe('NativeScriptPathTransformer', () => { + let nativeScriptPathTransformer: any; + let existsSyncStub; + + before(() => { + nativeScriptPathTransformer = new NativeScriptPathTransformer(); + }); + + describe('targetUrlToClientPath() method', () => { + const webRoot = 'C:\\projectpath'; + + for (const test of tests as any) { + it(`should transform [${test.platform}] device path ${test.scriptUrl} -> ${test.expectedResult}`, async () => { + (path as any).join = path.win32.join; + (path as any).resolve = path.win32.resolve; + nativeScriptPathTransformer.setTargetPlatform(test.platform); + existsSyncStub = sinon.stub(fs, 'existsSync').callsFake((arg: string) => arg === test.existingPath); + + const result = await nativeScriptPathTransformer.targetUrlToClientPath(webRoot, test.scriptUrl); + + assert.equal(result, test.expectedResult); + }); + } + + afterEach(() => { + existsSyncStub.restore(); + }); + }); + +}); diff --git a/src/tests/nativeScriptTargetDiscovery.tests.ts b/src/tests/nativeScriptTargetDiscovery.tests.ts new file mode 100644 index 0000000..18d6749 --- /dev/null +++ b/src/tests/nativeScriptTargetDiscovery.tests.ts @@ -0,0 +1,45 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { NativeScriptTargetDiscovery } from '../debug-adapter/nativeScriptTargetDiscovery'; + +describe('NativeScriptTargetDiscovery', () => { + let nativeScriptTargetDiscovery: NativeScriptTargetDiscovery; + let stub; + + before(() => { + nativeScriptTargetDiscovery = new NativeScriptTargetDiscovery(); + }); + + it(`getTarget returns correct target`, async () => { + const address = 'localhost'; + const port = 41000; + + const target = await nativeScriptTargetDiscovery.getTarget(address, port); + + assert.equal(target.webSocketDebuggerUrl, `ws://${address}:${port}`); + assert.equal(target.devtoolsFrontendUrl, `chrome-devtools://devtools/bundled/inspector.html?experiments=true&ws=${address}:${port}`); + }); + + it(`getTargets calls getTarget`, async () => { + const testTarget = { + devtoolsFrontendUrl: 'url', + webSocketDebuggerUrl: 'socket', + }; + const address = 'localhost'; + const port = 41000; + + stub = sinon.stub(nativeScriptTargetDiscovery, 'getTarget').callsFake(() => Promise.resolve(testTarget)); + const targets = await nativeScriptTargetDiscovery.getAllTargets(address, port); + + sinon.assert.calledOnce(stub); + sinon.assert.calledWith(stub, address, port); + assert.equal(targets.length, 1); + assert.deepEqual(targets[0], testTarget); + }); + + afterEach(() => { + if (stub) { + stub.restore(); + } + }); +}); diff --git a/src/tests/nsDebugClient.ts b/src/tests/nsDebugClient.ts deleted file mode 100644 index 57581f4..0000000 --- a/src/tests/nsDebugClient.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as assert from 'assert'; -import {DebugProtocol} from 'vscode-debugprotocol'; -import {DebugClient} from 'vscode-debugadapter-testsupport'; - -export class NsDebugClient extends DebugClient { - private _timeout: number = 90000; - - public getTimeout(): number { - return this._timeout; - } - - public setTimeout(timeout: number) { - this._timeout = timeout; - } - - // The method adds the following enhancements to its base implementation - // 1. It has no hardcoded value for the timeout - // 2. It removes the event listener after it is no needed anymore - public waitForEvent(eventType: string, timeout?: number): Promise { - timeout = timeout || this._timeout; - return new Promise((resolve, reject) => { - let eventListener: (event: DebugProtocol.Event) => any = (event) => { - resolve(event); - this.removeListener(eventType, eventListener); - }; - this.on(eventType, eventListener); - if (timeout) { - setTimeout(() => { - reject(new Error("no event '" + eventType + "' received after " + timeout + " ms")); - }, timeout); - } - }); - } - - public onNextTime(event: string): Promise { - return this.waitForEvent(event); - } - - public onNthTime(n: number, event: string): Promise { - return n == 1 ? - this.onNextTime(event) : - this.onNextTime(event).then(e => this.onNthTime(--n, event)); - } - - public assertSetBreakpoints(path: string, breakpoints: { line: number, condition?: string }[]): Promise { - return this.setBreakpointsRequest({ - lines: breakpoints.map(b => b.line), - breakpoints: breakpoints, - source: { path: path } - }) - .then(response => { - response.body.breakpoints.forEach((bp, i, a) => { - assert.equal(bp.verified, true, 'breakpoint verification mismatch: verified'); - assert.equal(bp.line, breakpoints[i].line, 'breakpoint verification mismatch: line'); - //assert.equal(bp.column, breakpointColumn, 'breakpoint verification mismatch: column'); - }); - return Promise.resolve(); - }); - } - - public assertNthStoppedLocation(n: number, reason: string, expected: { path?: string; line?: number; column?: number; }) - : Promise { - return n == 1 ? - this.assertStoppedLocation(reason, expected) : - this.onNextTime('stopped').then(e => this.assertNthStoppedLocation(--n, reason, expected)); - } -} \ No newline at end of file diff --git a/src/tests/pathTransformData.ts b/src/tests/pathTransformData.ts new file mode 100644 index 0000000..4e0c978 --- /dev/null +++ b/src/tests/pathTransformData.ts @@ -0,0 +1,20 @@ +/* tslint:disable:max-line-length */ +const tests = [ + { platform: 'android', scriptUrl: 'file:///data/data/org.nativescript.TabNavigation/files/app/main.js', expectedResult: 'C:\\projectpath\\app\\main.js', existingPath: 'C:\\projectpath\\app\\main.js' }, + { platform: 'android', scriptUrl: 'VM1', expectedResult: 'VM1' }, + { platform: 'android', scriptUrl: 'native prologue.js', expectedResult: 'native prologue.js' }, + { platform: 'android', scriptUrl: 'v8/gc', expectedResult: 'v8/gc' }, + { platform: 'android', scriptUrl: 'VM25', expectedResult: 'VM25' }, + { platform: 'android', scriptUrl: '/data/data/org.nativescript.TabNavigation/files/internal/ts_helpers.js', expectedResult: '/data/data/org.nativescript.TabNavigation/files/internal/ts_helpers.js' }, + { platform: 'android', scriptUrl: 'file:///data/data/org.nativescript.TabNavigation/files/app/tns_modules/nativescript-angular/platform.js', expectedResult: 'C:\\projectpath\\node_modules\\nativescript-angular\\platform.js', existingPath: 'C:\\projectpath\\node_modules\\nativescript-angular\\platform.js' }, + { platform: 'android', scriptUrl: 'file:///data/data/org.nativescript.TabNavigation/files/app/tns_modules/nativescript-angular/platform-common.js', expectedResult: 'C:\\projectpath\\node_modules\\nativescript-angular\\platform-common.js', existingPath: 'C:\\projectpath\\node_modules\\nativescript-angular\\platform-common.js' }, + { platform: 'android', scriptUrl: 'file:///data/data/org.nativescript.TabNavigation/files/app/tns_modules/@angular/common/bundles/common.umd.js', expectedResult: 'C:\\projectpath\\node_modules\\@angular\\common\\bundles\\common.umd.js', existingPath: 'C:\\projectpath\\node_modules\\@angular\\common\\bundles\\common.umd.js' }, + { platform: 'android', scriptUrl: 'file:///data/data/org.nativescript.TabNavigation/files/app/tns_modules/tns-core-modules/ui/gestures/gestures.js', expectedResult: 'C:\\projectpath\\node_modules\\tns-core-modules\\ui\\gestures\\gestures.android.js', existingPath: 'C:\\projectpath\\node_modules\\tns-core-modules\\ui\\gestures\\gestures.android.js' }, + { platform: 'android', scriptUrl: 'file:///data/data/org.nativescript.TabNavigation/files/app/tns_modules/tns-core-modules/ui/frame/fragment.transitions.js', expectedResult: 'C:\\projectpath\\node_modules\\tns-core-modules\\ui\\frame\\fragment.transitions.android.js', existingPath: 'C:\\projectpath\\node_modules\\tns-core-modules\\ui\\frame\\fragment.transitions.android.js' }, + { platform: 'android', scriptUrl: 'file:///data/data/org.nativescript.TabNavigation/files/app/tns_modules/tns-core-modules/debugger/devtools-elements.common.js', expectedResult: 'C:\\projectpath\\node_modules\\tns-core-modules\\debugger\\devtools-elements.common.js', existingPath: 'C:\\projectpath\\node_modules\\tns-core-modules\\debugger\\devtools-elements.common.js' }, + { platform: 'android', scriptUrl: 'file:///data/data/org.nativescript.TabNavigation/files/app/tns_modules/tns-core-modules/ui/page/page.js', expectedResult: 'C:\\projectpath\\node_modules\\tns-core-modules\\ui\\page\\page.android.js', existingPath: 'C:\\projectpath\\node_modules\\tns-core-modules\\ui\\page\\page.android.js' }, + { platform: 'android', scriptUrl: 'file:///data/data/org.nativescript.TabNavigation/files/app/tns_modules/tns-core-modules/ui/layouts/layout-base.js', expectedResult: 'C:\\projectpath\\node_modules\\tns-core-modules\\ui\\layouts\\layout-base.android.js', existingPath: 'C:\\projectpath\\node_modules\\tns-core-modules\\ui\\layouts\\layout-base.android.js' }, + { platform: 'android', scriptUrl: 'ng:///css/0/data/data/org.nativescript.TabNavigation/files/app/tabs/tabs.component.scss.ngstyle.js', expectedResult: 'ng:///css/0/data/data/org.nativescript.TabNavigation/files/app/tabs/tabs.component.scss.ngstyle.js' }, +]; + +export = tests; diff --git a/src/tests/scenario.ts b/src/tests/scenario.ts deleted file mode 100644 index badbf5d..0000000 --- a/src/tests/scenario.ts +++ /dev/null @@ -1,126 +0,0 @@ - -import {DebugProtocol} from 'vscode-debugprotocol'; -import {NsDebugClient} from './nsDebugClient'; - -export class Scenario { - private resolveStarted: () => any; - - private resolveBeforeLaunch: () => any; - private resolveBeforeAttach: () => any; - private resolveBeforeConfigurationDone: () => any; - - private resolveAfterLaunch: () => any; - private resolveAfterAttach: () => any; - private resolveAfterConfigurationDone: () => any; - - public client: NsDebugClient; - - public started: Promise = new Promise(r => this.resolveStarted = r); - public initializeRequest: Promise; - public launchRequest: Promise; - public attachRequest: Promise; - public configurationDoneRequest: Promise; - - // The corresponding command will not start before these promises are resolved - public beforeLaunchRequest: Promise = new Promise(r => this.resolveBeforeLaunch = r); - public beforeAttachRequest: Promise = new Promise(r => this.resolveBeforeAttach = r); - public beforeConfigurationDoneRequest: Promise = new Promise(r => this.resolveBeforeConfigurationDone = r); - - // The corresponding command promise will not be resolved/rejected before these promises are resolved - public afterLaunchRequest: Promise = new Promise(r => this.resolveAfterLaunch = r); - public afterAttachRequest: Promise = new Promise(r => this.resolveAfterAttach = r); - public afterConfigurationDoneRequest: Promise = new Promise(r => this.resolveAfterConfigurationDone = r); - - public initializedEvent: Promise; - public firstStoppedEvent: Promise; - - public initializeRequestArgs: DebugProtocol.InitializeRequestArguments; - public launchRequestArgs: DebugProtocol.LaunchRequestArguments; - public attachRequestArgs: DebugProtocol.AttachRequestArguments; - public configurationDoneArgs: DebugProtocol.ConfigurationDoneArguments; - - public attachInsteadOfLaunch: boolean; - - public static getDefaultInitArgs(): DebugProtocol.InitializeRequestArguments { - return { - adapterID: 'nativescript', - linesStartAt1: true, - columnsStartAt1: true, - pathFormat: 'path' - }; - } - - public static getDefaultLaunchArgs(platform: string, appRoot: string, emulator: boolean): DebugProtocol.LaunchRequestArguments { - let args = { - platform: platform, - request: "launch", - appRoot: appRoot, - sourceMaps: true, - emulator: emulator, - tnsArgs: process.env.DeviceId ? ['--device', process.env.DeviceId] : [] - }; - return args; - } - - public static getDefaultAttachArgs(platform: string, appRoot: string, emulator: boolean): DebugProtocol.LaunchRequestArguments { - let args = { - platform: platform, - request: "attach", - appRoot: appRoot, - sourceMaps: true, - emulator: emulator, - tnsArgs: process.env.DeviceId ? ['--device', process.env.DeviceId] : [] - }; - return args; - } - - constructor(client: NsDebugClient) { - this.client = client; - - this.initializedEvent = this.client.onNextTime('initialized'); - this.firstStoppedEvent = >this.client.onNextTime('stopped'); - - this.initializeRequest = this.started.then(() => { - return this.client.initializeRequest(this.initializeRequestArgs || Scenario.getDefaultInitArgs()); - }); - - this.launchRequest = this.initializeRequest.then(() => { - if (!this.attachInsteadOfLaunch) { - this.resolveBeforeLaunch(); - return this.beforeLaunchRequest.then(() => { - return this.client.launchRequest(this.launchRequestArgs).then(launchResponse => { - this.resolveAfterLaunch(); - return this.afterLaunchRequest.then(_ => launchResponse); - }); - }); - } - }); - - this.attachRequest = this.initializeRequest.then(() => { - if (this.attachInsteadOfLaunch) { - this.resolveBeforeAttach(); - return this.beforeAttachRequest.then(() => { - return this.client.attachRequest(this.attachRequestArgs).then(attachResponse => { - this.resolveAfterAttach(); - return this.afterAttachRequest.then(_ => attachResponse); - }); - }); - } - }); - - this.configurationDoneRequest = Promise.all<{}>([this.launchRequest, this.attachRequest, this.initializedEvent]).then(() => { - this.resolveBeforeConfigurationDone(); - return this.beforeConfigurationDoneRequest.then(() => { - return this.client.configurationDoneRequest(this.configurationDoneArgs).then(confDoneResponse => { - this.resolveAfterConfigurationDone(); - return this.afterConfigurationDoneRequest.then(_ => confDoneResponse); - }); - }); - }); - } - - public start(): Promise<{}> { - this.resolveStarted(); - return Promise.all<{}>([this.configurationDoneRequest, this.firstStoppedEvent]); - } -} \ No newline at end of file diff --git a/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/AndroidManifest.xml b/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/AndroidManifest.xml deleted file mode 100644 index 8d827dc..0000000 --- a/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/AndroidManifest.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/app.gradle b/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/app.gradle deleted file mode 100644 index 725fb59..0000000 --- a/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/app.gradle +++ /dev/null @@ -1,15 +0,0 @@ -// Add your native dependencies here: - -// Uncomment to add recyclerview-v7 dependency -//dependencies { -// compile 'com.android.support:recyclerview-v7:+' -//} - -android { - defaultConfig { - generatedDensities = [] - } - aaptOptions { - additionalParameters "--no-version-vectors" - } -} diff --git a/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/drawable-hdpi/icon.png b/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/drawable-hdpi/icon.png deleted file mode 100755 index 1034356..0000000 Binary files a/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/drawable-hdpi/icon.png and /dev/null differ diff --git a/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/drawable-ldpi/icon.png b/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/drawable-ldpi/icon.png deleted file mode 100755 index ddfc17a..0000000 Binary files a/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/drawable-ldpi/icon.png and /dev/null differ diff --git a/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/drawable-mdpi/icon.png b/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/drawable-mdpi/icon.png deleted file mode 100755 index 486e410..0000000 Binary files a/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/drawable-mdpi/icon.png and /dev/null differ diff --git a/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/drawable-nodpi/splashscreen.9.png b/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/drawable-nodpi/splashscreen.9.png deleted file mode 100644 index bd53be2..0000000 Binary files a/src/tests/testdata/DebuggerStatement/app/App_Resources/Android/drawable-nodpi/splashscreen.9.png and /dev/null differ diff --git a/src/tests/testdata/DebuggerStatement/app/App_Resources/iOS/Info.plist b/src/tests/testdata/DebuggerStatement/app/App_Resources/iOS/Info.plist deleted file mode 100644 index 0a8e1eb..0000000 --- a/src/tests/testdata/DebuggerStatement/app/App_Resources/iOS/Info.plist +++ /dev/null @@ -1,66 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleDisplayName - ${PRODUCT_NAME} - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIconFile - icon.png - CFBundleIcons - - CFBundlePrimaryIcon - - CFBundleIconFiles - - icon-40 - icon-60 - icon-72 - icon-76 - Icon-Small - Icon-Small-50 - - UIPrerenderedIcon - - - - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIRequiresFullScreen - - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/src/tests/testdata/DebuggerStatement/app/app.js b/src/tests/testdata/DebuggerStatement/app/app.js deleted file mode 100644 index efaf44a..0000000 --- a/src/tests/testdata/DebuggerStatement/app/app.js +++ /dev/null @@ -1,2 +0,0 @@ -var application = require("application"); -application.start({ moduleName: "main-page" }); diff --git a/src/tests/testdata/DebuggerStatement/app/debuggerStatement.js b/src/tests/testdata/DebuggerStatement/app/debuggerStatement.js deleted file mode 100644 index 419cedc..0000000 --- a/src/tests/testdata/DebuggerStatement/app/debuggerStatement.js +++ /dev/null @@ -1,5 +0,0 @@ -function log(message) { - console.log(message); -} -debugger; -log('Debugger statment test'); \ No newline at end of file diff --git a/src/tests/testdata/DebuggerStatement/app/main-page.js b/src/tests/testdata/DebuggerStatement/app/main-page.js deleted file mode 100644 index 82c042f..0000000 --- a/src/tests/testdata/DebuggerStatement/app/main-page.js +++ /dev/null @@ -1,6 +0,0 @@ -var ds = require('./debuggerStatement'); - -function onNavigatingTo(args) { - var page = args.object; -} -exports.onNavigatingTo = onNavigatingTo; \ No newline at end of file diff --git a/src/tests/testdata/DebuggerStatement/app/main-page.xml b/src/tests/testdata/DebuggerStatement/app/main-page.xml deleted file mode 100644 index d7c0b3f..0000000 --- a/src/tests/testdata/DebuggerStatement/app/main-page.xml +++ /dev/null @@ -1,7 +0,0 @@ - - -