diff --git a/package.json b/package.json index 18a1ba5..a9c76ed 100644 --- a/package.json +++ b/package.json @@ -111,81 +111,58 @@ { "name": "Sync on iOS", "type": "nativescript", - "platform": "ios", "request": "launch", + "platform": "ios", "appRoot": "${workspaceRoot}", - "sourceMaps": true, - "diagnosticLogging": false, - "emulator": false, - "rebuild": false, - "syncAllFiles": false + "sourceMaps": true }, { "name": "Launch on iOS", "type": "nativescript", - "platform": "ios", "request": "launch", + "platform": "ios", "appRoot": "${workspaceRoot}", "sourceMaps": true, - "diagnosticLogging": false, - "emulator": false, "rebuild": true }, { "name": "Attach on iOS", "type": "nativescript", - "platform": "ios", "request": "attach", + "platform": "ios", "appRoot": "${workspaceRoot}", - "sourceMaps": true, - "diagnosticLogging": false, - "emulator": false + "sourceMaps": true }, { "name": "Sync on Android", "type": "nativescript", - "platform": "android", "request": "launch", + "platform": "android", "appRoot": "${workspaceRoot}", - "sourceMaps": true, - "diagnosticLogging": false, - "emulator": false, - "rebuild": false + "sourceMaps": true }, { "name": "Launch on Android", "type": "nativescript", - "platform": "android", "request": "launch", + "platform": "android", "appRoot": "${workspaceRoot}", "sourceMaps": true, - "diagnosticLogging": false, - "emulator": false, "rebuild": true }, { "name": "Attach on Android", "type": "nativescript", - "platform": "android", "request": "attach", + "platform": "android", "appRoot": "${workspaceRoot}", - "sourceMaps": true, - "diagnosticLogging": false, - "emulator": false + "sourceMaps": true } ], "configurationAttributes": { "launch": { "required": [], "properties": { - "runtimeArgs": { - "type": "array", - "description": "Optional arguments passed to the runtime executable.", - "items": { - "type": "string" - }, - "default": [] - }, "tnsArgs": { "type": "array", "description": "Optional arguments passed to the NativeScript CLI executable.", @@ -219,9 +196,14 @@ "description": "NativeScript platform", "default": null }, - "emulator": { + "stopOnEntry": { "type": "boolean", - "description": "Whether the app to run in emulator or on a physical device.", + "description": "Automatically stop on the first line after lauch", + "default": false + }, + "noDebug": { + "type": "boolean", + "description": "If true the launch request will launch the program without enabling debugging", "default": false }, "rebuild": { @@ -244,14 +226,6 @@ "attach": { "required": [], "properties": { - "runtimeArgs": { - "type": "array", - "description": "Optional arguments passed to the runtime executable.", - "items": { - "type": "string" - }, - "default": [] - }, "tnsArgs": { "type": "array", "description": "Optional arguments passed to the NativeScript CLI executable.", @@ -285,11 +259,6 @@ "description": "NativeScript platform", "default": null }, - "emulator": { - "type": "boolean", - "description": "Whether the app to run in emulator or on a physical device.", - "default": false - }, "nativescriptCliPath": { "type": "string", "description": "Path to the nativescript CLI to be used by the NativeScript extension.", diff --git a/src/services/analytics/AnalyticsBaseInfo.ts b/src/analytics/AnalyticsBaseInfo.ts similarity index 100% rename from src/services/analytics/AnalyticsBaseInfo.ts rename to src/analytics/AnalyticsBaseInfo.ts diff --git a/src/services/analytics/AnalyticsService.ts b/src/analytics/AnalyticsService.ts similarity index 81% rename from src/services/analytics/AnalyticsService.ts rename to src/analytics/AnalyticsService.ts index ecbfadf..577b6ed 100644 --- a/src/services/analytics/AnalyticsService.ts +++ b/src/analytics/AnalyticsService.ts @@ -1,27 +1,18 @@ import * as os from 'os'; import * as vscode from 'vscode'; -import { Version } from '../../common/Version'; +import { Version } from '../common/version'; import { GUAService } from './GUAService'; import { TelerikAnalyticsService } from './TelerikAnalyticsService'; import { AnalyticsBaseInfo, OperatingSystem } from './AnalyticsBaseInfo'; -import { ExtensionVersionInfo } from '../ExtensionVersionInfo'; -import * as ns from '../NsCliService'; +import { ExtensionHostServices as Services } from '../services/extensionHostServices'; +import * as utils from '../common/utilities'; export class AnalyticsService { - private static _instance: AnalyticsService; - private _baseInfo: AnalyticsBaseInfo; private _gua: GUAService; private _ta: TelerikAnalyticsService; private _analyticsEnabled: boolean; - public static getInstance(): AnalyticsService { - if (!this._instance) { - this._instance = new AnalyticsService(); - } - return this._instance; - } - public static generateMachineId(): string { let machineId = ''; try { @@ -48,8 +39,8 @@ export class AnalyticsService { }; this._baseInfo = { - cliVersion: Version.stringify(ns.CliVersionInfo.getInstalledCliVersion()), - extensionVersion: Version.stringify(ExtensionVersionInfo.getExtensionVersion()), + cliVersion: Services.cli.version.toString(), + extensionVersion: utils.getInstalledExtensionVersion().toString(), operatingSystem: operatingSystem, userId: AnalyticsService.generateMachineId() }; @@ -79,7 +70,7 @@ export class AnalyticsService { this._gua.runRunCommand(platform), this._ta.runRunCommand(platform) ]); - } catch(e) {} + } catch(e) { } } return Promise.resolve(); } diff --git a/src/services/analytics/EqatecMonitor.min.js b/src/analytics/EqatecMonitor.min.js similarity index 100% rename from src/services/analytics/EqatecMonitor.min.js rename to src/analytics/EqatecMonitor.min.js diff --git a/src/services/analytics/GUAService.ts b/src/analytics/GUAService.ts similarity index 100% rename from src/services/analytics/GUAService.ts rename to src/analytics/GUAService.ts diff --git a/src/services/analytics/TelerikAnalyticsService.ts b/src/analytics/TelerikAnalyticsService.ts similarity index 100% rename from src/services/analytics/TelerikAnalyticsService.ts rename to src/analytics/TelerikAnalyticsService.ts diff --git a/src/common/Logger.ts b/src/common/Logger.ts new file mode 100644 index 0000000..8a2c4f0 --- /dev/null +++ b/src/common/Logger.ts @@ -0,0 +1,101 @@ +import * as fs from 'fs'; + +export enum LoggerMessageType { + Log, + Info, + Warning, + Error +} + +export interface LoggerMessageEventArgs { + message: string, + type: LoggerMessageType +} + +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) { + switch(args.type) { + case LoggerMessageType.Log: + console.log(args.message); + break; + case LoggerMessageType.Info: + console.info(args.message); + break; + case LoggerMessageType.Warning: + console.warn(args.message); + break; + case LoggerMessageType.Error: + console.error(args.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 diff --git a/src/common/Version.ts b/src/common/Version.ts deleted file mode 100644 index 424e6d4..0000000 --- a/src/common/Version.ts +++ /dev/null @@ -1,20 +0,0 @@ -export class Version { - public static parse(versionStr: string): number[] { - 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 version; - } - - public static stringify(version: number[]): string { - return `${version[0]}.${version[1]}.${version[2]}`; - } - - public static compareBySubminor(v1, v2): number { - return (v1[0] - v2[0] != 0) ? (v1[0] - v2[0]) : (v1[1] - v2[1] != 0) ? v1[1] - v2[1] : v1[2] - v2[2]; - } -} \ No newline at end of file diff --git a/src/common/extensionVersionService.ts b/src/common/extensionVersionService.ts new file mode 100644 index 0000000..ee87139 --- /dev/null +++ b/src/common/extensionVersionService.ts @@ -0,0 +1,84 @@ +import * as https from 'https'; +import * as vscode from 'vscode'; +import {Version} from './version'; +import * as utils from './utilities'; + +export type LatestPublishedVersionCheckResult = {latestPublishedVersion: string, timestamp: number}; + +export class ExtensionVersionService { + private static _extensionId: string = '8d837914-d8fa-45b5-965d-f76ebd6dbf5c'; + private static _getLatestPublishedVersionPromise: Promise = null; + private _memento: vscode.Memento; + + private static getExtensionMetadataFromVSCodeMarketplace(): Promise { + return new Promise((resolve, reject) =>{ + let postData: string = `{ filters: [{ criteria: [{ filterType: 4, value: "${ExtensionVersionService._extensionId}" }] }], flags: 262 }`; + + let request = https.request({ + hostname: 'marketplace.visualstudio.com', + path: '/_apis/public/gallery/extensionquery', + method: 'POST', + headers: { + 'Accept': 'application/json;api-version=2.2-preview.1', + 'Content-Type': 'application/json', + 'Transfer-Encoding': 'chunked', + 'Content-Length': Buffer.byteLength(postData) + } + }, response => { + if (response.statusCode != 200) { + reject(`Unable to download data from Visual Studio Marketplace. Status code: ${response.statusCode}`); + return; + } + let body = ''; + response.on('data', chunk => { + body += chunk; + }); + response.on('end', () => { + let bodyObj = JSON.parse(body); + if (bodyObj.results[0].extensions[0].extensionId == ExtensionVersionService._extensionId) { + let latestPublishedVersion = bodyObj.results[0].extensions[0].versions[0].version; + resolve({ latestPublishedVersion: latestPublishedVersion, timestamp: Date.now() }); + } + }); + }); + + request.on('error', (e) => { + reject(e); + }); + + request.end(postData); + }); + } + + constructor(context: vscode.Memento) { + this._memento = context; + } + + public get latestPublishedVersion(): Promise { + if (ExtensionVersionService._getLatestPublishedVersionPromise) { + return ExtensionVersionService._getLatestPublishedVersionPromise.then(result => Version.parse(result.latestPublishedVersion) ); + } + + // Check the cache for extension version information + let cachedResult: LatestPublishedVersionCheckResult = this._memento.get('LatestPublishedExtensionVersion'); + if (cachedResult && cachedResult.timestamp > Date.now() - 24 * 60 * 60 * 1000) { // Version is cached for a day + ExtensionVersionService._getLatestPublishedVersionPromise = Promise.resolve(cachedResult); + } + else { + ExtensionVersionService._getLatestPublishedVersionPromise = ExtensionVersionService.getExtensionMetadataFromVSCodeMarketplace().then((result: LatestPublishedVersionCheckResult) => { + this._memento.update('LatestPublishedExtensionVersion', result); // save in cache + return result; + }); + } + return ExtensionVersionService._getLatestPublishedVersionPromise.then(result => Version.parse(result.latestPublishedVersion)); + } + + public get isLatestInstalled(): Promise<{ result: boolean, error: string }> { + return this.latestPublishedVersion.then(latestVersion => { + let extensionVersion = utils.getInstalledExtensionVersion(); + let isLatest: boolean = extensionVersion.compareBySubminorTo(latestVersion) >= 0; + let error = isLatest ? null : `A new version of the NativeScript extension is available. Open "Extensions" panel to update to v${latestVersion}.`; + return {result: isLatest, error: error}; + }); + } +} \ No newline at end of file diff --git a/src/debug-adapter/utilities.ts b/src/common/utilities.ts similarity index 76% rename from src/debug-adapter/utilities.ts rename to src/common/utilities.ts index ff145d3..22fb959 100644 --- a/src/debug-adapter/utilities.ts +++ b/src/common/utilities.ts @@ -9,31 +9,7 @@ import * as os from 'os'; import * as fs from 'fs'; import * as url from 'url'; import * as path from 'path'; -import {DebugProtocol} from 'vscode-debugprotocol'; - -const DEFAULT_CHROME_PATH = { - OSX: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', - WIN: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', - WINx86: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', - LINUX: '/usr/bin/google-chrome' -}; - -export function getBrowserPath(): string { - const platform = getPlatform(); - if (platform === Platform.OSX) { - return existsSync(DEFAULT_CHROME_PATH.OSX) ? DEFAULT_CHROME_PATH.OSX : null; - } else if (platform === Platform.Windows) { - if (existsSync(DEFAULT_CHROME_PATH.WINx86)) { - return DEFAULT_CHROME_PATH.WINx86; - } else if (existsSync(DEFAULT_CHROME_PATH.WIN)) { - return DEFAULT_CHROME_PATH.WIN; - } else { - return null; - } - } else { - return existsSync(DEFAULT_CHROME_PATH.LINUX) ? DEFAULT_CHROME_PATH.LINUX : null; - } -} +import {Version} from './Version'; export const enum Platform { Windows, OSX, Linux @@ -59,37 +35,6 @@ export function existsSync(path: string): boolean { } } -export class DebounceHelper { - private waitToken: NodeJS.Timer; - - constructor(private timeoutMs: number) { } - - /** - * If not waiting already, call fn after the timeout - */ - public wait(fn: () => any): void { - if (!this.waitToken) { - this.waitToken = setTimeout(() => { - this.waitToken = null; - fn(); - }, - this.timeoutMs); - } - } - - /** - * If waiting for something, cancel it and call fn immediately - */ - public doAndCancel(fn: () => any): void { - if (this.waitToken) { - clearTimeout(this.waitToken); - this.waitToken = null; - } - - fn(); - } -} - /** * Returns a reversed version of arr. Doesn't modify the input. */ @@ -137,66 +82,6 @@ export function retryAsync(fn: () => Promise, timeoutMs: number): Promise void; - private _diagnosticLoggingEnabled: boolean; - - public static log(msg: string, forceDiagnosticLogging = false): void { - if (this._logger) this._logger._log(msg, forceDiagnosticLogging); - } - - public static init(isServer: boolean, logCallback: (msg: string) => void): void { - if (!this._logger) { - this._logger = new Logger(isServer); - this._logger._diagnosticLogCallback = logCallback; - - if (isServer) { - Logger.logVersionInfo(); - } - } - } - - public static enableDiagnosticLogging(): void { - if (this._logger) { - this._logger._diagnosticLoggingEnabled = true; - if (!this._logger._isServer) { - Logger.logVersionInfo(); - } - } - } - - public static logVersionInfo(): void { - Logger.log(`OS: ${os.platform() } ${os.arch() }`); - Logger.log('Node version: ' + process.version); - Logger.log('Adapter version: ' + require('../../package.json').version); - } - - constructor(isServer: boolean) { - this._isServer = isServer; - } - - private _log(msg: string, forceDiagnosticLogging: boolean): void { - if (this._isServer || this._diagnosticLoggingEnabled || forceDiagnosticLogging) { - this._sendLog(msg); - } - } - - private _sendLog(msg: string): void { - if (this._isServer) { - console.log(msg); - } else if (this._diagnosticLogCallback) { - this._diagnosticLogCallback(msg); - } - } -} - function tryFindSourcePathInNSProject(nsProjectPath: string, additionalFileExtension: string, resorcePath: string) : string { let guesses = []; const pathParts = resorcePath.split(path.sep); @@ -455,13 +340,6 @@ export function errP(msg: any): Promise { return Promise.reject(e); } -/** - * Calculates the appRoot from a launch/attach request. The appRoot is the root directory of the NativeScript app. - */ -export function getAppRoot(args: DebugProtocol.ILaunchRequestArgs | DebugProtocol.IAttachRequestArgs): string { - return (args.appRoot && path.isAbsolute(args.appRoot)) ? args.appRoot : ''; -} - /** * Helper function to GET the contents of a url */ @@ -499,3 +377,11 @@ export function lstrip(s: string, lStr: string): string { 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); +} \ No newline at end of file diff --git a/src/common/version.ts b/src/common/version.ts new file mode 100644 index 0000000..59ba47d --- /dev/null +++ b/src/common/version.ts @@ -0,0 +1,28 @@ +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/custom-typings/debugProtocolExtensions.d.ts b/src/custom-typings/debugProtocolExtensions.d.ts index a289a69..ee976f8 100644 --- a/src/custom-typings/debugProtocolExtensions.d.ts +++ b/src/custom-typings/debugProtocolExtensions.d.ts @@ -2,33 +2,29 @@ import {DebugProtocol} from 'vscode-debugprotocol'; declare module 'vscode-debugprotocol' { namespace DebugProtocol { - interface ILaunchRequestArgs extends DebugProtocol.LaunchRequestArguments { - platform: string; - appRoot?: string; - runtimeArgs?: string[]; - runtimeExecutable?: string; - stopOnEntry?: boolean; + + type RequestArguments = LaunchRequestArguments | AttachRequestArguments; + type RequestType = "launch" | "attach"; + type PlatformType = "android" | "ios"; + + interface IRequestArgs { + request: RequestType; + platform: PlatformType; + appRoot: string; sourceMaps?: boolean; diagnosticLogging?: boolean; - emulator?:boolean; - request: string; tnsArgs?: string[]; tnsOutput?: string; + nativescriptCliPath?: string; + } + + interface ILaunchRequestArgs extends DebugProtocol.LaunchRequestArguments, IRequestArgs { + stopOnEntry?: boolean; rebuild?: boolean; syncAllFiles?: boolean; - nativescriptCliPath?: string; } - interface IAttachRequestArgs extends DebugProtocol.AttachRequestArguments { - platform: string; - appRoot?: string; - sourceMaps?: boolean; - diagnosticLogging?: boolean; - emulator?:boolean; - request: string; - tnsArgs?: string[]; - tnsOutput?: string; - nativescriptCliPath?: string; + interface IAttachRequestArgs extends DebugProtocol.AttachRequestArguments, IRequestArgs { } interface ISetBreakpointsArgs extends DebugProtocol.SetBreakpointsArguments { diff --git a/src/debug-adapter/adapter/adapterProxy.ts b/src/debug-adapter/adapter/adapterProxy.ts index af75bf6..4ac4e3f 100644 --- a/src/debug-adapter/adapter/adapterProxy.ts +++ b/src/debug-adapter/adapter/adapterProxy.ts @@ -2,7 +2,8 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -import * as utils from '../utilities'; +import * as utils from '../../common/utilities'; +import {DebugAdapterServices as Services} from '../../services/debugAdapterServices'; import {DebugProtocol} from 'vscode-debugprotocol'; export type EventHandler = (event: DebugProtocol.Event) => void; @@ -77,7 +78,7 @@ export class AdapterProxy { this._eventHandler(event); } } catch (e) { - utils.Logger.log('Error handling adapter event: ' + (e ? e.stack : '')); + Services.logger.error('Error handling adapter event: ' + (e ? e.stack : '')); } } } diff --git a/src/debug-adapter/adapter/pathTransformer.ts b/src/debug-adapter/adapter/pathTransformer.ts index ef5aafb..9e21f99 100644 --- a/src/debug-adapter/adapter/pathTransformer.ts +++ b/src/debug-adapter/adapter/pathTransformer.ts @@ -2,9 +2,9 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -import * as utils from '../utilities'; +import * as utils from '../../common/utilities'; +import {DebugAdapterServices as Services} from '../../services/debugAdapterServices'; import {DebugProtocol} from 'vscode-debugprotocol'; -import * as path from 'path'; interface IPendingBreakpoint { resolve: () => void; @@ -16,7 +16,7 @@ interface IPendingBreakpoint { * Converts a local path from Code to a path on the target. */ export class PathTransformer implements DebugProtocol.IDebugTransformer { - private _webRoot: string; + private _appRoot: string; private _platform: string; private _clientPathToWebkitUrl = new Map(); private _webkitUrlToClientPath = new Map(); @@ -24,13 +24,13 @@ export class PathTransformer implements DebugProtocol.IDebugTransformer { private inferedDeviceRoot :string = null; public launch(args: DebugProtocol.ILaunchRequestArgs): void { - this._webRoot = utils.getAppRoot(args); + this._appRoot = args.appRoot; this._platform = args.platform; this.inferedDeviceRoot = (this._platform === 'ios') ? 'file://' : this.inferedDeviceRoot; } public attach(args: DebugProtocol.IAttachRequestArgs): void { - this._webRoot = utils.getAppRoot(args); + this._appRoot = args.appRoot; this._platform = args.platform; } @@ -43,7 +43,7 @@ export class PathTransformer implements DebugProtocol.IDebugTransformer { if (utils.isURL(args.source.path)) { // already a url, use as-is - utils.Logger.log(`Paths.setBP: ${args.source.path} is already a URL`); + Services.logger.log(`Paths.setBP: ${args.source.path} is already a URL`); resolve(); return; } @@ -51,11 +51,11 @@ export class PathTransformer implements DebugProtocol.IDebugTransformer { const url = utils.canonicalizeUrl(args.source.path); if (this._clientPathToWebkitUrl.has(url)) { args.source.path = this._clientPathToWebkitUrl.get(url); - utils.Logger.log(`Paths.setBP: Resolved ${url} to ${args.source.path}`); + Services.logger.log(`Paths.setBP: Resolved ${url} to ${args.source.path}`); resolve(); } else if (this.inferedDeviceRoot) { - let inferedUrl = url.replace(this._webRoot, this.inferedDeviceRoot).replace(/\\/g, "/"); + let inferedUrl = url.replace(this._appRoot, this.inferedDeviceRoot).replace(/\\/g, "/"); //change device path if {N} core module or {N} module if (inferedUrl.indexOf("/node_modules/tns-core-modules/") != -1) @@ -71,11 +71,11 @@ export class PathTransformer implements DebugProtocol.IDebugTransformer { inferedUrl = inferedUrl.replace(`.${this._platform}.`, '.'); args.source.path = inferedUrl; - utils.Logger.log(`Paths.setBP: Resolved (by infering) ${url} to ${args.source.path}`); + Services.logger.log(`Paths.setBP: Resolved (by infering) ${url} to ${args.source.path}`); resolve(); } else { - utils.Logger.log(`Paths.setBP: No target url cached for client path: ${url}, waiting for target script to be loaded.`); + 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 }); } @@ -95,8 +95,8 @@ export class PathTransformer implements DebugProtocol.IDebugTransformer { const webkitUrl: string = event.body.scriptUrl; if (!this.inferedDeviceRoot && this._platform === "android") { - this.inferedDeviceRoot = utils.inferDeviceRoot(this._webRoot, this._platform, webkitUrl); - utils.Logger.log("\n\n\n ***Inferred device root: " + this.inferedDeviceRoot + "\n\n\n"); + this.inferedDeviceRoot = utils.inferDeviceRoot(this._appRoot, this._platform, webkitUrl); + Services.logger.log("\n\n\n ***Inferred device root: " + this.inferedDeviceRoot + "\n\n\n"); if (this.inferedDeviceRoot.indexOf("/data/user/0/") != -1) { @@ -104,12 +104,12 @@ export class PathTransformer implements DebugProtocol.IDebugTransformer { } } - const clientPath = utils.webkitUrlToClientPath(this._webRoot, this._platform, webkitUrl); + const clientPath = utils.webkitUrlToClientPath(this._appRoot, this._platform, webkitUrl); if (!clientPath) { - utils.Logger.log(`Paths.scriptParsed: could not resolve ${webkitUrl} to a file in the workspace. webRoot: ${this._webRoot}`); + Services.logger.log(`Paths.scriptParsed: could not resolve ${webkitUrl} to a file in the workspace. webRoot: ${this._appRoot}`); } else { - utils.Logger.log(`Paths.scriptParsed: resolved ${webkitUrl} to ${clientPath}. webRoot: ${this._webRoot}`); + Services.logger.log(`Paths.scriptParsed: resolved ${webkitUrl} to ${clientPath}. webRoot: ${this._appRoot}`); this._clientPathToWebkitUrl.set(clientPath, webkitUrl); this._webkitUrlToClientPath.set(webkitUrl, clientPath); @@ -117,7 +117,7 @@ export class PathTransformer implements DebugProtocol.IDebugTransformer { } if (this._pendingBreakpointsByPath.has(event.body.scriptUrl)) { - utils.Logger.log(`Paths.scriptParsed: Resolving pending breakpoints for ${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); @@ -131,7 +131,7 @@ export class PathTransformer implements DebugProtocol.IDebugTransformer { if (frame.source && frame.source.path) { const clientPath = this._webkitUrlToClientPath.has(frame.source.path) ? this._webkitUrlToClientPath.get(frame.source.path) : - utils.webkitUrlToClientPath(this._webRoot, this._platform, 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) { @@ -141,4 +141,4 @@ export class PathTransformer implements DebugProtocol.IDebugTransformer { } }); } -} +} \ No newline at end of file diff --git a/src/debug-adapter/adapter/sourceMaps/pathUtilities.ts b/src/debug-adapter/adapter/sourceMaps/pathUtilities.ts index 411acd6..f04177c 100644 --- a/src/debug-adapter/adapter/sourceMaps/pathUtilities.ts +++ b/src/debug-adapter/adapter/sourceMaps/pathUtilities.ts @@ -6,8 +6,8 @@ import * as Path from 'path'; import * as URL from 'url'; - -import * as utils from '../../utilities'; +import {DebugAdapterServices as Services} from '../../../services/debugAdapterServices'; +import * as utils from '../../../common/utilities'; export function getPathRoot(p: string) { if (p) { @@ -83,16 +83,16 @@ export function getAbsSourceRoot(sourceRoot: string, webRoot: string, generatedP } } - utils.Logger.log(`SourceMap: resolved sourceRoot ${sourceRoot} -> ${absSourceRoot}`); + Services.logger.log(`SourceMap: resolved sourceRoot ${sourceRoot} -> ${absSourceRoot}`); } else { if (Path.isAbsolute(generatedPath)) { absSourceRoot = Path.dirname(generatedPath); - utils.Logger.log(`SourceMap: no sourceRoot specified, using script dirname: ${absSourceRoot}`); + 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); - utils.Logger.log(`SourceMap: no sourceRoot specified, using webRoot + script path dirname: ${absSourceRoot}`); + Services.logger.log(`SourceMap: no sourceRoot specified, using webRoot + script path dirname: ${absSourceRoot}`); } } diff --git a/src/debug-adapter/adapter/sourceMaps/sourceMapTransformer.ts b/src/debug-adapter/adapter/sourceMaps/sourceMapTransformer.ts index 787d31b..7982736 100644 --- a/src/debug-adapter/adapter/sourceMaps/sourceMapTransformer.ts +++ b/src/debug-adapter/adapter/sourceMaps/sourceMapTransformer.ts @@ -4,9 +4,10 @@ import * as path from 'path'; import * as fs from 'fs'; +import {DebugAdapterServices as Services} from '../../../services/debugAdapterServices'; import {DebugProtocol} from 'vscode-debugprotocol'; import {ISourceMaps, SourceMaps} from './sourceMaps'; -import * as utils from '../../utilities'; +import * as utils from '../../../common/utilities'; interface IPendingBreakpoint { resolve: () => void; @@ -35,9 +36,9 @@ export class SourceMapTransformer implements DebugProtocol.IDebugTransformer { this.init(args); } - private init(args: DebugProtocol.ILaunchRequestArgs | DebugProtocol.IAttachRequestArgs): void { + private init(args: DebugProtocol.IRequestArgs): void { if (args.sourceMaps) { - this._webRoot = utils.getAppRoot(args); + this._webRoot = args.appRoot; this._sourceMaps = new SourceMaps(this._webRoot); this._requestSeqToSetBreakpointsArgs = new Map(); this._allRuntimeScriptPaths = new Set(); @@ -59,7 +60,7 @@ export class SourceMapTransformer implements DebugProtocol.IDebugTransformer { const argsPath = args.source.path; const mappedPath = this._sourceMaps.MapPathFromSource(argsPath); if (mappedPath) { - utils.Logger.log(`SourceMaps.setBP: Mapped ${argsPath} to ${mappedPath}`); + Services.logger.log(`SourceMaps.setBP: Mapped ${argsPath} to ${mappedPath}`); args.authoredPath = argsPath; args.source.path = mappedPath; @@ -68,11 +69,11 @@ export class SourceMapTransformer implements DebugProtocol.IDebugTransformer { const mappedLines = args.lines.map((line, i) => { const mapped = this._sourceMaps.MapFromSource(argsPath, line, /*column=*/0); if (mapped) { - utils.Logger.log(`SourceMaps.setBP: Mapped ${argsPath}:${line}:0 to ${mappedPath}:${mapped.line}:${mapped.column}`); + Services.logger.log(`SourceMaps.setBP: Mapped ${argsPath}:${line}:0 to ${mappedPath}:${mapped.line}:${mapped.column}`); mappedCols[i] = mapped.column; return mapped.line; } else { - utils.Logger.log(`SourceMaps.setBP: Mapped ${argsPath} but not line ${line}, column 0`); + Services.logger.log(`SourceMaps.setBP: Mapped ${argsPath} but not line ${line}, column 0`); mappedCols[i] = 0; return line; } @@ -100,10 +101,10 @@ export class SourceMapTransformer implements DebugProtocol.IDebugTransformer { }); } else if (this._allRuntimeScriptPaths.has(argsPath)) { // It's a generated file which is loaded - utils.Logger.log(`SourceMaps.setBP: SourceMaps are enabled but ${argsPath} is a runtime script`); + 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 - utils.Logger.log(`SourceMaps.setBP: ${argsPath} can't be resolved to a loaded script.`); + Services.logger.log(`SourceMaps.setBP: ${argsPath} can't be resolved to a loaded script.`); this._pendingBreakpointsByPath.set(argsPath, { resolve, reject, args, requestSeq }); return; } @@ -131,10 +132,10 @@ export class SourceMapTransformer implements DebugProtocol.IDebugTransformer { response.breakpoints.forEach((bp, i) => { const mapped = this._sourceMaps.MapToSource(args.source.path, args.lines[i], args.cols[i]); if (mapped) { - utils.Logger.log(`SourceMaps.setBP: Mapped ${args.source.path}:${bp.line}:${bp.column} to ${mapped.path}:${mapped.line}`); + Services.logger.log(`SourceMaps.setBP: Mapped ${args.source.path}:${bp.line}:${bp.column} to ${mapped.path}:${mapped.line}`); bp.line = mapped.line; } else { - utils.Logger.log(`SourceMaps.setBP: Can't map ${args.source.path}:${bp.line}:${bp.column}, keeping the line number as-is.`); + 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); @@ -200,7 +201,7 @@ export class SourceMapTransformer implements DebugProtocol.IDebugTransformer { this._sourceMaps.ProcessNewSourceMap(event.body.scriptUrl, sourceMapUrlValue).then(() => { const sources = this._sourceMaps.AllMappedSources(event.body.scriptUrl); if (sources) { - utils.Logger.log(`SourceMaps.scriptParsed: ${event.body.scriptUrl} was just loaded and has mapped sources: ${JSON.stringify(sources)}`); + Services.logger.log(`SourceMaps.scriptParsed: ${event.body.scriptUrl} was just loaded and has mapped sources: ${JSON.stringify(sources)}`); sources.forEach(this.resolvePendingBreakpoints, this); } }); @@ -240,7 +241,7 @@ export class SourceMapTransformer implements DebugProtocol.IDebugTransformer { private resolvePendingBreakpoints(sourcePath: string): void { // If there's a setBreakpoints request waiting on this script, go through setBreakpoints again if (this._pendingBreakpointsByPath.has(sourcePath)) { - utils.Logger.log(`SourceMaps.scriptParsed: Resolving pending breakpoints for ${sourcePath}`); + Services.logger.log(`SourceMaps.scriptParsed: Resolving pending breakpoints for ${sourcePath}`); const pendingBreakpoint = this._pendingBreakpointsByPath.get(sourcePath); this._pendingBreakpointsByPath.delete(sourcePath); diff --git a/src/debug-adapter/adapter/sourceMaps/sourceMaps.ts b/src/debug-adapter/adapter/sourceMaps/sourceMaps.ts index 57c5c82..d608676 100644 --- a/src/debug-adapter/adapter/sourceMaps/sourceMaps.ts +++ b/src/debug-adapter/adapter/sourceMaps/sourceMaps.ts @@ -9,8 +9,8 @@ import * as URL from 'url'; import * as FS from 'fs'; import {SourceMapConsumer} from 'source-map'; import * as PathUtils from './pathUtilities'; -import * as utils from '../../utilities'; -import {Logger} from '../../utilities'; +import * as utils from '../../../common/utilities'; +import {DebugAdapterServices as Services} from '../../../services/debugAdapterServices'; export interface MappingResult { @@ -191,7 +191,7 @@ export class SourceMaps implements ISourceMaps { const matches = SourceMaps.SOURCE_MAPPING_MATCHER.exec(line); if (matches && matches.length === 2) { const uri = matches[1].trim(); - Logger.log(`_findSourceMapUrlInFile: source map url at end of generated file '${generatedFilePath}''`); + Services.logger.log(`_findSourceMapUrlInFile: source map url at end of generated file '${generatedFilePath}''`); return uri; } } @@ -329,7 +329,7 @@ export class SourceMaps implements ISourceMaps { } if (!FS.existsSync(generatedFilePath)) { - Logger.log("findGeneratedToSourceMappingSync: can't find the sourceMapping for file: " + generatedFilePath); + Services.logger.log("findGeneratedToSourceMappingSync: can't find the sourceMapping for file: " + generatedFilePath); return null; } @@ -379,7 +379,7 @@ export class SourceMaps implements ISourceMaps { } } catch (e) { - Logger.log(`can't parse inlince sourcemap. exception while processing data url (${e.stack})`); + Services.logger.log(`can't parse inlince sourcemap. exception while processing data url (${e.stack})`); } } @@ -390,14 +390,14 @@ export class SourceMaps implements ISourceMaps { let contentsP: Promise; if (utils.isURL(mapPath)) { contentsP = utils.getURL(mapPath).catch(e => { - Logger.log(`SourceMaps.createSourceMap: Could not download map from ${mapPath}`); + Services.logger.log(`SourceMaps.createSourceMap: Could not download map from ${mapPath}`); return null; }); } else { contentsP = new Promise((resolve, reject) => { FS.readFile(mapPath, (err, data) => { if (err) { - Logger.log(`SourceMaps.createSourceMap: Could not read map from ${mapPath}`); + Services.logger.log(`SourceMaps.createSourceMap: Could not read map from ${mapPath}`); resolve(null); } else { resolve(data); @@ -412,7 +412,7 @@ export class SourceMaps implements ISourceMaps { // Throws for invalid contents JSON return new SourceMap(pathToGenerated, contents, this._webRoot); } catch (e) { - Logger.log(`SourceMaps.createSourceMap: exception while processing sourcemap: ${e.stack}`); + Services.logger.log(`SourceMaps.createSourceMap: exception while processing sourcemap: ${e.stack}`); return null; } } else { @@ -427,7 +427,7 @@ export class SourceMaps implements ISourceMaps { // Throws for invalid contents JSON return new SourceMap(pathToGenerated, contents, this._webRoot); } catch (e) { - Logger.log(`SourceMaps.createSourceMap: exception while processing sourcemap: ${e.stack}`); + Services.logger.log(`SourceMaps.createSourceMap: exception while processing sourcemap: ${e.stack}`); return null; } } @@ -452,7 +452,7 @@ class SourceMap { * webRoot - an absolute path */ public constructor(generatedPath: string, json: string, webRoot: string) { - Logger.log(`SourceMap: creating SM for ${generatedPath}`) + Services.logger.log(`SourceMap: creating SM for ${generatedPath}`) this._generatedPath = generatedPath; this._webRoot = webRoot; diff --git a/src/debug-adapter/connection/androidConnection.ts b/src/debug-adapter/connection/androidConnection.ts index 1d4c9e6..5329aeb 100644 --- a/src/debug-adapter/connection/androidConnection.ts +++ b/src/debug-adapter/connection/androidConnection.ts @@ -1,8 +1,7 @@ import * as http from 'http'; import {EventEmitter} from 'events'; -import {Logger} from '../utilities'; +import {DebugAdapterServices as Services} from '../../services/debugAdapterServices'; import * as Net from 'net'; -import * as ns from '../../services/NsCliService'; import { INSDebugConnection } from './INSDebugConnection'; @@ -137,7 +136,7 @@ class ResReqNetSocket extends EventEmitter { this.debugBuffer = b.toString('utf8', this.msg.contentLength, b.length); if (this.msg.body.length > 0) { obj = JSON.parse(this.msg.body); - Logger.log('From target(' + (obj.type ? obj.type : '') + '): ' + this.msg.body); + Services.logger.log('From target(' + (obj.type ? obj.type : '') + '): ' + this.msg.body); if (typeof obj.running === 'boolean') { this.isRunning = obj.running; } @@ -182,7 +181,7 @@ class ResReqNetSocket extends EventEmitter { public send(data) { if (this.connected) { - Logger.log('To target: ' + data); + Services.logger.log('To target: ' + data); this.conn.write('Content-Length: ' + data.length + '\r\n\r\n' + data); this.hasNewDataMessage = true; if (!this.isMessageFlushLoopStarted) { @@ -438,7 +437,7 @@ export class AndroidConnection implements INSDebugConnection { } public attach(port: number, url?: string): Promise { - Logger.log('Attempting to attach on port ' + port); + Services.logger.log('Attempting to attach on port ' + port); return this._attach(port, url); //.then(() => this.sendMessage('Console.enable')) } diff --git a/src/debug-adapter/connection/iosConnection.ts b/src/debug-adapter/connection/iosConnection.ts index c56540f..5fce5cf 100644 --- a/src/debug-adapter/connection/iosConnection.ts +++ b/src/debug-adapter/connection/iosConnection.ts @@ -6,9 +6,8 @@ import * as net from 'net'; import * as stream from 'stream'; import {EventEmitter} from 'events'; import {INSDebugConnection} from './INSDebugConnection'; -import * as utils from '../utilities'; -import {Logger} from '../utilities'; -import * as ns from '../../services/NsCliService'; +import * as utils from '../../common/utilities'; +import {DebugAdapterServices as Services} from '../../services/debugAdapterServices'; interface IMessageWithId { id: number; @@ -74,7 +73,7 @@ class ResReqTcpSocket extends EventEmitter { }); unixSocket.on('close', () => { - Logger.log('Unix socket closed'); + Services.logger.log('Unix socket closed'); this.emit('close'); }); @@ -83,7 +82,7 @@ class ResReqTcpSocket extends EventEmitter { packetsStream.on('data', (buffer: Buffer) => { let packet = buffer.toString('utf16le'); - Logger.log('From target: ' + packet); + Services.logger.log('From target: ' + packet); this.onMessage(JSON.parse(packet)); }); } catch (e) { @@ -111,7 +110,7 @@ class ResReqTcpSocket extends EventEmitter { this._pendingRequests.set(message.id, resolve); this._unixSocketAttached.then(socket => { const msgStr = JSON.stringify(message); - Logger.log('To target: ' + msgStr); + Services.logger.log('To target: ' + msgStr); let encoding = "utf16le"; let length = Buffer.byteLength(msgStr, encoding); let payload = new Buffer(length + 4); @@ -156,7 +155,7 @@ export class IosConnection implements INSDebugConnection { * Attach the underlying Unix socket */ public attach(filePath: string): Promise { - Logger.log('Attempting to attach to path ' + filePath); + Services.logger.log('Attempting to attach to path ' + filePath); return utils.retryAsync(() => this._attach(filePath), 6000) .then(() => { Promise.all([ diff --git a/src/debug-adapter/consoleHelper.ts b/src/debug-adapter/consoleHelper.ts index 82b0052..1adfd4b 100644 --- a/src/debug-adapter/consoleHelper.ts +++ b/src/debug-adapter/consoleHelper.ts @@ -3,7 +3,7 @@ *--------------------------------------------------------*/ import * as url from 'url'; -import * as Utilities from './utilities'; +import * as Utilities from '../common/utilities'; export function formatConsoleMessage(m: WebKitProtocol.Console.Message, isClientPath :boolean = false): { text: string, isError: boolean } { let outputText: string; diff --git a/src/debug-adapter/debugRequest.ts b/src/debug-adapter/debugRequest.ts new file mode 100644 index 0000000..0413c24 --- /dev/null +++ b/src/debug-adapter/debugRequest.ts @@ -0,0 +1,60 @@ +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" && (this._requestArgs).rebuild; + } + + public get isSync(): boolean { + return this.args.request == "launch" && !(this._requestArgs).rebuild; + } + + 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.isSync) ? 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/webKitDebug.ts b/src/debug-adapter/webKitDebug.ts index 0a33ae0..1a250a3 100644 --- a/src/debug-adapter/webKitDebug.ts +++ b/src/debug-adapter/webKitDebug.ts @@ -3,6 +3,5 @@ *--------------------------------------------------------*/ import {WebKitDebugSession} from './webKitDebugSession'; -import {DebugSession} from 'vscode-debugadapter'; -DebugSession.run(WebKitDebugSession); +WebKitDebugSession.run(WebKitDebugSession); diff --git a/src/debug-adapter/webKitDebugAdapter.ts b/src/debug-adapter/webKitDebugAdapter.ts index fbd6f3a..06e6229 100644 --- a/src/debug-adapter/webKitDebugAdapter.ts +++ b/src/debug-adapter/webKitDebugAdapter.ts @@ -2,18 +2,22 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -import {spawn, ChildProcess} from 'child_process'; +import * as os from 'os'; +import * as fs from 'fs'; import * as path from 'path'; 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 * as utils from './utilities'; +import {Project, DebugResult} from '../project/project'; +import {IosProject} from '../project/iosProject'; +import {AndroidProject} from '../project/androidProject'; +import * as utils from '../common/utilities'; import {formatConsoleMessage} from './consoleHelper'; -import * as ns from '../services/NsCliService'; -import {AnalyticsService} from '../services/analytics/AnalyticsService'; -import {ExtensionClient} from '../services/ipc/ExtensionClient'; +import {DebugAdapterServices as Services} from '../services/debugAdapterServices'; +import {LoggerHandler, Handlers, Tags} from '../common/Logger'; +import {DebugRequest} from './debugRequest'; interface IScopeVarHandle { objectId: string; @@ -26,27 +30,27 @@ export class WebKitDebugAdapter implements DebugProtocol.IDebugAdapter { private _initArgs: DebugProtocol.InitializeRequestArguments; - private _clientAttached: boolean; private _variableHandles: Handles; private _currentStack: WebKitProtocol.Debugger.CallFrame[]; private _committedBreakpointsByUrl: Map; - private _overlayHelper: utils.DebounceHelper; private _exceptionValueObject: WebKitProtocol.Runtime.RemoteObject; private _expectingResumedEvent: boolean; private _scriptsById: Map; private _setBreakpointsRequestQ: Promise; - - private _chromeProc: ChildProcess; private _webKitConnection: INSDebugConnection; private _eventHandler: (event: DebugProtocol.Event) => void; - private appRoot: string; - private platform: string; - private isAttached: boolean; private _lastOutputEvent: OutputEvent; + private _loggerFrontendHandler: LoggerHandler = args => this.fireEvent(new OutputEvent(` ›${args.message}\n`, args.type.toString())); + private _request: DebugRequest; public constructor() { this._variableHandles = new Handles(); - this._overlayHelper = new utils.DebounceHelper(/*timeoutMs=*/200); + + // 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(); } @@ -63,7 +67,6 @@ export class WebKitDebugAdapter implements DebugProtocol.IDebugAdapter { } private clearClientContext(): void { - this._clientAttached = false; this.fireEvent({ seq: 0, type: 'event', event: 'clearClientContext'}); } @@ -97,81 +100,81 @@ export class WebKitDebugAdapter implements DebugProtocol.IDebugAdapter { } public launch(args: DebugProtocol.ILaunchRequestArgs): Promise { - return this._attach(args); + return this.processRequest(args); } public attach(args: DebugProtocol.IAttachRequestArgs): Promise { - return this._attach(args); + return this.processRequest(args); } - private initDiagnosticLogging(name: string, args: DebugProtocol.IAttachRequestArgs | DebugProtocol.ILaunchRequestArgs): void { + private configureLoggingForRequest(args: DebugProtocol.IRequestArgs): void { if (args.diagnosticLogging) { - utils.Logger.enableDiagnosticLogging(); - utils.Logger.log(`initialize(${JSON.stringify(this._initArgs) })`); - utils.Logger.log(`${name}(${JSON.stringify(args) })`); + // 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) })`); + Services.logger.log(`${args.request}(${JSON.stringify(args)})`); + } + + private processRequest(args: DebugProtocol.IRequestArgs) { + this.configureLoggingForRequest(args); + // Initialize the request + Services.appRoot = args.appRoot; + Services.cliPath = args.nativescriptCliPath || Services.cliPath; + this._request = new DebugRequest(args, Services.cli); + Services.extensionClient.analyticsLaunchDebugger({ request: this._request.isSync ? "sync" : args.request, platform: args.platform }); + + // Run CLI Command + let cliCommand: DebugResult; + if (this._request.isLaunch) { + cliCommand = this._request.project.debug({ stopOnEntry: this._request.launchArgs.stopOnEntry }, this._request.args.tnsArgs); + } + else if (this._request.isSync) { + cliCommand = this._request.project.debugWithSync({ stopOnEntry: this._request.launchArgs.stopOnEntry, syncAllFiles: this._request.launchArgs.syncAllFiles }, this._request.args.tnsArgs); + } + else if (this._request.isAttach) { + cliCommand = this._request.project.attach(this._request.args.tnsArgs); } - } - - private _attach(args: DebugProtocol.IAttachRequestArgs | DebugProtocol.ILaunchRequestArgs) { - ExtensionClient.setAppRoot(utils.getAppRoot(args)); - let analyticsRequest = (args.request == "launch" && !(args as DebugProtocol.ILaunchRequestArgs).rebuild) ? "sync" : args.request; - ExtensionClient.getInstance().analyticsLaunchDebugger({ request: analyticsRequest, platform: args.platform }); - this.initDiagnosticLogging(args.request, args); - this.appRoot = utils.getAppRoot(args); - this.platform = args.platform; - this.isAttached = false; - - return ((args.platform == 'ios') ? this._attachIos(args) : this._attachAndroid(args)) - .then(() => { - this.isAttached = true; - this.fireEvent(new InitializedEvent()); - }, - e => { - this.onTnsOutputMessage("Command failed: " + e, "error"); - this.clearEverything(); - return utils.errP(e); - }); - } - private _attachIos(args: DebugProtocol.IAttachRequestArgs | DebugProtocol.ILaunchRequestArgs): Promise { - let iosProject : ns.IosProject = new ns.IosProject(this.appRoot, args.tnsOutput); - iosProject.on('TNS.outputMessage', (message, level) => this.onTnsOutputMessage.apply(this, [message, level])); + if (cliCommand.tnsProcess) { + 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(`The tns command finished its execution with code ${code}.`, Tags.FrontendMessage); }); + } - return iosProject.debug(args) - .then((socketFilePath) => { - let iosConnection: IosConnection = new IosConnection(); - this.setConnection(iosConnection, args); - return iosConnection.attach(socketFilePath); + // Attach to the running application + let connectionEstablished = cliCommand.backendIsReadyForConnection.then((connectionToken: string | number) => { + if (this._request.isAndroid) { + Services.logger.log(`Attaching to application on port ${connectionToken}`); + let androidConnection = new AndroidConnection(); + this.setConnection(androidConnection); + let port: number = connectionToken; + return androidConnection.attach(port, 'localhost'); + } + if (this._request.isIos) { + Services.logger.log(`Attaching to application on socket path ${connectionToken}`); + let iosConnection = new IosConnection(); + this.setConnection(iosConnection); + let socketFilePath: string = connectionToken; + return iosConnection.attach(socketFilePath); + } }); - } - - private _attachAndroid(args: DebugProtocol.IAttachRequestArgs | DebugProtocol.ILaunchRequestArgs): Promise { - let androidProject: ns.AndroidProject = new ns.AndroidProject(this.appRoot, args.tnsOutput); - let thisAdapter: WebKitDebugAdapter = this; - androidProject.on('TNS.outputMessage', (message, level) => thisAdapter.onTnsOutputMessage.apply(thisAdapter, [message, level])); - - this.onTnsOutputMessage("Getting debug port"); - let androidConnection: AndroidConnection = null; - - let runDebugCommand: Promise = (args.request == 'launch') ? androidProject.debug(args) : Promise.resolve(); - - return runDebugCommand.then(_ => { - let port: number; - return androidProject.getDebugPort(args).then(debugPort => { - port = debugPort; - if (!thisAdapter._webKitConnection) { - androidConnection = new AndroidConnection(); - this.setConnection(androidConnection, args); - } - }).then(() => { - this.onTnsOutputMessage("Attaching to debug application"); - return androidConnection.attach(port, 'localhost'); - }); + // Send InitializedEvent + return connectionEstablished.then(() => this.fireEvent(new InitializedEvent()), e => { + Services.logger.error(`Error: ${e}`, Tags.FrontendMessage); + this.clearEverything(); + return utils.errP(e); }); } - private setConnection(connection: INSDebugConnection, args: DebugProtocol.IAttachRequestArgs | DebugProtocol.ILaunchRequestArgs) : INSDebugConnection { + private setConnection(connection: INSDebugConnection) : INSDebugConnection { + let args = this._request.args; connection.on('Debugger.paused', params => this.onDebuggerPaused(params)); connection.on('Debugger.resumed', () => this.onDebuggerResumed()); connection.on('Debugger.scriptParsed', params => this.onScriptParsed(params)); @@ -182,29 +185,13 @@ export class WebKitDebugAdapter implements DebugProtocol.IDebugAdapter { connection.on('Inspector.detached', () => this.terminateSession()); connection.on('close', () => this.terminateSession()); connection.on('error', () => this.terminateSession()); - connection.on('connect', () => this.onConnected(args)) + connection.on('connect', () => this.onConnected()) this._webKitConnection = connection; return connection; } - private onConnected(args: DebugProtocol.IAttachRequestArgs | DebugProtocol.ILaunchRequestArgs): void { - this.onTnsOutputMessage("Debugger connected"); - } - - private onTnsOutputMessage(message: string, level: string = "log"): void { - utils.Logger.log(message); - - if (!this.isAttached) { - let messageParams: WebKitProtocol.Console.MessageAddedParams = { - message: { - level: level, - type: 'log', - text: message - } - }; - - this.onConsoleMessage(messageParams); - } + private onConnected(): void { + Services.logger.log("Debugger connected"); } private fireEvent(event: DebugProtocol.Event): void { @@ -214,22 +201,18 @@ export class WebKitDebugAdapter implements DebugProtocol.IDebugAdapter { } private terminateSession(): void { - if (this._clientAttached) { - this.fireEvent(new TerminatedEvent()); - } + //this.fireEvent(new TerminatedEvent()); - this.onTnsOutputMessage("Terminating debug session"); + Services.logger.log("Terminating debug session"); this.clearEverything(); } private clearEverything(): void { this.clearClientContext(); this.clearTargetContext(); - this.isAttached = false; - this._chromeProc = null; if (this._webKitConnection) { - this.onTnsOutputMessage("Closing debug connection"); + Services.logger.log("Closing debug connection"); this._webKitConnection.close(); this._webKitConnection = null; @@ -318,7 +301,7 @@ export class WebKitDebugAdapter implements DebugProtocol.IDebugAdapter { let isClientPath = false; if (localMessage.url) { - const clientPath = utils.webkitUrlToClientPath(this.appRoot, this.platform, localMessage.url); + const clientPath = utils.webkitUrlToClientPath(this._request.args.appRoot, this._request.args.platform, localMessage.url); if (clientPath !== '') { localMessage.url = clientPath; isClientPath = true; @@ -340,11 +323,6 @@ export class WebKitDebugAdapter implements DebugProtocol.IDebugAdapter { } public disconnect(): Promise { - if (this._chromeProc) { - this._chromeProc.kill('SIGINT'); - this._chromeProc = null; - } - this.clearEverything(); return Promise.resolve(); diff --git a/src/debug-adapter/webKitDebugSession.ts b/src/debug-adapter/webKitDebugSession.ts index 7e0d66b..a127c7d 100644 --- a/src/debug-adapter/webKitDebugSession.ts +++ b/src/debug-adapter/webKitDebugSession.ts @@ -1,11 +1,9 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ import {OutputEvent, DebugSession, ErrorDestination} from 'vscode-debugadapter'; import {DebugProtocol} from 'vscode-debugprotocol'; import {WebKitDebugAdapter} from './webKitDebugAdapter'; -import {Logger} from './utilities'; +import {Handlers} from '../common/Logger'; +import {DebugAdapterServices as Services} from '../services/debugAdapterServices'; import {AdapterProxy} from './adapter/adapterProxy'; import {LineNumberTransformer} from './adapter/lineNumberTransformer'; @@ -18,10 +16,15 @@ export class WebKitDebugSession extends DebugSession { public constructor(targetLinesStartAt1: boolean, isServer: boolean = false) { super(targetLinesStartAt1, isServer); - Logger.init(isServer, msg => this.sendEvent(new OutputEvent(` ›${msg}\n`))); + // 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 => { - Logger.log(`******** ERROR! Unhandled promise rejection: ${reason}`); + Services.logger.log(`******** ERROR! Unhandled promise rejection: ${reason}`); }); this._adapterProxy = new AdapterProxy( @@ -40,7 +43,7 @@ export class WebKitDebugSession extends DebugSession { public sendEvent(event: DebugProtocol.Event): void { if (event.event !== 'output') { // Don't create an infinite loop... - Logger.log(`To client: ${JSON.stringify(event) }`); + Services.logger.log(`To client: ${JSON.stringify(event) }`); } super.sendEvent(event); @@ -50,7 +53,7 @@ export class WebKitDebugSession extends DebugSession { * Overload sendResponse to log */ public sendResponse(response: DebugProtocol.Response): void { - Logger.log(`To client: ${JSON.stringify(response) }`); + Services.logger.log(`To client: ${JSON.stringify(response) }`); super.sendResponse(response); } @@ -85,7 +88,7 @@ export class WebKitDebugSession extends DebugSession { // These errors show up in the message bar at the top (or nowhere), sometimes not obvious that they // come from the adapter response.message = '[NativeScript-Debug-Adapter] ' + eStr; - Logger.log('Error: ' + e ? e.stack : eStr); + Services.logger.log('Error: ' + e ? e.stack : eStr); } response.success = false; @@ -99,7 +102,7 @@ export class WebKitDebugSession extends DebugSession { protected dispatchRequest(request: DebugProtocol.Request): void { const response = { seq: 0, type: 'response', request_seq: request.seq, command: request.command, success: true }; try { - Logger.log(`From client: ${request.command}(${JSON.stringify(request.arguments) })`); + Services.logger.log(`From client: ${request.command}(${JSON.stringify(request.arguments) })`); this.sendResponseAsync( request, response, diff --git a/src/services/ipc/ExtensionClient.ts b/src/ipc/ExtensionClient.ts similarity index 75% rename from src/services/ipc/ExtensionClient.ts rename to src/ipc/ExtensionClient.ts index 2921a39..3ff6738 100644 --- a/src/services/ipc/ExtensionClient.ts +++ b/src/ipc/ExtensionClient.ts @@ -5,39 +5,19 @@ import * as extProtocol from './ExtensionProtocol'; let ipc = require('node-ipc'); export class ExtensionClient { - private static _appRoot: string; - private static _instance: ExtensionClient; - + private _appRoot: string; private _idCounter = 0; private _pendingRequests: Object; private _ipcClientInitialized: Promise; - public static getInstance() { - if (!this._instance) { - this._instance = new ExtensionClient(); - } - return this._instance; - } - - public static getAppRoot() { - return this._appRoot; - } - public static getTempFilePathForDirectory(directoryPath: string) { let fileName: string = 'vsc-ns-ext-' + crypto.createHash('md5').update(directoryPath).digest("hex") + '.sock'; return path.join(os.tmpdir(), fileName); } - public static setAppRoot(appRoot: string) { + constructor(appRoot: string) { this._appRoot = appRoot; - } - - constructor() { - if (!ExtensionClient.getAppRoot()) { - throw new Error(`Unable to connect to extension host. App root is '${ExtensionClient.getAppRoot()}'`); - } - this._idCounter = 0; this._pendingRequests = {}; @@ -47,7 +27,7 @@ export class ExtensionClient { this._ipcClientInitialized = new Promise((res, rej) => { ipc.connectTo( 'extHost', - ExtensionClient.getTempFilePathForDirectory(ExtensionClient.getAppRoot()), + ExtensionClient.getTempFilePathForDirectory(this._appRoot), () => { ipc.of.extHost.on('connect', () => { res(); diff --git a/src/services/ipc/ExtensionProtocol.ts b/src/ipc/ExtensionProtocol.ts similarity index 100% rename from src/services/ipc/ExtensionProtocol.ts rename to src/ipc/ExtensionProtocol.ts diff --git a/src/services/ipc/ExtensionServer.ts b/src/ipc/ExtensionServer.ts similarity index 82% rename from src/services/ipc/ExtensionServer.ts rename to src/ipc/ExtensionServer.ts index ff3a60e..c1818ae 100644 --- a/src/services/ipc/ExtensionServer.ts +++ b/src/ipc/ExtensionServer.ts @@ -3,21 +3,12 @@ import * as os from 'os'; import * as crypto from 'crypto'; import * as vscode from 'vscode'; import * as extProtocol from './ExtensionProtocol'; -import {AnalyticsService} from '../analytics/AnalyticsService'; +import {ExtensionHostServices as Services} from '../services/extensionHostServices'; let ipc = require('node-ipc'); export class ExtensionServer { - private static _instance: ExtensionServer; - private _isRunning: boolean; - public static getInstance() { - if (!this._instance) { - this._instance = new ExtensionServer(); - } - return this._instance; - } - public static getTempFilePathForDirectory(directoryPath: string) { let fileName: string = 'vsc-ns-ext-' + crypto.createHash('md5').update(directoryPath).digest("hex") + '.sock'; return path.join(os.tmpdir(), fileName); @@ -66,10 +57,10 @@ export class ExtensionServer { } public analyticsLaunchDebugger(args: extProtocol.AnalyticsLaunchDebuggerArgs): Promise { - return AnalyticsService.getInstance().launchDebugger(args.request, args.platform); + return Services.analyticsService.launchDebugger(args.request, args.platform); } public runRunCommand(args: extProtocol.AnalyticsRunRunCommandArgs): Promise { - return AnalyticsService.getInstance().runRunCommand(args.platform); + return Services.analyticsService.runRunCommand(args.platform); } } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 3a08fde..e561700 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,89 +1,66 @@ import * as vscode from 'vscode'; -import * as child from 'child_process'; -import * as ns from './services/NsCliService'; -import {ExtensionVersionInfo} from './services/ExtensionVersionInfo'; -import {AnalyticsService} from './services/analytics/AnalyticsService'; -import {ExtensionServer} from './services/ipc/ExtensionServer'; +import {CliVersion} from './project/nativeScriptCli'; +import {ExtensionHostServices as Services} from './services/extensionHostServices'; +import {Project} from './project/project'; +import {IosProject} from './project/iosProject'; +import {AndroidProject} from './project/androidProject'; -function performVersionsCheck(context: vscode.ExtensionContext) { - // Check the state of the existing NativeScript CLI - let cliInfo: ns.CliVersionInfo = new ns.CliVersionInfo(); - if (cliInfo.getErrorMessage() !== null) { - vscode.window.showErrorMessage(cliInfo.getErrorMessage()); - } - else { - // Checks whether a new version of the extension is available - let extensionVersionPromise: Promise = null; +// this method is called when the extension is activated +export function activate(context: vscode.ExtensionContext) { + Services.globalState = context.globalState; + Services.extensionServer.start(); - // Check the cache for extension version information - let extensionVersion: any = context.globalState.get('ExtensionVersionInfo'); - if (extensionVersion) { - let extensionVersionInfo = new ExtensionVersionInfo(extensionVersion.latestVersionMetadata, extensionVersion.timestamp); - if (extensionVersionInfo.getTimestamp() > Date.now() - 24 * 60 * 60 * 1000 /* Cache the version for a day */) { - extensionVersionPromise = Promise.resolve(extensionVersionInfo); - } + // Check for newer extension version + Services.extensionVersionService.isLatestInstalled.then(result => { + if (!result.result) { + vscode.window.showWarningMessage(result.error); } + }); - if (!extensionVersionPromise) { - // Takes the slow path and checks for newer version in the VS Code Marketplace - extensionVersionPromise = ExtensionVersionInfo.createFromMarketplace(); - } - extensionVersionPromise.then(extensionInfo => { - if (extensionInfo) { - context.globalState.update('ExtensionVersionInfo', { latestVersionMetadata: extensionInfo.getLatestVersionMeta(), timestamp: extensionInfo.getTimestamp() }); // save in cache - if (!extensionInfo.isLatest()) { - vscode.window.showWarningMessage(`A new version of the NativeScript extension is available. Run "Extensions: Show Outdated Extensions" command and select "NativeScript" to update to v${extensionInfo.getLatestVersionMeta().version}.`); - } - } - }, error => { /* In case of error behave as if the extension verison is latest */ }); + // 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); } -} -// this method is called when the extension is activated -export function activate(context: vscode.ExtensionContext) { - ExtensionServer.getInstance().start(); - performVersionsCheck(context); - - let runCommand = (project: ns.NSProject) => { + let 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.platform()}`); + let runChannel: vscode.OutputChannel = vscode.window.createOutputChannel(`Run on ${project.platformName()}`); runChannel.clear(); runChannel.show(vscode.ViewColumn.Two); - AnalyticsService.getInstance().runRunCommand(project.platform()); + Services.analyticsService.runRunCommand(project.platformName()); - return project.run() - .then(tnsProcess => { - tnsProcess.on('error', err => { - vscode.window.showErrorMessage('Unexpected error executing NativeScript Run command.'); - }); - tnsProcess.stderr.on('data', data => { - runChannel.append(data.toString()); - }); - tnsProcess.stdout.on('data', data => { - runChannel.append(data.toString()); - }); - tnsProcess.on('exit', exitCode => { - tnsProcess.stdout.removeAllListeners('data'); - tnsProcess.stderr.removeAllListeners('data'); - }); - tnsProcess.on('close', exitCode => { - runChannel.hide(); - }); + let tnsProcess = project.run(); + tnsProcess.on('error', err => { + vscode.window.showErrorMessage('Unexpected error executing NativeScript Run command.'); + }); + tnsProcess.stderr.on('data', data => { + runChannel.append(data.toString()); + }); + tnsProcess.stdout.on('data', data => { + runChannel.append(data.toString()); + }); + tnsProcess.on('exit', exitCode => { + tnsProcess.stdout.removeAllListeners('data'); + tnsProcess.stderr.removeAllListeners('data'); + }); + tnsProcess.on('close', exitCode => { + runChannel.hide(); }); }; let runIosCommand = vscode.commands.registerCommand('nativescript.runIos', () => { - return runCommand(new ns.IosProject(vscode.workspace.rootPath)); + return runCommand(new IosProject(vscode.workspace.rootPath, Services.cli)); }); let runAndroidCommand = vscode.commands.registerCommand('nativescript.runAndroid', () => { - return runCommand(new ns.AndroidProject(vscode.workspace.rootPath)); + return runCommand(new AndroidProject(vscode.workspace.rootPath, Services.cli)); }); context.subscriptions.push(runIosCommand); @@ -91,5 +68,5 @@ export function activate(context: vscode.ExtensionContext) { } export function deactivate() { - ExtensionServer.getInstance().stop(); + Services.extensionServer.stop(); } \ No newline at end of file diff --git a/src/project/androidProject.ts b/src/project/androidProject.ts new file mode 100644 index 0000000..c1660d8 --- /dev/null +++ b/src/project/androidProject.ts @@ -0,0 +1,60 @@ +import {ChildProcess} from 'child_process'; +import * as stream from 'stream'; +import {Project, DebugResult} 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 { + + constructor(appRoot: string, cli: NativeScriptCli) { + super(appRoot, cli); + } + + public platformName(): string { + return "android"; + } + + public attach(tnsArgs?: string[]): DebugResult { + return { tnsProcess: null, backendIsReadyForConnection: this.getDebugPort().debugPort }; + } + + public debugWithSync(options: { stopOnEntry: boolean, syncAllFiles: boolean }, tnsArgs?: string[]): DebugResult { + let args: string[] = ["--no-rebuild"]; + if (options.syncAllFiles) { args.push("--syncAllFiles"); } + args = args.concat(tnsArgs); + + return this.debug({stopOnEntry: options.stopOnEntry}, args); + } + + public debug(options: { stopOnEntry: boolean }, tnsArgs?: string[]): DebugResult { + let args: string[] = []; + if (options.stopOnEntry) { args.push("--debug-brk"); } + args = args.concat(tnsArgs); + + let debugProcess : ChildProcess = super.executeDebugCommand(args); + let backendIsReady = new scanner.StringMatchingScanner(debugProcess.stdout).nextMatch('# NativeScript Debugger started #').then(_ => { + // wait a little before trying to connect, this gives a chance for adb to be able to connect to the debug socket + return new Promise((resolve, reject) => setTimeout(() => { resolve(); }, 500)); + }).then(() => this.getDebugPort().debugPort); + + return { tnsProcess: debugProcess, backendIsReadyForConnection: backendIsReady }; + } + + private getDebugPort(): GetDebugPortResult { + let debugProcess : ChildProcess = super.executeDebugCommand(["--get-port"]); + let portNumberPromise: Promise = this.waitForPortNumber(debugProcess.stdout); + return { tnsProcess: debugProcess, debugPort: portNumberPromise }; + } + + private waitForPortNumber(readableStream: stream.Readable): Promise { + let streamScanner = new scanner.StringMatchingScanner(readableStream); + return streamScanner.nextMatch(new RegExp("(?:debug port: )([\\d]{5})")).then((match: scanner.MatchFound) => { + let portNumber = parseInt(match.matches[1]); + streamScanner.stop(); + return portNumber; + }); + } +} diff --git a/src/project/iosProject.ts b/src/project/iosProject.ts new file mode 100644 index 0000000..4de4849 --- /dev/null +++ b/src/project/iosProject.ts @@ -0,0 +1,63 @@ +import {ChildProcess} from 'child_process'; +import * as stream from 'stream'; +import {Project, DebugResult} from './project'; +import * as scanner from './streamScanner'; +import {Version} from '../common/version'; +import {NativeScriptCli} from './NativeScriptCli'; + +export class IosProject extends Project { + + constructor(appRoot: string, cli: NativeScriptCli) { + super(appRoot, cli); + + if (!this.isPlatformOSX()) { + throw new Error('iOS platform is supported only on OS X.'); + } + } + + public platformName(): string { + return "ios"; + } + + public attach(tnsArgs?: string[]): DebugResult { + let args: string[] = ["--start"]; + args = args.concat(tnsArgs); + + let debugProcess : ChildProcess = super.executeDebugCommand(args); + let socketPathPromise = this.waitForSocketPath(debugProcess.stdout); + return { tnsProcess: debugProcess, backendIsReadyForConnection: socketPathPromise }; + } + + public debugWithSync(options: { stopOnEntry: boolean, syncAllFiles: boolean }, tnsArgs?: string[]): DebugResult { + let args: string[] = ["--no-rebuild"]; + if (options.syncAllFiles) { args.push("--syncAllFiles"); } + args = args.concat(tnsArgs); + + return this.debug({stopOnEntry: options.stopOnEntry}, args); + } + + public debug(options: { stopOnEntry: boolean }, tnsArgs?: string[]): DebugResult { + let args: string[] = []; + if (options.stopOnEntry) { args.push("--debug-brk"); } + args = args.concat(tnsArgs); + + let debugProcess : ChildProcess = super.executeDebugCommand(args); + let socketPathPromise = this.waitForSocketPath(debugProcess.stdout); + return { tnsProcess: debugProcess, backendIsReadyForConnection: socketPathPromise }; + } + + private waitForSocketPath(readableStream: stream.Readable): Promise { + let socketPathPrefix = 'socket-file-location: '; + let streamScanner = new scanner.StringMatchingScanner(readableStream); + + return streamScanner.nextMatch(new RegExp(socketPathPrefix + '.*\.sock')).then((match: scanner.MatchFound) => { + let socketPath = (match.matches[0]).substr(socketPathPrefix.length); + streamScanner.stop(); + return socketPath; + }); + } + + private isPlatformOSX(): boolean { + return /^darwin/.test(process.platform); + } +} diff --git a/src/project/nativeScriptCli.ts b/src/project/nativeScriptCli.ts new file mode 100644 index 0000000..b5e61ee --- /dev/null +++ b/src/project/nativeScriptCli.ts @@ -0,0 +1,82 @@ +import {spawn, execSync, ChildProcess} from 'child_process'; +import {Version} from '../common/version'; +import {Services} from '../services/services'; +import * as utils from '../common/utilities'; + +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 _path: string; + private _cliVersion: CliVersion; + + constructor(cliPath: string) { + this._path = cliPath; + + let versionStr = this.executeSync(["--version"], undefined); + 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); + } + } + + public get path(): string { return this._path; } + + public get version(): CliVersion { + return this._cliVersion; + } + + public executeSync(args: string[], cwd: string): string { + let command: string = `${this._path} ` + args.join(' '); + Services.logger.log(`[NativeScriptCli] execute sync: ${command}`); + return execSync(command, { encoding: "utf8", cwd: cwd }).toString().trim(); + } + + public execute(args: string[], cwd: string): ChildProcess { + let command: string = `${this._path} ` + args.join(' '); + Services.logger.log(`[NativeScriptCli] execute async: ${command}`); + + let options = { cwd: cwd, shell: true }; + let child: ChildProcess = spawn(this._path, args, options); + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + return child; + } +} diff --git a/src/project/project.ts b/src/project/project.ts new file mode 100644 index 0000000..426ff3c --- /dev/null +++ b/src/project/project.ts @@ -0,0 +1,39 @@ +import {ChildProcess} from 'child_process'; +import {Version} from '../common/version'; +import {NativeScriptCli} from './nativeScriptCli'; + +export type DebugResult = { tnsProcess: ChildProcess, backendIsReadyForConnection: Promise }; + +export abstract class Project { + private _appRoot: string; + private _cli: NativeScriptCli; + + constructor(appRoot: string, cli: NativeScriptCli) { + this._appRoot = appRoot; + this._cli = cli; + } + + public get rootPath(): string { return this._appRoot; } + + public get cli(): NativeScriptCli { return this._cli; } + + public abstract platformName(): string; + + public run(tnsArgs?: string[]): ChildProcess { + return this.executeRunCommand(tnsArgs); + } + + public abstract attach(tnsArgs?: string[]): DebugResult; + + public abstract debugWithSync(options: { stopOnEntry: boolean, syncAllFiles: boolean }, tnsArgs?: string[]): DebugResult; + + public abstract debug(options: { stopOnEntry: boolean }, tnsArgs?: string[]): DebugResult; + + protected executeRunCommand(args: string[]): ChildProcess { + 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); + } +} diff --git a/src/project/streamScanner.ts b/src/project/streamScanner.ts new file mode 100644 index 0000000..9fc0b1f --- /dev/null +++ b/src/project/streamScanner.ts @@ -0,0 +1,84 @@ +import * as stream from 'stream'; + +export class StreamScanner { + private _stream: stream.Readable; + private _scanCallback: (data: string, stop: () => void) => void; + + constructor(stream: stream.Readable, scanCallback: (data: string, stop: () => void) => void) { + this._stream = stream; + this._scanCallback = scanCallback; + this._stream.on("data", this.scan.bind(this)); + } + + public stop() { + this._stream.removeListener("data", this.scan); + } + + private scan(data: string | Buffer): void { + this._scanCallback(data.toString(), this.stop); + } +} + +export type MatchFound = { + chunk: string, + matches: RegExpMatchArray | number[] +}; + +type MatchMeta = { + promise: Promise, + resolve: (match: MatchFound) => void, + reject: (err: Error) => void, + test: string | RegExp +}; + +export class StringMatchingScanner extends StreamScanner { + private _metas: MatchMeta[]; + + constructor(stream: stream.Readable) { + super(stream, (data: string, stop: () => void) => { + this._metas.forEach((meta, metaIndex) => { + if (meta.test instanceof RegExp) { + let result: RegExpMatchArray = data.match(meta.test); + 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) { + result.push(dataIndex); + } + if (result.length > 0) { + this.matchFound(metaIndex, { chunk: data, matches: result }); + } + } + else { + throw new TypeError("Invalid type"); + } + }); + }); + this._metas = []; + } + + public nextMatch(test: string | RegExp): Promise { + let meta: MatchMeta = { + test: test, + resolve: null, + reject: null, + promise: null + }; + meta.promise = new Promise((resolve, reject) => { + meta.resolve = resolve; + meta.reject = reject; + }); + 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 + meta.resolve(matchResult); + } +} diff --git a/src/services/ExtensionVersionInfo.ts b/src/services/ExtensionVersionInfo.ts deleted file mode 100644 index 67f7c6c..0000000 --- a/src/services/ExtensionVersionInfo.ts +++ /dev/null @@ -1,104 +0,0 @@ -import {Version} from '../common/Version'; -import * as https from 'https'; - -export class ExtensionVersionInfo { - private static extensionVersion: number[] = null; - private static minNativescriptCliVersion: number[] = null; - private static extensionId: string = '8d837914-d8fa-45b5-965d-f76ebd6dbf5c'; - private static marketplaceQueryResult: Promise = null; - - private latestVersionMeta: any; - private timestamp: number; - - private static initVersionsFromPackageJson() { - let packageJson = require('../../package.json'); - this.extensionVersion = Version.parse(packageJson.version); - this.minNativescriptCliVersion = Version.parse(packageJson.minNativescriptCliVersion); - } - - public static getExtensionVersion(): number[] { - if (this.extensionVersion === null) { - this.initVersionsFromPackageJson(); - } - return this.extensionVersion; - } - - public static getMinSupportedNativeScriptVersion(): number[] { - if (this.minNativescriptCliVersion === null) { - this.initVersionsFromPackageJson(); - } - return this.minNativescriptCliVersion; - } - - public static getMarketplaceExtensionData(): Promise { - if (this.marketplaceQueryResult == null) { - this.marketplaceQueryResult = new Promise((resolve, reject) => { - let postData: string = `{ filters: [{ criteria: [{ filterType: 4, value: "${ExtensionVersionInfo.extensionId}" }] }], flags: 262 }`; - - let request = https.request({ - hostname: 'marketplace.visualstudio.com', - path: '/_apis/public/gallery/extensionquery', - method: 'POST', - headers: { - 'Accept': 'application/json;api-version=2.2-preview.1', - 'Content-Type': 'application/json', - 'Transfer-Encoding': 'chunked', - 'Content-Length': Buffer.byteLength(postData) - } - }, response => { - if (response.statusCode != 200) { - reject(`Unable to download data from Visual Studio Marketplace. Status code: ${response.statusCode}`); - return; - } - let body = ''; - response.on('data', chunk => { - body += chunk; - }); - response.on('end', () => { - resolve(JSON.parse(body)); - }); - }); - - request.on('error', (e) => { - reject(e); - }); - - request.end(postData); - }); - } - return this.marketplaceQueryResult; - } - - public static createFromMarketplace(): Promise { - return this.getMarketplaceExtensionData() - .then(marketplaceData => { - let latestVersion = null; - try { - if (marketplaceData.results[0].extensions[0].extensionId == ExtensionVersionInfo.extensionId) { - latestVersion = marketplaceData.results[0].extensions[0].versions[0]; - } - } catch (e) { } - return new ExtensionVersionInfo(latestVersion); - }); - } - - constructor(latestVersionMeta: any, timestamp?: number) { - this.latestVersionMeta = latestVersionMeta; - this.timestamp = timestamp || Date.now(); - } - - public getLatestVersionMeta(): any { - return this.latestVersionMeta; - } - - public isLatest(): boolean { - if (!this.getLatestVersionMeta()) { - return true; - } - return Version.compareBySubminor(ExtensionVersionInfo.getExtensionVersion(), Version.parse(this.getLatestVersionMeta().version)) >= 0; - } - - public getTimestamp(): number { - return this.timestamp; - } -} \ No newline at end of file diff --git a/src/services/NsCliService.ts b/src/services/NsCliService.ts deleted file mode 100644 index 2531492..0000000 --- a/src/services/NsCliService.ts +++ /dev/null @@ -1,373 +0,0 @@ -import {spawn, execSync, ChildProcess} from 'child_process'; -import * as fs from 'fs'; -import {EventEmitter} from 'events'; -import * as path from 'path'; -import * as https from 'https'; -import {Version} from '../common/Version'; -import {Logger} from '../debug-adapter/utilities'; -import {ExtensionVersionInfo} from './ExtensionVersionInfo'; -import {DebugProtocol} from 'vscode-debugprotocol'; - -export enum CliVersionState { - NotExisting, - OlderThanSupported, - Compatible -} - -export class CliVersionInfo { - private static installedCliVersion: number[] = null; - - private _state: CliVersionState; - - public static getInstalledCliVersion(): number[] { - if (this.installedCliVersion === null) { - // get the currently installed CLI version - let getVersionCommand: string = new CommandBuilder().appendParam('--version').buildAsString(); // tns --version - try { - let versionStr: string = execSync(getVersionCommand).toString().trim(); // execute it - this.installedCliVersion = versionStr ? Version.parse(versionStr) : null; // parse the version string - } catch(e) { - this.installedCliVersion = null; - } - } - - return this.installedCliVersion; - } - - constructor() { - let installedCliVersion: number[] = CliVersionInfo.getInstalledCliVersion(); - if (installedCliVersion === null) { - this._state = CliVersionState.NotExisting; - } - else { - let minSupportedCliVersion = ExtensionVersionInfo.getMinSupportedNativeScriptVersion(); - this._state = Version.compareBySubminor(installedCliVersion, minSupportedCliVersion) < 0 ? CliVersionState.OlderThanSupported : CliVersionState.Compatible; - } - } - - public getState(): CliVersionState { - return this._state; - } - - public isCompatible(): boolean { - return this._state === CliVersionState.Compatible; - } - - public getErrorMessage(): string { - switch (this._state) { - case CliVersionState.NotExisting: - return `NativeScript CLI not found, please run 'npm -g install nativescript' to install it.`; - case CliVersionState.OlderThanSupported: - return `The existing NativeScript extension is compatible with NativeScript CLI v${Version.stringify(ExtensionVersionInfo.getMinSupportedNativeScriptVersion())} or greater. The currently installed NativeScript CLI is v${Version.stringify(CliVersionInfo.getInstalledCliVersion())}. You can update the NativeScript CLI by executing 'npm install -g nativescript'.`; - default: - return null; - } - } -} - -export abstract class NSProject extends EventEmitter { - private _projectPath: string; - private _tnsOutputFileStream: fs.WriteStream; - private _cliVersionInfo: CliVersionInfo; - - constructor(projectPath: string, tnsOutputFilePath?: string) { - super(); - this._projectPath = projectPath; - this._tnsOutputFileStream = tnsOutputFilePath ? fs.createWriteStream(tnsOutputFilePath) : null; - this._cliVersionInfo = new CliVersionInfo(); - } - - public getProjectPath(): string { - return this._projectPath; - } - - public getCliVersionInfo() { - return this._cliVersionInfo; - } - - public abstract platform(): string; - - public abstract run(): Promise; - - public abstract debug(args: DebugProtocol.IAttachRequestArgs | DebugProtocol.ILaunchRequestArgs): Promise; - - protected spawnProcess(commandPath: string, commandArgs: string[], tnsOutput?: string): ChildProcess { - let options = { cwd: this.getProjectPath(), shell: true }; - let child: ChildProcess = spawn(commandPath, commandArgs, options); - child.stdout.setEncoding('utf8'); - child.stderr.setEncoding('utf8'); - return child; - } - - protected writeToTnsOutputFile(message: string) { - if (this._tnsOutputFileStream) { - this._tnsOutputFileStream.write(message, 'utf8'); - } - } -} - -export class IosProject extends NSProject { - - constructor(projectPath: string, tnsOutputFilePath?: string) { - super(projectPath, tnsOutputFilePath); - } - - public platform(): string { - return 'ios'; - } - - public run(): Promise { - if (!this.isOSX()) { - return Promise.reject('iOS platform is only supported on OS X.'); - } - - // build command to execute - let command = new CommandBuilder() - .appendParam("run") - .appendParam(this.platform()) - .build(); - - let child: ChildProcess = this.spawnProcess(command.path, command.args); - return Promise.resolve(child); - } - - public debug(args: DebugProtocol.IAttachRequestArgs | DebugProtocol.ILaunchRequestArgs): Promise { - if (!this.isOSX()) { - return Promise.reject('iOS platform is supported only on OS X.'); - } - - let rebuild = (args.request == "launch") ? (args as DebugProtocol.ILaunchRequestArgs).rebuild : true; - // build command to execute - let command = new CommandBuilder(args.nativescriptCliPath) - .appendParam("debug") - .appendParam(this.platform()) - .appendParamIf("--emulator", args.emulator) - .appendParamIf("--start", args.request === "attach") - .appendParamIf("--debug-brk", args.request === "launch") - .appendParamIf("--no-rebuild", !rebuild) - .appendParamIf("--syncAllFiles", args.request === "launch" && !rebuild && (args as DebugProtocol.ILaunchRequestArgs).syncAllFiles) - .appendParam("--no-client") - .appendParams(args.tnsArgs) - .build(); - - let socketPathPrefix = 'socket-file-location: '; - let socketPathPattern: RegExp = new RegExp(socketPathPrefix + '.*\.sock'); - - let isSocketOpened = (cliOutput: string): string => { - let matches: RegExpMatchArray = cliOutput.match(socketPathPattern); - if(matches && matches.length > 0) { - return matches[0].substr(socketPathPrefix.length); - } - return null; - }; - - let isAppSynced = (cliOutput: string) => { - return cliOutput.indexOf('Successfully synced application') > -1; - }; - - return new Promise((resolve, reject) => { - // run NativeScript CLI command - let child: ChildProcess = this.spawnProcess(command.path, command.args, args.tnsOutput); - - let appSynced = false; - let socketPath: string = null; - - child.stdout.on('data', (data) => { - let cliOutput: string = data.toString(); - this.emit('TNS.outputMessage', cliOutput, 'log'); - this.writeToTnsOutputFile(cliOutput); - - socketPath = socketPath || isSocketOpened(cliOutput); - appSynced = rebuild ? false : (appSynced || isAppSynced(cliOutput)); - - if ((rebuild && socketPath) || (!rebuild && socketPath && appSynced)) { - resolve(socketPath); - } - }); - - child.stderr.on('data', (data) => { - this.emit('TNS.outputMessage', data, 'error'); - this.writeToTnsOutputFile(data.toString()); - }); - - child.on('close', (code, signal) => { - reject("The debug process exited unexpectedly code:" + code); - }); - }); - } - - private isOSX(): boolean { - return /^darwin/.test(process.platform); - } -} - -export class AndroidProject extends NSProject { - - constructor(projectPath: string, tnsOutputFilePath?: string) { - super(projectPath, tnsOutputFilePath); - } - - public platform(): string { - return 'android'; - } - - public run(): Promise { - // build command to execute - let command = new CommandBuilder() - .appendParam("run") - .appendParam(this.platform()) - .build(); - - let child: ChildProcess = this.spawnProcess(command.path, command.args); - return Promise.resolve(child); - } - - public debug(params: DebugProtocol.IAttachRequestArgs | DebugProtocol.ILaunchRequestArgs): Promise { - if (params.request === "attach") { - return Promise.resolve(); - } - else if (params.request === "launch") { - let args: DebugProtocol.ILaunchRequestArgs = params as DebugProtocol.ILaunchRequestArgs; - let that = this; - let launched = false; - - return new Promise((resolve, reject) => { - let command = new CommandBuilder(args.nativescriptCliPath) - .appendParam("debug") - .appendParam(this.platform()) - .appendParamIf("--emulator", args.emulator) - .appendParamIf("--no-rebuild", args.rebuild !== true) - .appendParam("--debug-brk") - .appendParam("--no-client") - .appendParams(args.tnsArgs) - .build(); - - Logger.log("tns debug command: " + command); - - // run NativeScript CLI command - let child: ChildProcess = this.spawnProcess(command.path, command.args, args.tnsOutput); - child.stdout.on('data', function(data) { - let strData: string = data.toString(); - that.emit('TNS.outputMessage', data.toString(), 'log'); - that.writeToTnsOutputFile(strData); - if (!launched) { - if (args.request === "launch" && strData.indexOf('# NativeScript Debugger started #') > -1) { - launched = true; - //wait a little before trying to connect, this gives a changes for adb to be able to connect to the debug socket - setTimeout(() => { - resolve(); - }, 500); - } - } - }); - - child.stderr.on('data', function(data) { - that.emit('TNS.outputMessage', data.toString(), 'error'); - that.writeToTnsOutputFile(data.toString()); - }); - - child.on('close', function(code) { - if (!args.rebuild) { - setTimeout(() => { - reject("The debug process exited unexpectedly code:" + code); - }, 3000); - } - else { - reject("The debug process exited unexpectedly code:" + code); - } - }); - }); - } - } - - public getDebugPort(args: DebugProtocol.IAttachRequestArgs | DebugProtocol.ILaunchRequestArgs): Promise { - //TODO: Call CLI to get the debug port - //return Promise.resolve(40001); - - //return Promise.resolve(40001); - - let command = new CommandBuilder(args.nativescriptCliPath) - .appendParam("debug") - .appendParam(this.platform()) - .appendParam("--get-port") - .appendParams(args.tnsArgs) - .build(); - let that = this; - // run NativeScript CLI command - return new Promise((resolve, reject) => { - let child: ChildProcess = this.spawnProcess(command.path, command.args, args.tnsOutput); - - child.stdout.on('data', function(data) { - that.emit('TNS.outputMessage', data.toString(), 'log'); - that.writeToTnsOutputFile(data.toString()); - - let regexp = new RegExp("(?:debug port: )([\\d]{5})"); - - //for the new output - // var input = "device: 030b258308e6ce89 debug port: 40001"; - - let portNumberMatch = null; - let match = data.toString().match(regexp); - if (match) - { - portNumberMatch = match[1]; - } - - if (portNumberMatch) { - Logger.log("port number match '" + portNumberMatch + "'"); - let portNumber = parseInt(portNumberMatch); - if (portNumber) { - Logger.log("port number " + portNumber); - child.stdout.removeAllListeners('data'); - resolve(portNumber); - } - } - }); - - child.stderr.on('data', function(data) { - that.emit('TNS.outputMessage', data.toString(), 'error'); - that.writeToTnsOutputFile(data.toString()); - }); - - child.on('close', function(code) { - reject("Getting debug port failed with code: " + code); - }); - }); - } -} - -class CommandBuilder { - - private _tnsPath: string; - private _command: string[] = []; - - constructor(tnsPath?: string) { - this._tnsPath = tnsPath || "tns"; - } - - public appendParam(parameter: string): CommandBuilder { - this._command.push(parameter); - return this; - } - - public appendParams(parameters: string[] = []): CommandBuilder { - parameters.forEach(param => this.appendParam(param)); - return this; - } - - public appendParamIf(parameter: string, condtion: boolean): CommandBuilder { - if (condtion) { - this._command.push(parameter); - } - return this; - } - - public build(): { path: string, args: string[] } { - return { path: this._tnsPath, args: this._command }; - } - - public buildAsString(): string { - let result = this.build(); - return `${result.path} ` + result.args.join(' '); - } -} diff --git a/src/services/debugAdapterServices.ts b/src/services/debugAdapterServices.ts new file mode 100644 index 0000000..0ba5927 --- /dev/null +++ b/src/services/debugAdapterServices.ts @@ -0,0 +1,20 @@ +import {Services} from './services'; +import {ExtensionClient} from '../ipc/extensionClient'; +import {Logger} from '../common/Logger'; + +export class DebugAdapterServices extends Services { + private static _extensionClient: ExtensionClient; + private static _appRoot: string; + + public static get appRoot(): string { return this._appRoot; } + + public static set appRoot(appRoot: string) { this._appRoot = appRoot; } + + public static get 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; + } +} \ No newline at end of file diff --git a/src/services/extensionHostServices.ts b/src/services/extensionHostServices.ts new file mode 100644 index 0000000..146d0db --- /dev/null +++ b/src/services/extensionHostServices.ts @@ -0,0 +1,35 @@ +import * as vscode from 'vscode'; +import {Services} from './services'; +import {ExtensionVersionService} from '../common/extensionVersionService'; +import {ExtensionServer} from '../ipc/extensionServer'; +import {AnalyticsService} from '../analytics/analyticsService'; + +export class ExtensionHostServices extends Services { + private static _globalState: vscode.Memento; + + private static _extensionVersionService: ExtensionVersionService; + private static _extensionServer: ExtensionServer; + private static _analyticsService: AnalyticsService; + + public static get globalState(): vscode.Memento { return this._globalState; } + + public static set globalState(globalState: vscode.Memento) { this._globalState = globalState; } + + public static get extensionVersionService(): ExtensionVersionService { + if (!this._extensionVersionService && !this._globalState) { + throw new Error("Global state has no value."); + } + this._extensionVersionService = this._extensionVersionService || new ExtensionVersionService(this.globalState); + return this._extensionVersionService; + } + + public static get extensionServer(): ExtensionServer { + this._extensionServer = this._extensionServer || new ExtensionServer(); + return this._extensionServer; + } + + public static get analyticsService(): AnalyticsService { + this._analyticsService = this._analyticsService || new AnalyticsService(); + return this._analyticsService; + } +} \ No newline at end of file diff --git a/src/services/services.ts b/src/services/services.ts new file mode 100644 index 0000000..d1701c3 --- /dev/null +++ b/src/services/services.ts @@ -0,0 +1,23 @@ +import {Logger} from '../common/logger'; +import {NativeScriptCli} from '../project/nativeScriptCli'; + +export class Services { + protected static _cliPath: string = "tns"; + + protected static _logger: Logger; + protected static _cli: NativeScriptCli; + + public static get cliPath(): string { return this._cliPath; } + + public static set cliPath(cliPath: string) { this._cliPath = cliPath; } + + public static get logger(): Logger { + this._logger = this._logger || new Logger(); + return this._logger; + } + + public static get cli(): NativeScriptCli { + this._cli = this._cli || new NativeScriptCli(this._cliPath); + return this._cli; + } +} diff --git a/src/tsconfig.json b/src/tsconfig.json index 8eb0dbf..c139045 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -20,6 +20,6 @@ "./debug-adapter/webKitDebug.ts", "./main.ts", "./tests/adapter.test.ts", - "./services/analytics/EqatecMonitor.min.js" + "./analytics/EqatecMonitor.min.js" ] } \ No newline at end of file