diff --git a/.github/ISSUE_TEMPLATE/do-not-file-issues-here-.md b/.github/ISSUE_TEMPLATE/do-not-file-issues-here-.md new file mode 100644 index 00000000..3eb8e95a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/do-not-file-issues-here-.md @@ -0,0 +1,29 @@ +--- +name: Do not file issues here! +about: VS Code has a new debugger which can be found in the vscode-js-debug repo +title: '' +labels: '' +assignees: '' + +--- + +--- +name: Do not file issues here! +about: VS Code has a new debugger which can be found in the vscode-js-debug repo +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.gitignore b/.gitignore index c4280ce1..fb4aabc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ .DS_Store -node_modules/ -out/ -plugin/ -upload/ +package-lock.json +/dist/ +/node_modules/ +/out/ +/plugin/ +/upload/ +npm-debug.log +package.nls.*.json +node-debug-*.vsix \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..04c9c4e6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: node_js +node_js: + - "10.2.0" +sudo: false +before_install: + - nvm install 7.9.0 + - nvm install 10.2.0 + - curl -o- -L https://yarnpkg.com/install.sh | bash + - export PATH="$HOME/.yarn/bin:$PATH" diff --git a/.vscode/launch.json b/.vscode/launch.json index deda9eea..4a841406 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,28 +2,64 @@ "version": "0.1.0", "configurations": [ { - "name": "node-debug extension", "type": "extensionHost", "request": "launch", + "name": "Extension", "runtimeExecutable": "${execPath}", "args": [ - "--extensionDevelopmentPath=${workspaceRoot}" + "--extensionDevelopmentPath=${workspaceFolder}" ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "out" + "outFiles": [ "${workspaceFolder}/out/**/*.js" ] }, { - "name": "node-debug server", "type": "node", "request": "launch", - "runtimeExecutable": "/usr/local/bin/iojs", - //"runtimeArgs": ["--harmony"], - "program": "./node/nodeDebug.ts", - "stopOnEntry": false, + "name": "Server", + "program": "${workspaceFolder}/src/node/nodeDebug.ts", "args": [ "--server=4711" ], - "sourceMaps": true, - "outDir": "out" + "outFiles": [ "${workspaceFolder}/out/**/*.js" ], + "internalConsoleOptions": "openOnSessionStart" + }, + { + "type": "node", + "request": "launch", + "name": "Server (nodemon)", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run-script", "nodemon" + ], + "restart": true, + "port": 5858, + "outFiles": [ "${workspaceFolder}/out/**/*.js" ], + "console": "integratedTerminal" + }, + { + "type": "node", + "request": "launch", + "name": "Tests", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": [ + "-u", "tdd", + "--timeout", "999999", + "--colors", + "./out/tests" + ], + "env": { + "LEGACY_NODE_PATH": "${env:NVM_DIR}/versions/node/v7.9.0/bin:${env:PATH}" + }, + "windows": { + "env": { + "LEGACY_NODE_PATH": "${env:NVM_HOME}\\v7.9.0;${env:Path}" + } + }, + "outFiles": [ "${workspaceFolder}/out/**/*.js" ], + "internalConsoleOptions": "openOnSessionStart" + } + ], + "compounds": [ + { + "name": "Extension + Server", + "configurations": [ "Extension", "Server" ] } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 014bb0bb..99dcd966 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,13 @@ // Place your settings in this file to overwrite default and user settings. { - "explorer.workingFiles.maxVisible": 30 + "typescript.tsdk": "node_modules/typescript/lib", + "editor.insertSpaces": false, + "files.eol": "\n", + "files.trimTrailingWhitespace": true, + + "search.exclude": { + "**/node_modules": true, + "out*/**": true, + "i18n/**": true + } } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 48d6f18b..4053962f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,25 +1,25 @@ { - "version": "0.1.0", - "command": "gulp", - "isShellCommand": true, - "tasks": [ - { - "taskName": "ts-watch", - "args": [], - "isBuildCommand": true, - "isWatching": true, - "problemMatcher": { - "owner": "typescript", - "fileLocation": ["absolute"], - "pattern": { - "regexp": "^\\*\\*\\* Error: ([^(]+)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\): (.*)$", - "file": 1, - "location": 2, - "message": 3 - }, - "watchedTaskBeginsRegExp": "^\\*\\*\\* Starting\\.\\.\\.$", - "watchedTaskEndsRegExp": "^\\*\\*\\* Finished with \\d+ errors\\.$" + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "problemMatcher": [ + "$gulp-tsc" + ], + "isBackground": false, + "group": { + "kind": "build", + "isDefault": true } - } - ] -} \ No newline at end of file + }, + { + "type": "npm", + "script": "watch", + "problemMatcher": [ + "$gulp-tsc" + ], + "isBackground": true + } + ] +} diff --git a/.vscodeignore b/.vscodeignore index 1d99aba7..f99a3322 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,6 +1,13 @@ -* -*/** -**/.DS_Store -!out/** -!package.json -!node_modules/source-map/** +node_modules +out +src/**/* +testdata/**/* +.gitignore +.travis.yml +.vscode/**/* +appveyor.yml +gulpfile.js +package-lock.json +CODE_OF_CONDUCT.md +README.md +webpack.config.js \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..65c8a42b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1 @@ +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/LICENSE.txt b/LICENSE.txt index 3423e958..092af5c4 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,10 @@ -The MIT License (MIT) +VS Code - Node Debug -Copyright (c) 2015 Microsoft +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4e14162f..5e604091 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ -# vscode-node-debug -A VS Code debug adapter for node \ No newline at end of file +# Node Debug (legacy) + +[![build status](https://travis-ci.org/Microsoft/vscode-node-debug.svg?branch=master)](https://travis-ci.org/Microsoft/vscode-node-debug) +[![build status](https://ci.appveyor.com/api/projects/status/t74psolxi3k7bcjp/branch/master?svg=true)](https://ci.appveyor.com/project/weinand/vscode-node-debug) + +This extension is bundled with Visual Studio Code and together with **Node Debug** forms the [Node.js](https://nodejs.org) debugging experience. + +**Node debug (legacy)** is the debugger for Node.js versions < 8.0. + +See a general overview of debugging in VS Code [here](https://code.visualstudio.com/docs/editor/debugging). + +Documentation for Node.js specific debugging can be found [here](https://code.visualstudio.com/docs/nodejs/nodejs-debugging). + +Please submit bugs and feature requests to the [VS Code repository](https://github.com/microsoft/vscode/issues). + + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the [MIT](LICENSE.txt) License. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..a050f362 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..22e78f95 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,8 @@ +install: + - ps: Install-Product node 7.9.0 x64 + +build_script: + - npm install + +test_script: + - npm test diff --git a/build/pipeline.yml b/build/pipeline.yml new file mode 100644 index 00000000..b84567cd --- /dev/null +++ b/build/pipeline.yml @@ -0,0 +1,24 @@ +name: $(Date:yyyyMMdd)$(Rev:.r) + +trigger: + branches: + include: + - main + tags: + include: ["*"] +pr: none + +resources: + repositories: + - repository: templates + type: github + name: microsoft/vscode-engineering + ref: main + endpoint: Monaco + +extends: + template: azure-pipelines/extension/stable.yml@templates + parameters: + buildSteps: + - script: yarn install --frozen-lockfile + displayName: Install dependencies diff --git a/common/debugProtocol.d.ts b/common/debugProtocol.d.ts deleted file mode 100644 index 9321515c..00000000 --- a/common/debugProtocol.d.ts +++ /dev/null @@ -1,497 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** Declaration module describing the VS Code debug protocol - */ -declare module DebugProtocol { - - //---- V8 inspired protocol - - /** Base class of V8 requests, responses, and events. */ - export interface V8Message { - /** Sequence number */ - seq: number; - /** One of "request", "response", or "event" */ - type: string; - } - - /** Client-initiated request */ - export interface Request extends V8Message { - /** The command to execute */ - command: string; - /** Object containing arguments for the command */ - arguments?: any; - } - - /** Server-initiated event */ - export interface Event extends V8Message { - /** Type of event */ - event: string; - /** Event-specific information */ - body?: any; - } - - /** Server-initiated response to client request */ - export interface Response extends V8Message { - /** Sequence number of the corresponding request */ - request_seq: number; - /** Outcome of the request */ - success: boolean; - /** The command requested */ - command: string; - /** Contains error message if success == false. */ - message?: string; - /** Contains request result if success is true and optional error details if success is false. */ - body?: any; - } - - //---- Events - - /** Event message for "initialized" event type. - The event indicates that the debugee is ready to accept SetBreakpoint calls. - */ - export interface InitializedEvent extends Event { - } - - /** Event message for "stopped" event type. - The event indicates that the execution of the debugee has stopped due to a break condition. - This can be caused by by a break point previously set, a stepping action has completed or by executing a debugger statement. - */ - export interface StoppedEvent extends Event { - body: { - /** The reason for the event (such as: 'step', 'breakpoint', 'exception', 'pause') */ - reason: string; - /** The thread which was stopped. */ - threadId?: number; - /** Additonal information. E.g. if reason is 'exception', text contains the exception name. */ - text?: string; - }; - } - - /** Event message for "exited" event type. - The event indicates that the debugee has exited. - */ - export interface ExitedEvent extends Event { - body: { - /** The exit code returned from the debuggee. */ - exitCode: number; - }; - } - - /** Event message for "terminated" event types. - The event indicates that debugging of the debuggee has terminated. - */ - export interface TerminatedEvent extends Event { - } - - /** Event message for "thread" event type. - The event indicates that a thread has started or exited. - */ - export interface ThreadEvent extends Event { - body: { - /** The reason for the event (such as: 'started', 'exited'). */ - reason: string; - /** The identifier of the thread. */ - threadId: number; - }; - } - - /** Event message for "output" event type. - The event indicates that the target has produced output. - */ - export interface OutputEvent extends Event { - body: { - /** The category of output (such as: 'console', 'stdout', 'stderr'). If not specified, 'console' is assumed. */ - category?: string; - /** The output */ - output: string; - }; - } - - //---- Requests - - /** On error that is whenever 'success' is false, the body can provide more details. - */ - export interface ErrorResponse extends Response { - body: { - /** An optional, structured error message. */ - error?: Message - } - } - - /** Initialize request; value of command field is "initialize". - */ - export interface InitializeRequest extends Request { - arguments: InitializeRequestArguments; - } - /** Arguments for "initialize" request. */ - export interface InitializeRequestArguments { - /** The ID of the debugger adapter. Used to select or verify debugger adapter. */ - adapterID: string; - /** If true all line numbers are 1-based (default). */ - linesStartAt1?: boolean; - /** If true all column numbers are 1-based (default). */ - columnsStartAt1?: boolean; - /** Determines in what format paths are specified. Possible values are 'path' or 'uri'. The default is 'path', which is the native format. */ - pathFormat?: string; - } - /** Response to Initialize request. */ - export interface InitializeResponse extends Response { - } - - /** Launch request; value of command field is "launch". - */ - export interface LaunchRequest extends Request { - arguments: LaunchRequestArguments; - } - /** Arguments for "launch" request. */ - export interface LaunchRequestArguments { - /* The launch request has no standardized attributes. */ - } - /** Response to "launch" request. This is just an acknowledgement, so no body field is required. */ - export interface LaunchResponse extends Response { - } - - /** Attach request; value of command field is "attach". - */ - export interface AttachRequest extends Request { - arguments: AttachRequestArguments; - } - /** Arguments for "attach" request. */ - export interface AttachRequestArguments { - /* The attach request has no standardized attributes. */ - } - /** Response to "attach" request. This is just an acknowledgement, so no body field is required. */ - export interface AttachResponse extends Response { - } - - /** Disconnect request; value of command field is "disconnect". - */ - export interface DisconnectRequest extends Request { - arguments?: DisconnectArguments; - } - /** Arguments for "disconnect" request. */ - export interface DisconnectArguments { - } - /** Response to "disconnect" request. This is just an acknowledgement, so no body field is required. */ - export interface DisconnectResponse extends Response { - } - - /** SetBreakpoints request; value of command field is "setBreakpoints". - Sets multiple breakpoints for a single source and clears all previous breakpoints in that source. - To clear all breakpoint for a source, specify an empty array. - When a breakpoint is hit, a StoppedEvent (event type 'breakpoint') is generated. - */ - export interface SetBreakpointsRequest extends Request { - arguments: SetBreakpointsArguments; - } - /** Arguments for "setBreakpoints" request. */ - export interface SetBreakpointsArguments { - /** The source location of the breakpoints; either source.path or source.reference must be specified. */ - source: Source; - /** The code locations of the breakpoints */ - lines: number[]; - } - /** Response to "setBreakpoints" request. - Returned is information about each breakpoint created by this request. - This includes the actual code location and whether the breakpoint could be verified. - */ - export interface SetBreakpointsResponse extends Response { - body: { - /** Information about the breakpoints. The array elements correspond to the elements of the 'lines' array. */ - breakpoints: Breakpoint[]; - }; - } - - /** SetExceptionBreakpoints request; value of command field is "setExceptionBreakpoints". - Enable that the debuggee stops on exceptions with a StoppedEvent (event type 'exception'). - */ - export interface SetExceptionBreakpointsRequest extends Request { - arguments: SetExceptionBreakpointsArguments; - } - /** Arguments for "setExceptionBreakpoints" request. */ - export interface SetExceptionBreakpointsArguments { - /** Names of enabled exception breakpoints. */ - filters: string[]; - } - /** Response to "setExceptionBreakpoints" request. This is just an acknowledgement, so no body field is required. */ - export interface SetExceptionBreakpointsResponse extends Response { - } - - /** Continue request; value of command field is "continue". - The request starts the debuggee to run again. - */ - export interface ContinueRequest extends Request { - arguments: ContinueArguments; - } - /** Arguments for "continue" request. */ - export interface ContinueArguments { - /** continue execution for this thread. */ - threadId: number; - } - /** Response to "continue" request. This is just an acknowledgement, so no body field is required. */ - export interface ContinueResponse extends Response { - } - - /** Next request; value of command field is "next". - The request starts the debuggee to run again for one step. - penDebug will respond with a StoppedEvent (event type 'step') after running the step. - */ - export interface NextRequest extends Request { - arguments: NextArguments; - } - /** Arguments for "next" request. */ - export interface NextArguments { - /** Continue execution for this thread. */ - threadId: number; - } - /** Response to "next" request. This is just an acknowledgement, so no body field is required. */ - export interface NextResponse extends Response { - } - - /** StepIn request; value of command field is "stepIn". - The request starts the debuggee to run again for one step. - The debug adapter will respond with a StoppedEvent (event type 'step') after running the step. - */ - export interface StepInRequest extends Request { - arguments: StepInArguments; - } - /** Arguments for "stepIn" request. */ - export interface StepInArguments { - /** Continue execution for this thread. */ - threadId: number; - } - /** Response to "stepIn" request. This is just an acknowledgement, so no body field is required. */ - export interface StepInResponse extends Response { - } - - /** StepOutIn request; value of command field is "stepOut". - The request starts the debuggee to run again for one step. - penDebug will respond with a StoppedEvent (event type 'step') after running the step. - */ - export interface StepOutRequest extends Request { - arguments: StepOutArguments; - } - /** Arguments for "stepOut" request. */ - export interface StepOutArguments { - /** Continue execution for this thread. */ - threadId: number; - } - /** Response to "stepOut" request. This is just an acknowledgement, so no body field is required. */ - export interface StepOutResponse extends Response { - } - - /** Pause request; value of command field is "pause". - The request suspenses the debuggee. - penDebug will respond with a StoppedEvent (event type 'pause') after a successful 'pause' command. - */ - export interface PauseRequest extends Request { - arguments: PauseArguments; - } - /** Arguments for "pause" request. */ - export interface PauseArguments { - /** Pause execution for this thread. */ - threadId: number; - } - /** Response to "pause" request. This is just an acknowledgement, so no body field is required. */ - export interface PauseResponse extends Response { - } - - /** StackTrace request; value of command field is "stackTrace". - The request returns a stacktrace from the current execution state. - */ - export interface StackTraceRequest extends Request { - arguments: StackTraceArguments; - } - /** Arguments for "stackTrace" request. */ - export interface StackTraceArguments { - /** Retrieve the stacktrace for this thread. */ - threadId: number; - /** The maximum number of frames to return. If levels is not specified or 0, all frames are returned. */ - levels?: number; - } - /** Response to "stackTrace" request. */ - export interface StackTraceResponse extends Response { - body: { - /** The frames of the stackframe. If the array has length zero, there are no stackframes available. - This means that there is no location information available. */ - stackFrames: StackFrame[]; - }; - } - - /** Scopes request; value of command field is "scopes". - The request returns the variable scopes for a given stackframe ID. - */ - export interface ScopesRequest extends Request { - arguments: ScopesArguments; - } - /** Arguments for "scopes" request. */ - export interface ScopesArguments { - /** Retrieve the scopes for this stackframe. */ - frameId: number; - } - /** Response to "scopes" request. */ - export interface ScopesResponse extends Response { - body: { - /** The scopes of the stackframe. If the array has length zero, there are no scopes available. */ - scopes: Scope[]; - }; - } - - /** Variables request; value of command field is "variables". - Retrieves all children for the given variable reference. - */ - export interface VariablesRequest extends Request { - arguments: VariablesArguments; - } - /** Arguments for "variables" request. */ - export interface VariablesArguments { - /** The Variable reference. */ - variablesReference: number; - } - /** Response to "variables" request. */ - export interface VariablesResponse extends Response { - body: { - /** All children for the given variable reference */ - variables: Variable[]; - }; - } - - /** Source request; value of command field is "source". - The request retrieves the source code for a given source reference. - */ - export interface SourceRequest extends Request { - arguments: SourceArguments; - } - /** Arguments for "source" request. */ - export interface SourceArguments { - /** The reference to the source. This is the value received in Source.reference. */ - sourceReference: number; - } - /** Response to "source" request. */ - export interface SourceResponse extends Response { - body: { - /** Content of the source reference */ - content: string; - }; - } - - /** Thread request; value of command field is "threads". - The request retrieves a list of all threads. - */ - export interface ThreadsRequest extends Request { - } - /** Response to "threads" request. */ - export interface ThreadsResponse extends Response { - body: { - /** All threads. */ - threads: Thread[]; - }; - } - - /** Evaluate request; value of command field is "evaluate". - Evaluates the given expression in the context of the top most stack frame. - The expression has access to any variables and arguments that are in scope. - */ - export interface EvaluateRequest extends Request { - arguments: EvaluateArguments; - } - /** Arguments for "evaluate" request. */ - export interface EvaluateArguments { - /** The expression to evaluate */ - expression: string; - /** Evaluate the expression in the context of this stack frame. If not specified, the expression is evaluated in the global context. */ - frameId?: number; - } - /** Response to "evaluate" request. */ - export interface EvaluateResponse extends Response { - body: { - /** The result of the evaluate. */ - result: string; - /** If variablesReference is > 0, the evaluate result is structured and its children can be retrieved by passing variablesReference to the VariablesRequest */ - variablesReference: number; - }; - } - - //---- Types - - /** A structured message object. Used to return errors from requests. */ - export interface Message { - /** Unique identifier for the message. */ - id: number; - /** A format string for the message. Embedded variables have the form '{name}'. - If variable name starts with an underscore character, the variable does not contain user data (PII) and can be safely used for telemetry purposes. */ - format: string; - /** An object used as a dictionary for looking up the variables in the format string. */ - variables?: { [key: string]: string }; - /** if true send to telemetry (Experimental) */ - sendTelemetry?: boolean; - /** if true show user (Experimental) */ - showUser?: boolean; - } - - /** A Thread */ - export interface Thread { - /** Unique identifier for the thread. */ - id: number; - /** A name of the thread. */ - name: string; - } - - /** A Source .*/ - export interface Source { - /** The short name of the source. Every source returned from the debug adapter has a name. When specifying a source to the debug adapter this name is optional. */ - name?: string; - /** The long (absolute) path of the source. It is not guaranteed that the source exists at this location. */ - path?: string; - /** If sourceReference > 0 the contents of the source can be retrieved through the SourceRequest. A sourceReference is only valid for a session, so it must not be used to persist a source. */ - sourceReference?: number; - } - - /** A Stackframe contains the source location. */ - export interface StackFrame { - /** An identifier for the stack frame. This id can be used to retrieve the scopes of the frame with the 'scopesRequest'. */ - id: number; - /** The name of the stack frame, typically a method name */ - name: string; - /** The optional source of the frame. */ - source?: Source; - /** The line within the file of the frame. If source is null or doesn't exist, line is 0 and must be ignored. */ - line: number; - /** The column within the line. If source is null or doesn't exist, column is 0 and must be ignored. */ - column: number; - } - - /** A Scope is a named container for variables. */ - export interface Scope { - /** name of the scope (as such 'Arguments', 'Locals') */ - name: string; - /** The variables of this scope can be retrieved by passing the value of variablesReference to the VariablesRequest. */ - variablesReference: number; - /** If true, the number of variables in this scope is large or expensive to retrieve. */ - expensive: boolean; - } - - /** A Variable is a name/value pair. - If the value is structured (has children), a handle is provided to retrieve the children with the VariablesRequest. - */ - export interface Variable { - /** The variable's name */ - name: string; - /** The variable's value. For structured objects this can be a multi line text, e.g. for a function the body of a function. */ - value: string; - /** If variablesReference is > 0, the variable is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. */ - variablesReference: number; - } - - /** Information about a Breakpoint created in the setBreakpoints request. - */ - export interface Breakpoint { - /** If true breakpoint could be set (but not necessarily at the correct location). */ - verified: boolean; - /** The actual location of the breakpoint. */ - line: number; - } -} diff --git a/common/debugSession.ts b/common/debugSession.ts deleted file mode 100644 index 979d947e..00000000 --- a/common/debugSession.ts +++ /dev/null @@ -1,474 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import {V8Protocol, Response, Event} from './v8Protocol'; -import * as Net from 'net'; -import * as Path from 'path'; -import * as Url from 'url'; - - -export class Source implements DebugProtocol.Source { - name: string; - path: string; - sourceReference: number; - - public constructor(name: string, path: string, id: number = 0) { - this.name = name; - this.path = path; - this.sourceReference = id; - } -} - -export class Scope implements DebugProtocol.Scope { - name: string; - variablesReference: number; - expensive: boolean; - - public constructor(name: string, reference: number, expensive: boolean = false) { - this.name = name; - this.variablesReference = reference; - this.expensive = expensive; - } -} - -export class StackFrame implements DebugProtocol.StackFrame { - id: number; - source: Source; - line: number; - column: number; - name: string; - - public constructor(i: number, nm: string, src: Source, ln: number, col: number) { - this.id = i; - this.source = src; - this.line = ln; - this.column = col; - this.name = nm; - } -} - -export class Thread implements DebugProtocol.Thread { - id: number; - name: string; - - public constructor(id: number, name: string) { - this.id = id; - if (name) { - this.name = name; - } else { - this.name = "Thread #" + id; - } - } -} - -export class Variable implements DebugProtocol.Variable { - name: string; - value: string; - variablesReference: number; - - public constructor(name: string, value: string, ref: number = 0) { - this.name = name; - this.value = value; - this.variablesReference = ref; - } -} - -export class Breakpoint implements DebugProtocol.Breakpoint { - verified: boolean; - line: number; - - public constructor(verified: boolean, line: number) { - this.verified = verified; - this.line = line; - } -} - -export class StoppedEvent extends Event implements DebugProtocol.StoppedEvent { - body: { - reason: string; - threadId: number; - }; - - public constructor(reason: string, threadId: number, exception_text: string = null) { - super('stopped'); - this.body = { - reason: reason, - threadId: threadId - }; - - if (exception_text) { - (this).body.text = exception_text; - } - } -} - -export class InitializedEvent extends Event implements DebugProtocol.InitializedEvent { - public constructor() { - super('initialized'); - } -} - -export class TerminatedEvent extends Event implements DebugProtocol.TerminatedEvent { - public constructor() { - super('terminated'); - } -} - -export class OutputEvent extends Event implements DebugProtocol.OutputEvent { - body: { - category: string, - output: string - }; - - public constructor(output: string, category: string = 'console') { - super('output'); - this.body = { - category: category, - output: output - }; - } -} - -export enum ErrorDestination { - User = 1, - Telemetry = 2 -}; - -export class DebugSession extends V8Protocol { - - private _debuggerLinesStartAt1: boolean; - private _debuggerColumnsStartAt1: boolean; - private _debuggerPathsAreURIs: boolean; - - private _clientLinesStartAt1: boolean; - private _clientColumnsStartAt1: boolean; - private _clientPathsAreURIs: boolean; - - private _isServer: boolean; - - public constructor(debuggerLinesAndColumnsStartAt1: boolean, isServer: boolean = false) { - super(); - - this._debuggerLinesStartAt1 = debuggerLinesAndColumnsStartAt1; - this._debuggerColumnsStartAt1 = debuggerLinesAndColumnsStartAt1; - this._debuggerPathsAreURIs = false; - - this._clientLinesStartAt1 = true; - this._clientColumnsStartAt1 = true; - this._clientPathsAreURIs = false; - - this._isServer = isServer; - - this.on('close', () => { - this.shutdown(); - }); - this.on('error', (error) => { - this.shutdown(); - }); - } - - /** - * A virtual constructor... - */ - public static run(debugSession: typeof DebugSession) { - - // parse arguments - let port = 0; - const args = process.argv.slice(2); - args.forEach(function (val, index, array) { - const portMatch = /^--server=(\d{4,5})$/.exec(val); - if (portMatch) { - port = parseInt(portMatch[1], 10); - } - }); - - if (port > 0) { - // start as a server - console.error(`waiting for v8 protocol on port ${port}`); - Net.createServer((socket) => { - console.error('>> accepted connection from client'); - socket.on('end', () => { - console.error('>> client connection closed\n'); - }); - new debugSession(false, true).start(socket, socket); - }).listen(port); - } else { - - // start a session - console.error("waiting for v8 protocol on stdin/stdout"); - let session = new debugSession(false); - process.on('SIGTERM', () => { - session.shutdown(); - }); - session.start(process.stdin, process.stdout); - } - } - - public shutdown(): void { - if (this._isServer) { - console.error('process.exit ignored in server mode'); - } else { - // wait a bit before shutting down - setTimeout(() => { - process.exit(0); - }, 100); - } - } - - protected sendErrorResponse(response: DebugProtocol.Response, code: number, format: string, args?: any, dest: ErrorDestination = ErrorDestination.User): void { - - const message = DebugSession.formatPII(format, true, args); - - response.success = false; - response.message = `${response.command}: ${message}`; - if (!response.body) { - response.body = {}; - } - const msg = { - id: code, - format: format - }; - if (args) { - msg.variables = args; - } - if (dest & ErrorDestination.User) { - msg.showUser = true; - } - if (dest & ErrorDestination.Telemetry) { - msg.sendTelemetry = true; - } - response.body.error = msg; - - this.sendResponse(response); - } - - protected dispatchRequest(request: DebugProtocol.Request): void { - - const response = new Response(request); - - try { - if (request.command === 'initialize') { - var args = request.arguments; - - if (typeof args.linesStartAt1 === 'boolean') { - this._clientLinesStartAt1 = args.linesStartAt1; - } - if (typeof args.columnsStartAt1 === 'boolean') { - this._clientColumnsStartAt1 = args.columnsStartAt1; - } - - if (args.pathFormat !== 'path') { - this.sendErrorResponse(response, 2018, "debug adapter only supports native paths", null, ErrorDestination.Telemetry); - } else { - this.initializeRequest( response, args); - } - - } else if (request.command === 'launch') { - this.launchRequest( response, request.arguments); - - } else if (request.command === 'attach') { - this.attachRequest( response, request.arguments); - - } else if (request.command === 'disconnect') { - this.disconnectRequest( response, request.arguments); - - } else if (request.command === 'setBreakpoints') { - this.setBreakPointsRequest( response, request.arguments); - - } else if (request.command === 'setExceptionBreakpoints') { - this.setExceptionBreakPointsRequest( response, request.arguments); - - } else if (request.command === 'continue') { - this.continueRequest( response, request.arguments); - - } else if (request.command === 'next') { - this.nextRequest( response, request.arguments); - - } else if (request.command === 'stepIn') { - this.stepInRequest( response, request.arguments); - - } else if (request.command === 'stepOut') { - this.stepOutRequest( response, request.arguments); - - } else if (request.command === 'pause') { - this.pauseRequest( response, request.arguments); - - } else if (request.command === 'stackTrace') { - this.stackTraceRequest( response, request.arguments); - - } else if (request.command === 'scopes') { - this.scopesRequest( response, request.arguments); - - } else if (request.command === 'variables') { - this.variablesRequest( response, request.arguments); - - } else if (request.command === 'source') { - this.sourceRequest( response, request.arguments); - - } else if (request.command === 'threads') { - this.threadsRequest( response); - - } else if (request.command === 'evaluate') { - this.evaluateRequest( response, request.arguments); - - } else { - this.sendErrorResponse(response, 1014, "unrecognized request", null, ErrorDestination.Telemetry); - } - } catch (e) { - this.sendErrorResponse(response, 1104, "exception while processing request (exception: {_exception})", { _exception: e.message }, ErrorDestination.Telemetry); - } - } - - protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { - this.sendResponse(response); - } - - protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { - this.sendResponse(response); - this.shutdown(); - } - - protected launchRequest(response: DebugProtocol.LaunchResponse, args: DebugProtocol.LaunchRequestArguments): void { - this.sendResponse(response); - } - - protected attachRequest(response: DebugProtocol.AttachResponse, args: DebugProtocol.AttachRequestArguments): void { - this.sendResponse(response); - } - - protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { - this.sendResponse(response); - } - - protected setExceptionBreakPointsRequest(response: DebugProtocol.SetExceptionBreakpointsResponse, args: DebugProtocol.SetExceptionBreakpointsArguments): void { - this.sendResponse(response); - } - - protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) : void { - this.sendResponse(response); - } - - protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) : void { - this.sendResponse(response); - } - - protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) : void { - this.sendResponse(response); - } - - protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) : void { - this.sendResponse(response); - } - - protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) : void { - this.sendResponse(response); - } - - protected sourceRequest(response: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments) : void { - this.sendResponse(response); - } - - protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { - this.sendResponse(response); - } - - protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { - this.sendResponse(response); - } - - protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { - this.sendResponse(response); - } - - protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void { - this.sendResponse(response); - } - - protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { - this.sendResponse(response); - } - - //---- protected ------------------------------------------------------------------------------------------------- - - protected convertClientLineToDebugger(line: number): number { - if (this._debuggerLinesStartAt1) { - return this._clientLinesStartAt1 ? line : line + 1; - } - return this._clientLinesStartAt1 ? line - 1 : line; - } - - protected convertDebuggerLineToClient(line: number): number { - if (this._debuggerLinesStartAt1) { - return this._clientLinesStartAt1 ? line : line - 1; - } - return this._clientLinesStartAt1 ? line + 1 : line; - } - - protected convertClientColumnToDebugger(column: number): number { - if (this._debuggerColumnsStartAt1) { - return this._clientColumnsStartAt1 ? column : column + 1; - } - return this._clientColumnsStartAt1 ? column - 1 : column; - } - - protected convertDebuggerColumnToClient(column: number): number { - if (this._debuggerColumnsStartAt1) { - return this._clientColumnsStartAt1 ? column : column + 1; - } - return this._clientColumnsStartAt1 ? column - 1 : column; - } - - protected convertClientPathToDebugger(clientPath: string): string { - if (this._clientPathsAreURIs != this._debuggerPathsAreURIs) { - if (this._clientPathsAreURIs) { - return DebugSession.uri2path(clientPath); - } else { - return DebugSession.path2uri(clientPath); - } - } - return clientPath; - } - - protected convertDebuggerPathToClient(debuggerPath: string): string { - if (this._debuggerPathsAreURIs != this._clientPathsAreURIs) { - if (this._debuggerPathsAreURIs) { - return DebugSession.uri2path(debuggerPath); - } else { - return DebugSession.path2uri(debuggerPath); - } - } - return debuggerPath; - } - - //---- private ------------------------------------------------------------------------------- - - private static path2uri(str: string): string { - var pathName = str.replace(/\\/g, '/'); - if (pathName[0] !== '/') { - pathName = '/' + pathName; - } - return encodeURI('file://' + pathName); - } - - private static uri2path(url: string): string { - return Url.parse(url).pathname; - } - - private static _formatPIIRegexp = /{([^}]+)}/g; - - /* - * If argument starts with '_' it is OK to send its value to telemetry. - */ - private static formatPII(format:string, excludePII: boolean, args: {[key: string]: string}): string { - return format.replace(DebugSession._formatPIIRegexp, function(match, paramName) { - if (excludePII && paramName.length > 0 && paramName[0] !== '_') { - return match; - } - return args[paramName] && args.hasOwnProperty(paramName) ? - args[paramName] : - match; - }) - } -} diff --git a/common/handles.ts b/common/handles.ts deleted file mode 100644 index c0fb0a49..00000000 --- a/common/handles.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export class Handles { - - private START_HANDLE = 1000; - - private _nextHandle : number; - private _handleMap = new Map(); - - public constructor() { - this._nextHandle = this.START_HANDLE; - } - - public reset(): void { - this._nextHandle = this.START_HANDLE; - this._handleMap = new Map(); - } - - public create(value: T): number { - var handle = this._nextHandle++; - this._handleMap[handle] = value; - return handle; - } - - public get(handle: number, dflt?: T): T { - return this._handleMap[handle] || dflt; - } -} diff --git a/common/v8Protocol.ts b/common/v8Protocol.ts deleted file mode 100644 index 550674e0..00000000 --- a/common/v8Protocol.ts +++ /dev/null @@ -1,240 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ee from 'events'; - -export class Message implements DebugProtocol.V8Message { - seq: number; - type: string; - - public constructor(type: string) { - this.seq = 0; - this.type = type; - } -} - -export class Response extends Message implements DebugProtocol.Response { - request_seq: number; - success: boolean; - command: string; - - public constructor(request: DebugProtocol.Request, message?: string) { - super('response'); - this.request_seq = request.seq; - this.command = request.command; - if (message) { - this.success = false; - (this).message = message; - } else { - this.success = true; - } - } -} - -export class Event extends Message implements DebugProtocol.Event { - event: string; - - public constructor(event: string, body?: any) { - super('event'); - this.event = event; - if (body) { - (this).body = body; - } - } -} - -export class V8Protocol extends ee.EventEmitter { - - private static TIMEOUT = 3000; - - private _state: string; - private _contentLength: number; - private _bodyStartByteIndex: number; - private _res: any; - private _sequence: number; - private _writableStream: NodeJS.WritableStream; - private _pendingRequests = new Map(); - - constructor() { - super(); - } - - protected start(inStream: NodeJS.ReadableStream, outStream: NodeJS.WritableStream): void { - this._sequence = 1; - this._writableStream = outStream; - this._newRes(null); - - inStream.setEncoding('utf8'); - - inStream.on('data', (data) => this._handleData(data)); - inStream.on('close', () => { - this._emitEvent(new Event('close')); - }); - inStream.on('error', (error) => { - this._emitEvent(new Event('error')); - }); - - outStream.on('error', (error) => { - this._emitEvent(new Event('error')); - }); - - inStream.resume(); - } - - public stop(): void { - if (this._writableStream) { - this._writableStream.end(); - } - } - - protected send(command: string, args: any, timeout: number = V8Protocol.TIMEOUT): Promise { - return new Promise((completeDispatch, errorDispatch) => { - this._sendRequest(command, args, timeout, (result: DebugProtocol.Response) => { - if (result.success) { - completeDispatch(result); - } else { - errorDispatch(result); - } - }); - }); - } - - public sendEvent(event: DebugProtocol.Event): void { - this._send('event', event); - } - - public sendResponse(response: DebugProtocol.Response): void { - if (response.seq > 0) { - console.error('attempt to send more than one response for command {0}', response.command); - } else { - this._send('response', response); - } - } - - // ---- protected ---------------------------------------------------------- - - protected dispatchRequest(request: DebugProtocol.Request): void { - } - - // ---- private ------------------------------------------------------------ - - private _sendRequest(command: string, args: any, timeout: number, cb: (response: DebugProtocol.Response) => void): void { - - const request: any = { - command: command - }; - if (args && Object.keys(args).length > 0) { - request.arguments = args; - } - - this._send('request', request); - - if (cb) { - this._pendingRequests[request.seq] = cb; - - const timer = setTimeout(() => { - clearTimeout(timer); - const clb = this._pendingRequests[request.seq]; - if (clb) { - delete this._pendingRequests[request.seq]; - clb(new Response(request, 'timeout after ' + timeout + 'ms')); - - this._emitEvent(new Event('diagnostic', { reason: 'unresponsive ' + command })); - } - }, timeout); - } - } - - private _emitEvent(event: DebugProtocol.Event) { - this.emit(event.event, event); - } - - private _send(typ: string, message: DebugProtocol.V8Message): void { - message.type = typ; - message.seq = this._sequence++; - const json = JSON.stringify(message); - const data = 'Content-Length: ' + Buffer.byteLength(json, 'utf8') + '\r\n\r\n' + json; - if (this._writableStream) { - this._writableStream.write(data); - } - } - - private _newRes(raw: string): void { - this._res = { - raw: raw || '', - headers: {} - }; - this._state = 'headers'; - this._handleData(''); - } - - private _handleData(d): void { - const res = this._res; - res.raw += d; - - switch (this._state) { - case 'headers': - const endHeaderIndex = res.raw.indexOf('\r\n\r\n'); - if (endHeaderIndex < 0) - break; - - const rawHeader = res.raw.slice(0, endHeaderIndex); - const endHeaderByteIndex = Buffer.byteLength(rawHeader, 'utf8'); - const lines = rawHeader.split('\r\n'); - for (let i = 0; i < lines.length; i++) { - const kv = lines[i].split(/: +/); - res.headers[kv[0]] = kv[1]; - } - - this._contentLength = +res.headers['Content-Length']; - this._bodyStartByteIndex = endHeaderByteIndex + 4; - - this._state = 'body'; - - const len = Buffer.byteLength(res.raw, 'utf8'); - if (len - this._bodyStartByteIndex < this._contentLength) { - break; - } - // pass thru - - case 'body': - const resRawByteLength = Buffer.byteLength(res.raw, 'utf8'); - if (resRawByteLength - this._bodyStartByteIndex >= this._contentLength) { - const buf = new Buffer(resRawByteLength); - buf.write(res.raw, 0, resRawByteLength, 'utf8'); - res.body = buf.slice(this._bodyStartByteIndex, this._bodyStartByteIndex + this._contentLength).toString('utf8'); - res.body = res.body.length ? JSON.parse(res.body) : {}; - this._dispatch(res.body); - this._newRes(buf.slice(this._bodyStartByteIndex + this._contentLength).toString('utf8')); - } - break; - - default: - throw new Error('Unknown state'); - break; - } - } - - private _dispatch(message: DebugProtocol.V8Message): void { - switch (message.type) { - case 'event': - this._emitEvent( message); - break; - case 'response': - const response = message; - const clb = this._pendingRequests[response.request_seq]; - if (clb) { - delete this._pendingRequests[response.request_seq]; - clb(response); - } - break; - case 'request': - this.dispatchRequest( message); - break; - default: - break; - } - } -} diff --git a/gulpfile.js b/gulpfile.js index 85ff5235..f8aad537 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -5,78 +5,143 @@ var gulp = require('gulp'); var path = require('path'); -var tsb = require('gulp-tsb'); -var log = require('gulp-util').log; -var azure = require('gulp-azure-storage'); -var git = require('git-rev-sync'); +var ts = require('gulp-typescript'); +var sourcemaps = require('gulp-sourcemaps'); +var tslint = require("gulp-tslint"); +var filter = require('gulp-filter'); +var uglify = require('gulp-uglify'); var del = require('del'); -var runSequence = require('run-sequence'); -var vzip = require('gulp-vinyl-zip'); +var typescript = require('typescript'); -var compilation = tsb.create(path.join(__dirname, 'tsconfig.json'), true); +var tsProject = ts.createProject('./src/tsconfig.json', { typescript }); +var nls = require('vscode-nls-dev'); -var sources = [ - 'common/**/*.ts', - 'node/**/*.ts', - 'typings/**/*.ts', - 'test/**/*.ts' +var inlineMap = true; +var inlineSource = false; + +var watchedSources = [ + 'src/**/*', + '!src/tests/data/**' +]; + +var scripts = [ + 'src/node/terminateProcess.sh' +]; + +var scripts2 = [ + 'src/node/debugInjection.js' ]; var outDest = 'out'; -var uploadDest = 'upload/' + git.short(); +var webPackedDest = 'dist'; -gulp.task('default', function(callback) { - runSequence('build', callback); -}); +const transifexProjectName = 'vscode-extensions'; +const transifexExtensionName = 'vscode-node-debug'; -gulp.task('build', function(callback) { - runSequence('clean', 'internal-build', callback); +gulp.task('clean', () => { + return del(['out/**', 'dist/**', 'package.nls.*.json', 'node-debug-*.vsix', 'package-lock.json']); }); -gulp.task('zip', function(callback) { - runSequence('build', 'internal-zip', callback); +gulp.task('internal-compile', () => { + return compile(); }); -gulp.task('upload', function(callback) { - runSequence('zip', 'internal-upload', callback); +gulp.task('internal-copy-scripts', () => { + return gulp.src(scripts) + .pipe(gulp.dest(outDest + '/node')); }); -gulp.task('clean', function() { - return del(['out/**', 'upload/**']); -}) - -gulp.task('ts-watch', ['internal-build'], function(cb) { - log('Watching build sources...'); - gulp.watch(sources, ['internal-compile']); +gulp.task('internal-minify-scripts', () => { + return gulp.src(scripts2) + .pipe(uglify()) + .pipe(gulp.dest(outDest + '/node')); }); -//---- internal - // compile and copy everything to outDest -gulp.task('internal-build', function(callback) { - runSequence('internal-compile', 'internal-copy-scripts', callback); +gulp.task('internal-build', gulp.series('internal-compile', 'internal-copy-scripts', 'internal-minify-scripts', done => { + done(); +})); + +gulp.task('build', gulp.series('clean', 'internal-build', done => { + done(); +})); + +gulp.task('default', gulp.series('build', done => { + done(); +})); + +gulp.task('compile', gulp.series('clean', 'internal-build', done => { + done(); +})); + +gulp.task('nls-bundle-create', () => { + var r = tsProject.src() + .pipe(sourcemaps.init()) + .pipe(tsProject()).js + .pipe(nls.createMetaDataFiles()) + .pipe(nls.bundleMetaDataFiles('ms-vscode.node-debug', webPackedDest)) + .pipe(nls.bundleLanguageFiles()) + .pipe(filter('**/nls.*.json')); + + return r.pipe(gulp.dest(webPackedDest)); }); -gulp.task('internal-copy-scripts', function() { - return gulp.src(['node/terminateProcess.sh', 'node/TerminalHelper.scpt']) - .pipe(gulp.dest(outDest + '/node')); -}); +gulp.task('prepare-for-webpack', gulp.series('clean', 'internal-minify-scripts', 'nls-bundle-create', done => { + done(); +})); -gulp.task('internal-compile', function() { - return gulp.src(sources, { base: '.' }) - .pipe(compilation()) - .pipe(gulp.dest(outDest)); -}); -gulp.task('internal-zip', function(callback) { - return gulp.src([outDest + '/**/*', 'node_modules/source-map/**/*', 'package.json', 'ThirdPartyNotices.txt', 'LICENSE.txt'], { base: '.' }).pipe(vzip.dest(uploadDest + '/node-debug.zip')); -}); +gulp.task('watch', gulp.series('internal-build', done => { + //log('Watching build sources...'); + gulp.watch(watchedSources, gulp.series('internal-build')); + done(); +})); + +gulp.task('translations-export', gulp.series('build', 'prepare-for-webpack', () => { + return gulp.src(['package.nls.json', path.join(webPackedDest, 'nls.metadata.header.json'), path.join(webPackedDest, 'nls.metadata.json')]) + .pipe(nls.createXlfFiles(transifexProjectName, transifexExtensionName)) + .pipe(gulp.dest(path.join('..', 'vscode-translations-export'))); +})); + +//---- internal -gulp.task('internal-upload', function() { - return gulp.src('upload/**/*') - .pipe(azure.upload({ - account: process.env.AZURE_STORAGE_ACCOUNT, - key: process.env.AZURE_STORAGE_ACCESS_KEY, - container: 'debuggers' +function compile() { + var r = tsProject.src() + .pipe(sourcemaps.init()) + .pipe(tsProject()).js; + + if (inlineMap && inlineSource) { + r = r.pipe(sourcemaps.write()); + } else { + r = r.pipe(sourcemaps.write("../out", { + // no inlined source + includeContent: inlineSource, + // Return relative source map root directories per file. + sourceRoot: "../src" })); + } + + return r.pipe(gulp.dest(outDest)); +} + +var allTypeScript = [ + 'src/**/*.ts' +]; + +var tslintFilter = [ + '**', + '!**/*.d.ts' +]; + +gulp.task('tslint', done => { + gulp.src(allTypeScript) + .pipe(filter(tslintFilter)) + .pipe(tslint({ + formatter: "prose", + rulesDirectory: "node_modules/tslint-microsoft-contrib" + })) + .pipe(tslint.report( { + emitError: false + })); + done(); }); diff --git a/images/node-debug-icon.png b/images/node-debug-icon.png new file mode 100644 index 00000000..9e31f672 Binary files /dev/null and b/images/node-debug-icon.png differ diff --git a/node/TerminalHelper.scpt b/node/TerminalHelper.scpt deleted file mode 100644 index 9c04e241..00000000 Binary files a/node/TerminalHelper.scpt and /dev/null differ diff --git a/node/debugExtension.ts b/node/debugExtension.ts deleted file mode 100644 index 96799b90..00000000 --- a/node/debugExtension.ts +++ /dev/null @@ -1,113 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -!function() { - var vm = require('vm'); - var LookupMirror = vm.runInDebugContext('LookupMirror'); - var PropertyKind = vm.runInDebugContext('PropertyKind'); - var DebugCommandProcessor = vm.runInDebugContext('DebugCommandProcessor'); - var JSONProtocolSerializer = vm.runInDebugContext('JSONProtocolSerializer'); - - JSONProtocolSerializer.prototype.serializeReferencedObjects = function () { - var content = []; - for (var i = 0; i < this.mirrors_.length; i++) { - var m = this.mirrors_[i]; - if (m.isArray()) continue; - if (m.isObject() && m.propertyNames(PropertyKind.Indexed | PropertyKind.Named, 100).length >= 100) continue; - content.push(this.serialize_(m, false, false)); - } - return content; - }; - - DebugCommandProcessor.prototype.dispatch_['vscode_range'] = function(request, response) { - var handle = request.arguments.handle; - var from_index = request.arguments.from; - var to_index = request.arguments.to; - var mirror = LookupMirror(handle); - if (!mirror) { - return response.failed('Object #' + handle + '# not found'); - } - var result; - if (mirror.isArray()) { - result = new Array(to_index - from_index + 1); - var a = mirror.indexedPropertiesFromRange(from_index, to_index); - for (var i = 0; i < a.length; i++) { - result[i] = a[i].value(); - - } - } else if (mirror.isObject()) { - result = new Array(to_index - from_index + 1); - for (var j = from_index; j <= to_index; j++) { - var p = mirror.property(j.toString()); - result[j - from_index] = p.value(); - } - } else { - result = new Array(to_index - from_index + 1); - } - response.body = { - result: result - }; - }; - - DebugCommandProcessor.prototype.vscode_dehydrate = function(mirror) { - var className = null; - var size = -1; - if (mirror.isArray()) { - className = "Array"; - size = mirror.length(); - } else if (mirror.isObject()) { - switch (mirror.toText()) { - case "#": - className = "Buffer"; - size = mirror.propertyNames(PropertyKind.Indexed).length; - break; - case "#": - case "#": - case "#": - case "#": - case "#": - case "#": - case "#": - case "#": - case "#": - className = mirror.className(); - size = mirror.propertyNames(PropertyKind.Indexed).length; - break; - default: - break; - } - } - if (size > 1000) { - return { - handle: mirror.handle(), - type: "object", - className: className, - size: size, - value: className - }; - } - return mirror; - }; - - DebugCommandProcessor.prototype.dispatch_['vscode_lookup'] = function(request, response) { - var result = this.lookupRequest_(request, response); - if (!result) { - var handles = request.arguments.handles; - for (var i = 0; i < handles.length; i++) { - var handle = handles[i]; - response.body[handle] = this.vscode_dehydrate(response.body[handle]); - } - } - return result; - }; - - DebugCommandProcessor.prototype.dispatch_['vscode_evaluate'] = function(request, response) { - var result = this.evaluateRequest_(request, response); - if (!result) { - response.body = this.vscode_dehydrate(response.body); - } - return result; - }; -}() \ No newline at end of file diff --git a/node/nodeDebug.ts b/node/nodeDebug.ts deleted file mode 100644 index 0699b684..00000000 --- a/node/nodeDebug.ts +++ /dev/null @@ -1,2037 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import {DebugSession, Thread, Source, StackFrame, Scope, Variable, Breakpoint, TerminatedEvent, InitializedEvent, StoppedEvent, OutputEvent, ErrorDestination} from '../common/debugSession'; -import {NodeV8Protocol, NodeV8Event, NodeV8Response} from './nodeV8Protocol'; -import {Handles} from '../common/handles'; -import {ISourceMaps, SourceMaps} from './sourceMaps'; -import {Terminal} from './terminal'; -import * as PathUtils from './pathUtilities'; -import * as CP from 'child_process'; -import * as Net from 'net'; -import * as Path from 'path'; -import * as FS from 'fs'; - - -const RANGESIZE = 1000; - -export interface Expandable { - Expand(session: NodeDebugSession, results: Array, done: () => void): void; -} - -export class PropertyExpander implements Expandable { - - private _object: any; - private _this: any; - protected _mode: string; - protected _start: number; - protected _end: number; - - public constructor(obj: any, ths?: any) { - this._object = obj; - this._this = ths; - this._mode = 'all'; - this._start = 0; - this._end = -1; - } - - public Expand(session: NodeDebugSession, variables: Array, done: () => void): void { - session._addProperties(variables, this._object, this._mode, this._start, this._end, () => { - if (this._this) { - session._addVariable(variables, 'this', this._this, done); - } else { - done(); - } - }); - } -} - -export class PropertyRangeExpander extends PropertyExpander { - public constructor(obj: any, start: number, end: number) { - super(obj, null); - this._mode = 'range'; - this._start = start; - this._end = end; - } -} - -export class ArrayExpander implements Expandable { - - private _object: any; - private _size: number; - - public constructor(obj: any, size: number) { - this._object = obj; - this._size = size; - } - - public Expand(session: NodeDebugSession, variables: Array, done: () => void): void { - // first add named properties - session._addProperties(variables, this._object, 'named', 0, -1, () => { - // then add indexed properties as ranges - for (let start = 0; start < this._size; start += RANGESIZE) { - let end = Math.min(start + RANGESIZE, this._size)-1; - variables.push(new Variable(`[${start}..${end}]`, ' ', session._variableHandles.create(new PropertyRangeExpander(this._object, start, end)))); - } - done(); - }); - } -} - -/** - * This interface should always match the schema found in the node-debug extension manifest. - */ -export interface SourceMapsArguments { - /** Configure source maps. By default source maps are disabled. */ - sourceMaps?: boolean; - /** Where to look for the generated code. Only used if sourceMaps is true. */ - outDir?: string; -} - -/** - * This interface should always match the schema found in the node-debug extension manifest. - */ -export interface LaunchRequestArguments extends SourceMapsArguments { - /** An absolute path to the program to debug. */ - program: string; - /** Automatically stop target after launch. If not specified, target does not stop. */ - stopOnEntry?: boolean; - /** Optional arguments passed to the debuggee. */ - args?: string[]; - /** Launch the debuggee in this working directory (specified as an absolute path). If omitted the debuggee is lauched in its own directory. */ - cwd?: string; - /** Absolute path to the runtime executable to be used. Default is the runtime executable on the PATH. */ - runtimeExecutable?: string; - /** Optional arguments passed to the runtime executable. */ - runtimeArgs?: string[]; - /** Optional environment variables to pass to the debuggee. The string valued properties of the 'environmentVariables' are used as key/value pairs. */ - env?: { [key: string]: string; }; - /** If true launch the target in an external console. */ - externalConsole?: boolean; -} - -/** - * This interface should always match the schema found in the node-debug extension manifest. - */ -export interface AttachRequestArguments extends SourceMapsArguments { - /** The local port to attach to */ - port: number; -} - -export class NodeDebugSession extends DebugSession { - - private static TRACE = false; - private static TRACE_INITIALISATION = false; - - private static NODE = 'node'; - private static DUMMY_THREAD_ID = 1; - private static DUMMY_THREAD_NAME = 'Node'; - private static FIRST_LINE_OFFSET = 62; - private static PROTO = '__proto__'; - private static DEBUG_EXTENSION = 'debugExtension.js'; - private static NODE_TERMINATION_POLL_INTERVAL = 3000; - - private static NODE_SHEBANG_MATCHER = new RegExp('#! */usr/bin/env +node'); - - // stop reasons - private static ENTRY_REASON = "entry"; - private static STEP_REASON = "step"; - private static BREAKPOINT_REASON = "breakpoint"; - private static EXCEPTION_REASON = "exception"; - private static DEBUGGER_REASON = "debugger statement"; - private static USER_REQUEST_REASON = "user request"; - - private static ANON_FUNCTION = "(anonymous function)"; - - private static SCOPE_NAMES = [ "Global", "Local", "With", "Closure", "Catch", "Block", "Script" ]; - - private static LARGE_DATASTRUCTURE_TIMEOUT = "<...>"; // ""; - - private _adapterID: string; - public _variableHandles = new Handles(); - public _frameHandles = new Handles(); - private _refCache = new Map(); - - private _externalConsole: boolean; - private _isTerminated: boolean; - private _inShutdown: boolean; - private _terminalProcess: CP.ChildProcess; // the terminal process or undefined - private _nodeProcessId: number = -1; // pid of the node runtime - private _node: NodeV8Protocol; - private _exception; - private _lastStoppedEvent; - private _nodeExtensionsAvailable: boolean = false; - private _tryToExtendNode: boolean = true; - private _attachMode: boolean = false; - private _sourceMaps: ISourceMaps; - private _stopOnEntry: boolean; - private _needContinue: boolean; - private _needBreakpointEvent: boolean; - private _lazy: boolean; // whether node is in 'lazy' mode - - private _gotEntryEvent: boolean; - private _entryPath: string; - private _entryLine: number; - private _entryColumn: number; - - - public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) { - super(debuggerLinesStartAt1, isServer); - - this._node = new NodeV8Protocol(); - - this._node.on('break', (event: NodeV8Event) => { - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: got break event from node'); - this._stopped(); - this._lastStoppedEvent = this.createStoppedEvent(event.body); - if (this._lastStoppedEvent.body.reason === NodeDebugSession.ENTRY_REASON) { - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: supressed stop-on-entry event'); - } else { - this.sendEvent(this._lastStoppedEvent); - } - }); - - this._node.on('exception', (event: NodeV8Event) => { - this._stopped(); - this._lastStoppedEvent = this.createStoppedEvent(event.body); - this.sendEvent(this._lastStoppedEvent); - }); - - this._node.on('close', (event: NodeV8Event) => { - this._terminated('node v8protocol close'); - }); - - this._node.on('error', (event: NodeV8Event) => { - this._terminated('node v8protocol error'); - }); - - this._node.on('diagnostic', (event: NodeV8Event) => { - // console.error('diagnostic event: ' + event.body.reason); - }); - } - - /** - * clear everything that is no longer valid after a new stopped event. - */ - private _stopped(): void { - this._exception = undefined; - this._variableHandles.reset(); - this._frameHandles.reset(); - this._refCache = new Map(); - } - - /** - * The debug session has terminated. - * If a port is given, this data is added to the event so that a client can try to reconnect. - */ - private _terminated(reason: string, reattachPort?: number): void { - if (NodeDebugSession.TRACE) console.error('_terminate: ' + reason); - - if (this._terminalProcess) { - // delay the TerminatedEvent so that the user can see the result of the process in the terminal - return; - } - - if (!this._isTerminated) { - this._isTerminated = true; - const e = new TerminatedEvent(); - // piggyback the port to re-attach - if (reattachPort) { - if (!(e).body) { - (e).body = {}; - } - (e).body.extensionHost = { - reattachPort: reattachPort - }; - } - this.sendEvent(e); - } - } - - //---- initialize request ------------------------------------------------------------------------------------------------- - - protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { - - this._adapterID = args.adapterID; - this.sendResponse(response); - } - - //---- launch request ----------------------------------------------------------------------------------------------------- - - protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { - - this._externalConsole = (typeof args.externalConsole === 'boolean') && args.externalConsole; - this._stopOnEntry = (typeof args.externalConsole === 'boolean') && args.stopOnEntry; - - this._initializeSourceMaps(args); - - var port = random(3000, 50000); - - let runtimeExecutable = this.convertClientPathToDebugger(args.runtimeExecutable); - if (runtimeExecutable) { - if (!FS.existsSync(runtimeExecutable)) { - this.sendErrorResponse(response, 2006, "runtime executable '{path}' does not exist", { path: runtimeExecutable }); - return; - } - } else { - if (!Terminal.isOnPath(NodeDebugSession.NODE)) { - this.sendErrorResponse(response, 2001, "cannot find runtime '{_runtime}' on PATH", { _runtime: NodeDebugSession.NODE }); - return; - } - runtimeExecutable = NodeDebugSession.NODE; // use node from PATH - } - - const runtimeArgs = args.runtimeArgs || []; - const programArgs = args.args || []; - - this._lazy = true; // node by default starts in '--lazy' mode - - // special code for 'extensionHost' debugging - if (this._adapterID === 'extensionHost') { - - let extensionHostData = (args).extensionHostData; - if (extensionHostData) { - // re-attach to the received port - port = extensionHostData.reattachPort; - } else { - // make sure that we launch VSCode and not just electron pretending to be node - delete process.env['ATOM_SHELL_INTERNAL_RUN_AS_NODE']; - - // we know that extensionHost is always launched with --nolazy - this._lazy = false; - - // we always launch in 'debug-brk' mode, but we only show the break event if 'stopOnEntry' attribute is true. - const launchArgs = [ runtimeExecutable, `--debugBrkPluginHost=${port}` ].concat(runtimeArgs, programArgs); - - this._sendLaunchCommandToConsole(launchArgs); - - const cmd = CP.spawn(runtimeExecutable, launchArgs.slice(1)); - cmd.on('error', (err) => { - this._terminated(`failed to launch extensionHost (${err})`); - }); - this._captureOutput(cmd); - } - - // try to attach - setTimeout(() => { - this._attach(response, port, 3000); - }, 2000); - - // we are done! - return; - } - - let programPath = args.program; - if (programPath) { - programPath = this.convertClientPathToDebugger(programPath); - if (!FS.existsSync(programPath)) { - this.sendErrorResponse(response, 2007, "program '{path}' does not exist", { path: programPath }); - return; - } - } else { - this.sendErrorResponse(response, 2005, "property 'program' is missing or empty"); - return; - } - - if (NodeDebugSession.isJavaScript(programPath)) { - if (this._sourceMaps) { - // source maps enabled indicates that a tool like Babel is used to transpile js to js - const generatedPath = this._sourceMaps.MapPathFromSource(programPath); - if (generatedPath) { - // there seems to be a generated file, so use that - programPath = generatedPath; - } - } - } else { - // node cannot execute the program directly - if (!this._sourceMaps) { - this.sendErrorResponse(response, 2002, "cannot launch program '{path}'; enabling source maps might help", { path: programPath }); - return; - } - const generatedPath = this._sourceMaps.MapPathFromSource(programPath); - if (!generatedPath) { // cannot find generated file - this.sendErrorResponse(response, 2003, "cannot launch program '{path}'; setting the 'outDir' attribute might help", { path: programPath }); - return; - } - programPath = generatedPath; - } - - let program: string; - let workingDirectory = this.convertClientPathToDebugger(args.cwd); - if (workingDirectory) { - if (!FS.existsSync(workingDirectory)) { - this.sendErrorResponse(response, 2004, "working directory '{path}' does not exist", { path: workingDirectory }); - return; - } - // if working dir is given and if the executable is within that folder, we make the executable path relative to the working dir - program = Path.relative(workingDirectory, programPath); - } - else { // should not happen - // if no working dir given, we use the direct folder of the executable - workingDirectory = Path.dirname(programPath); - program = Path.basename(programPath); - } - - if (runtimeArgs.indexOf('--nolazy') >= 0) { - this._lazy = false; - } else { - if (runtimeArgs.indexOf('--lazy') < 0) { // if user does not force 'lazy' mode - runtimeArgs.push('--nolazy'); // we force node to compile everything so that breakpoints work immediately - this._lazy = false; - } - } - - // we always break on entry (but if user did not request this, we will not stop in the UI). - const launchArgs = [ runtimeExecutable, `--debug-brk=${port}` ].concat(runtimeArgs, [ program ], programArgs); - - if (this._externalConsole) { - - Terminal.launchInTerminal(workingDirectory, launchArgs, args.env).then((term: CP.ChildProcess) => { - - if (term) { - // if we got a terminal process, we will track it - this._terminalProcess = term; - term.on('exit', () => { - this._terminalProcess = null; - this._terminated('terminal exited'); - }); - } - - this._attach(response, port); - }).catch((error) => { - this.sendErrorResponse(response, 2011, "cannot launch target in terminal (reason: {_error})", { _error: error.message }, ErrorDestination.Telemetry | ErrorDestination.User ); - this._terminated('terminal error: ' + error.message); - }); - - } else { - - this._sendLaunchCommandToConsole(launchArgs); - - // merge environment variables into a copy of the process.env - const env = extendObject(extendObject( { }, process.env), args.env); - - const options = { - cwd: workingDirectory, - env: env - }; - - const cmd = CP.spawn(runtimeExecutable, launchArgs.slice(1), options); - cmd.on('error', (error) => { - this.sendErrorResponse(response, 2017, "cannot launch target (reason: {_error})", { _error: error.message }, ErrorDestination.Telemetry | ErrorDestination.User ); - this._terminated(`failed to launch target (${error})`); - }); - cmd.on('exit', () => { - this._terminated('target exited'); - }); - cmd.on('close', (code) => { - this._terminated('target closed'); - }); - - this._captureOutput(cmd); - - //cmd.stdin.end(); // close stdin because we do not support input for a target - - this._attach(response, port); - } - } - - private _sendLaunchCommandToConsole(args: string[]) { - // print the command to launch tghe target to the debug console - let cli = ''; - for (var a of args) { - if (a.indexOf(' ') >= 0) { - cli += '\'' + a + '\''; - } else { - cli += a; - } - cli += ' '; - } - this.sendEvent(new OutputEvent(cli, 'console')); - } - - private _captureOutput(process: CP.ChildProcess) { - - var sanitize = (s: string) => s.toString().replace(/\r\n$/mg, '\n'); - - process.stdout.on('data', (data: string) => { - this.sendEvent(new OutputEvent(data.toString(), 'stdout')); - }); - process.stderr.on('data', (data: string) => { - this.sendEvent(new OutputEvent(data.toString(), 'stderr')); - }); - } - - private _initializeSourceMaps(args: SourceMapsArguments) { - if (typeof args.sourceMaps === 'boolean' && args.sourceMaps) { - const generatedCodeDirectory = args.outDir; - this._sourceMaps = new SourceMaps(generatedCodeDirectory); - } - } - - //---- attach request ----------------------------------------------------------------------------------------------------- - - protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - - this._initializeSourceMaps(args); - - if (!args.port) { - this.sendErrorResponse(response, 2008, "property 'port' is missing"); - return; - } - const port = args.port; - this._attachMode = true; - this._attach(response, port); - } - - /* - * shared code used in launchRequest and attachRequest - */ - private _attach(response: DebugProtocol.Response, port: number, timeout: number = 5000): void { - let connected = false; - const socket = new Net.Socket(); - socket.connect(port); - socket.on('connect', (err: any) => { - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: connect event in _attach'); - connected = true; - this._node.startDispatch(socket, socket); - - if (this._adapterID === 'extensionHost' /* && this._node.embeddedHostVersion === 4 */) { - // for some reason we need a 'continue' request to make node send the stop-on-entry event in node versions 4.x - this._node.command('continue', null, (resp: NodeV8Response) => { - this._initialize(response); - }); - } else { - this._initialize(response); - } - return; - }); - const endTime = new Date().getTime() + timeout; - socket.on('error', (err: any) => { - if (connected) { - // since we are connected this error is fatal - this._terminateAndRetry('socket error', port); - } else { - // we are not yet connected so retry a few times - if (err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET') { - const now = new Date().getTime(); - if (now < endTime) { - setTimeout(() => { - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: retry socket.connect'); - socket.connect(port); - }, 200); // retry after 200 ms - } else { - this.sendErrorResponse(response, 2009, "cannot connect to runtime process (timeout after {_timeout}ms)", { _timeout: timeout }); - } - } else { - this.sendErrorResponse(response, 2010, "cannot connect to runtime process (reason: {_error})", { _error: err }); - } - } - }); - socket.on('end', (err: any) => { - this._terminateAndRetry('socket end', port); - }); - } - - private _terminateAndRetry(reason: string, port: number): void { - if (this._adapterID === 'extensionHost' && !this._inShutdown) { - this._terminated(reason, port); - } else { - this._terminated(reason); - } - } - - private _initialize(response: DebugProtocol.Response, retryCount: number = 0) : void { - - this._node.command('evaluate', { expression: 'process.pid', global: true }, (resp: NodeV8Response) => { - - let ok = resp.success; - if (resp.success) { - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: retrieve node pid: OK'); - this._nodeProcessId = parseInt(resp.body.value); - } else { - if (resp.message.indexOf('process is not defined') >= 0) { - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: process not defined error; got no pid'); - ok = true; // continue and try to get process.pid later - } - } - - if (ok) { - - this._pollForNodeTermination(); - - const runtimeSupportsExtension = this._node.embeddedHostVersion === 0; // node version 0.x.x (io.js has version >= 1) - if (this._tryToExtendNode && runtimeSupportsExtension) { - this._extendDebugger((success: boolean) => { - this.sendResponse(response); - this._startInitialize(!resp.running); - return; - }); - } else { - this.sendResponse(response); - this._startInitialize(!resp.running); - return; - } - } else { - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: retrieve node pid: failed'); - - if (retryCount < 10) { - setTimeout(() => { - // recurse - this._initialize(response, retryCount+1); - }, 50); - return; - } else { - this.sendNodeResponse(response, resp); - } - } - }); - } - - private _pollForNodeTermination() : void { - const id = setInterval(() => { - try { - if (this._nodeProcessId > 0) { - (process).kill(this._nodeProcessId, 0); // node.d.ts doesn't like number argumnent - } else { - clearInterval(id); - } - } catch(e) { - clearInterval(id); - this._terminated('node process kill exception'); - } - }, NodeDebugSession.NODE_TERMINATION_POLL_INTERVAL); - } - - /* - * Inject code into node.js to fix timeout issues with large data structures. - */ - private _extendDebugger(done: (success: boolean) => void) : void { - try { - const contents = FS.readFileSync(Path.join(__dirname, NodeDebugSession.DEBUG_EXTENSION), 'utf8'); - - this._repeater(4, done, (callback: (again: boolean) => void) => { - - this._node.command('evaluate', { expression: contents }, (resp: NodeV8Response) => { - if (resp.success) { - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: node code inject: OK'); - this._nodeExtensionsAvailable = true; - callback(false); - } else { - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: node code inject: failed, try again...'); - callback(true); - } - }); - - }); - - } catch(e) { - done(false); - } - } - - /* - * start the initialization sequence: - * 1. wait for "break-on-entry" (with timeout) - * 2. send "inititialized" event in order to trigger setBreakpointEvents request from client - * 3. prepare for sending "break-on-entry" or "continue" later in _finishInitialize() - */ - private _startInitialize(stopped: boolean, n: number = 0): void { - - if (NodeDebugSession.TRACE_INITIALISATION) console.error(`_init: _startInitialize(${stopped})`); - - // wait at most 500ms for receiving the break on entry event - // (since in attach mode we cannot enforce that node is started with --debug-brk, we cannot assume that we receive this event) - - if (!this._gotEntryEvent && n < 10) { - setTimeout(() => { - // recurse - this._startInitialize(stopped, n+1); - }, 50); - return; - } - - if (this._gotEntryEvent) { - if (NodeDebugSession.TRACE_INITIALISATION) console.error(`_init: got break on entry event after ${n} retries`); - if (this._nodeProcessId <= 0) { - // if we haven't gotten a process pid so far, we try it again - this._node.command('evaluate', { expression: 'process.pid', global: true }, (resp: NodeV8Response) => { - if (resp.success) { - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: 2nd retrieve node pid: OK'); - this._nodeProcessId = parseInt(resp.body.value); - } - this._middleInitialize(stopped); - }); - } else { - this._middleInitialize(stopped); - } - } else { - if (NodeDebugSession.TRACE_INITIALISATION) console.error(`_init: no entry event after ${n} retries; give up`); - - this._gotEntryEvent = true; // we pretend to got one so that no ENTRY_REASON event will show up later... - - this._node.command('frame', null, (resp: NodeV8Response) => { - if (resp.success) { - this.cacheRefs(resp); - let s = this.getValueFromCache(resp.body.script); - this.rememberEntryLocation(s.name, resp.body.line, resp.body.column); - } - - this._middleInitialize(stopped); - }); - } - } - - private _middleInitialize(stopped: boolean): void { - // request UI to send breakpoints - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: -> fire initialize event'); - this.sendEvent(new InitializedEvent()); - - // in attach-mode we don't know whether the debuggee has been launched in 'stop on entry' mode - // so we use the stopped state of the VM - if (this._attachMode) { - if (NodeDebugSession.TRACE_INITIALISATION) console.error(`_init: in attach mode we guess stopOnEntry flag to be "${stopped}"`); - this._stopOnEntry = stopped; - } - - if (this._stopOnEntry) { - // user has requested 'stop on entry' so send out a stop-on-entry - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: -> fire stop-on-entry event'); - this.sendEvent(new StoppedEvent(NodeDebugSession.ENTRY_REASON, NodeDebugSession.DUMMY_THREAD_ID)); - } - else { - // since we are stopped but UI doesn't know about this, remember that we continue later in finishInitialize() - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: remember to do a "Continue" later'); - this._needContinue = true; - } - } - - private _finishInitialize(): void { - if (this._needContinue) { - this._needContinue = false; - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: do a "Continue"'); - this._node.command('continue', null, (nodeResponse) => { }); - } - if (this._needBreakpointEvent) { - this._needBreakpointEvent = false; - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: fire a breakpoint event'); - this.sendEvent(new StoppedEvent(NodeDebugSession.BREAKPOINT_REASON, NodeDebugSession.DUMMY_THREAD_ID)); - } - } - - //---- disconnect request ------------------------------------------------------------------------------------------------- - - protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { - - // special code for 'extensionHost' debugging - if (this._adapterID === 'extensionHost') { - // detect whether this disconnect request is part of a restart session - if (args && (args).extensionHostData && (args).extensionHostData.restart && this._nodeProcessId > 0) { - this._nodeProcessId = 0; - } - } - - super.disconnectRequest(response, args); - } - - /** - * we rely on the generic implementation from debugSession but we override 'v8Protocol.shutdown' - * to disconnect from node and kill node & subprocesses - */ - public shutdown(): void { - if (!this._inShutdown) { - this._inShutdown = true; - - this._node.command('disconnect'); // we don't wait for reponse - this._node.stop(); // stop socket connection (otherwise node.js dies with ECONNRESET on Windows) - - if (!this._attachMode) { - // kill the whole process tree either starting with the terminal or with the node process - let pid = this._terminalProcess ? this._terminalProcess.pid : this._nodeProcessId; - if (pid > 0) { - Terminal.killTree(pid).then(() => { - this._terminalProcess = null; - this._nodeProcessId = -1; - super.shutdown(); - }).catch((error) => { - this._terminalProcess = null; - this._nodeProcessId = -1; - super.shutdown(); - }); - return; - } - } - - super.shutdown(); - } - } - - //--- set breakpoints request --------------------------------------------------------------------------------------------- - - protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { - - let sourcemap = false; - - const source = args.source; - const clientLines = args.lines; - - // convert line numbers from client - const lines = new Array(clientLines.length); - const columns = new Array(clientLines.length); - for (let i = 0; i < clientLines.length; i++) { - lines[i] = this.convertClientLineToDebugger(clientLines[i]); - columns[i] = 0; - } - - let scriptId = -1; - let path: string = null; - - // we assume that only one of the source attributes is specified. - if (source.path) { - path = this.convertClientPathToDebugger(source.path); - // resolve the path to a real path (resolve symbolic links) - //path = PathUtilities.RealPath(path, _realPathMap); - - let p: string = null; - if (this._sourceMaps) { - p = this._sourceMaps.MapPathFromSource(path); - } - if (p) { - sourcemap = true; - // source map line numbers - for (let i = 0; i < lines.length; i++) { - let pp = path; - const mr = this._sourceMaps.MapFromSource(pp, lines[i], columns[i]); - if (mr) { - pp = mr.path; - lines[i] = mr.line; - columns[i] = mr.column; - } - if (pp !== p) { - // console.error(`setBreakPointsRequest: sourceMap limitation ${pp}`); - } - } - path = p; - } - else if (!NodeDebugSession.isJavaScript(path)) { - // return these breakpoints as unverified - const bpts = new Array(); - for (let l of clientLines) { - bpts.push(new Breakpoint(false, l)); - } - response.body = { - breakpoints: bpts - }; - this.sendResponse(response); - return; - } - this._clearAllBreakpoints(response, path, -1, lines, columns, sourcemap, clientLines); - return; - } - - if (source.name) { - this.findModule(source.name, (id: number) => { - scriptId = id; - this._clearAllBreakpoints(response, null, scriptId, lines, columns, sourcemap, clientLines); - return; - }); - } - - if (source.sourceReference > 0) { - scriptId = source.sourceReference - 1000; - this._clearAllBreakpoints(response, null, scriptId, lines, columns, sourcemap, clientLines); - return; - } - - this.sendErrorResponse(response, 2012, "no source specified", null, ErrorDestination.Telemetry); - } - - /* - * Phase 2 of setBreakpointsRequest: clear all breakpoints of a given file - */ - private _clearAllBreakpoints(response: DebugProtocol.SetBreakpointsResponse, path: string, scriptId: number, lines: number[], columns: number[], sourcemap: boolean, clientLines: number[]): void { - - // clear all existing breakpoints for the given path or script ID - this._node.command('listbreakpoints', null, (nodeResponse: NodeV8Response) => { - - if (nodeResponse.success) { - const toClear = new Array(); - - // try to match breakpoints - for (let breakpoint of nodeResponse.body.breakpoints) { - const type: string = breakpoint.type; - switch (type) { - case 'scriptId': - const script_id: number = breakpoint.script_id; - if (script_id === scriptId) { - toClear.push(breakpoint.number); - } - break; - case 'scriptName': - const script_name: string = breakpoint.script_name; - if (script_name === path) { - toClear.push(breakpoint.number); - } - break; - } - } - - this._clearBreakpoints(toClear, 0, () => { - this._finishSetBreakpoints(response, path, scriptId, lines, columns, sourcemap, clientLines); - }); - - } else { - this.sendNodeResponse(response, nodeResponse); - } - - }); - } - - /** - * Recursive function for deleting node breakpoints. - */ - private _clearBreakpoints(ids: Array, ix: number, done: () => void) : void { - - if (ids.length == 0) { - done(); - return; - } - - this._node.command('clearbreakpoint', { breakpoint: ids[ix] }, (nodeResponse: NodeV8Response) => { - if (!nodeResponse.success) { - // we ignore errors for now - // console.error('clearbreakpoint error: ' + rr.message); - } - if (ix+1 < ids.length) { - setImmediate(() => { - // recurse - this._clearBreakpoints(ids, ix+1, done); - }); - } else { - done(); - } - }); - } - - /* - * Finish the setBreakpointsRequest: set the breakpooints and send the verification response back to client - */ - private _finishSetBreakpoints(response: DebugProtocol.SetBreakpointsResponse, path: string, scriptId: number, lines: number[], columns: number[], sourcemap: boolean, clientLines: number[]): void { - - const breakpoints = new Array(); - - this._setBreakpoints(breakpoints, 0, path, scriptId, lines, columns, sourcemap, clientLines, () => { - response.body = { - breakpoints: breakpoints - }; - this.sendResponse(response); - }); - } - - /** - * Recursive function for setting node breakpoints. - */ - private _setBreakpoints(breakpoints: Array, ix: number, path: string, scriptId: number, lines: number[], columns: number[], sourcemap: boolean, clientLines: number[], done: () => void) : void { - - if (lines.length == 0) { // nothing to do - done(); - return; - } - - this._robustSetBreakPoint(scriptId, path, lines[ix], columns[ix], (verified: boolean, actualLine, actualColumn) => { - - // prepare sending breakpoint locations back to client - let sourceLine = clientLines[ix]; // we start with the original lines from the client - - if (verified) { - if (sourcemap) { - if (!this._lazy) { // only if not in lazy mode we try to map actual Positions back - // map adjusted js breakpoints back to source language - if (path && this._sourceMaps) { - const p = path; - const mr = this._sourceMaps.MapToSource(p, actualLine, actualColumn); - if (mr) { - actualLine = mr.line; - actualColumn = mr.column; - } - } - sourceLine = this.convertDebuggerLineToClient(actualLine); - } - } else { - sourceLine = this.convertDebuggerLineToClient(actualLine); - } - } - breakpoints[ix] = new Breakpoint(verified, sourceLine); - - // nasty corner case: since we ignore the break-on-entry event we have to make sure that we - // stop in the entry point line if the user has an explicit breakpoint there. - // For this we check here whether a breakpoint is at the same location as the "break-on-entry" location. - // If yes, then we plan for hitting the breakpoint instead of "continue" over it! - if (!this._stopOnEntry) { // only relevant if we do not stop on entry - const li = verified ? actualLine : lines[ix]; - const co = columns[ix]; // verified ? actualColumn : columns[ix]; - if (this._entryPath === path && this._entryLine === li && this._entryColumn === co) { - // if yes, we do not have to "continue" but we have to generate a stopped event instead - this._needContinue = false; - this._needBreakpointEvent = true; - if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: remember to fire a breakpoint event later'); - } - } - - if (ix+1 < lines.length) { - setImmediate(() => { - // recurse - this._setBreakpoints(breakpoints, ix+1, path, scriptId, lines, columns, sourcemap, clientLines, done); - }); - } else { - done(); - } - }); - } - - /* - * register a single breakpoint with node and retry if it fails due to drive letter casing (on Windows) - */ - private _robustSetBreakPoint(scriptId: number, path: string, l: number, c: number, done: (success: boolean, actualLine?: number, actualColumn?: number) => void): void { - this._setBreakpoint(scriptId, path, l, c, (verified: boolean, actualLine, actualColumn) => { - if (verified) { - done(true, actualLine, actualColumn); - return; - } - - // take care of a mismatch of drive letter caseing - const root = PathUtils.getPathRoot(path); - if (root && root.length === 3) { // root contains a drive letter - path = path.substring(0, 1).toUpperCase() + path.substring(1); - this._setBreakpoint(scriptId, path, l, c, (verified: boolean, actualLine, actualColumn) => { - if (verified) { - done(true, actualLine, actualColumn); - } else { - done(false); - } - }); - } else { - done(false); - } - }); - } - - /* - * register a single breakpoint with node. - */ - private _setBreakpoint(scriptId: number, path: string, l: number, c: number, cb: (success: boolean, actualLine?: number, actualColumn?: number) => void): void { - - if (l === 0) { - c += NodeDebugSession.FIRST_LINE_OFFSET; - } - - let actualLine = l; - let actualColumn = c; - - let a: any; - if (scriptId > 0) { - a = { type: 'scriptId', target: scriptId, line: l, column: c }; - } else { - a = { type: 'script', target: path, line: l, column: c }; - } - - this._node.command('setbreakpoint', a, (resp: NodeV8Response) => { - if (resp.success) { - const al = resp.body.actual_locations; - if (al.length > 0) { - actualLine = al[0].line; - actualColumn = al[0].column; - - if (actualLine === 0) { - actualColumn -= NodeDebugSession.FIRST_LINE_OFFSET; - if (actualColumn < 0) - actualColumn = 0; - } - - if (actualLine !== l) { - // console.error(`setbreakpoint: ${l} !== ${actualLine}`); - } - cb(true, actualLine, actualColumn); - return; - } - //console.error(`setbreakpoint: could not set breakpoint in ${path}:{l}`); - } - cb(false); - return; - }); - } - - //--- set exception request ----------------------------------------------------------------------------------------------- - - protected setExceptionBreakPointsRequest(response: DebugProtocol.SetExceptionBreakpointsResponse, args: DebugProtocol.SetExceptionBreakpointsArguments): void { - - let f: string; - const filters = args.filters; - if (filters) { - if (filters.indexOf('all') >= 0) { - f = 'all'; - } else if (filters.indexOf('uncaught') >= 0) { - f = 'uncaught'; - } - } - - // we need to simplify this... - this._node.command('setexceptionbreak', { type: 'all', enabled: false }, (nodeResponse1: NodeV8Response) => { - if (nodeResponse1.success) { - this._node.command('setexceptionbreak', { type: 'uncaught', enabled: false }, (nodeResponse2: NodeV8Response) => { - if (nodeResponse2.success) { - if (f) { - this._node.command('setexceptionbreak', { type: f, enabled: true }, (nodeResponse3: NodeV8Response) => { - if (nodeResponse3.success) { - this.sendResponse(response); // send response for setexceptionbreak - this._finishInitialize(); - } else { - this.sendNodeResponse(response, nodeResponse3); - } - }); - } else { - this.sendResponse(response); // send response for setexceptionbreak - this._finishInitialize(); - } - } else { - this.sendNodeResponse(response, nodeResponse2); - } - }); - } else { - this.sendNodeResponse(response, nodeResponse1); - } - }); - } - - //--- threads request ----------------------------------------------------------------------------------------------------- - - protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { - this._node.command('threads', null, (nodeResponse: NodeV8Response) => { - const threads = new Array(); - if (nodeResponse.success) { - const ths = nodeResponse.body.threads; - if (ths) { - for (let thread of ths) { - const id = thread.id; - if (id >= 0) { - threads.push(new Thread(id, NodeDebugSession.DUMMY_THREAD_NAME)); - } - } - } - } - if (threads.length === 0) { - threads.push(new Thread(NodeDebugSession.DUMMY_THREAD_ID, NodeDebugSession.DUMMY_THREAD_NAME)); - } - response.body = { - threads: threads - }; - this.sendResponse(response); - }); - } - - //--- stacktrace request -------------------------------------------------------------------------------------------------- - - protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { - - const threadReference = args.threadId; - const maxLevels = args.levels; - - if (threadReference !== NodeDebugSession.DUMMY_THREAD_ID) { - this.sendErrorResponse(response, 2014, "unexpected thread reference {_thread}", { _thread: threadReference }, ErrorDestination.Telemetry); - return; - } - - const stackframes = new Array(); - - this._getStackFrames(stackframes, 0, maxLevels, () => { - response.body = { - stackFrames: stackframes - }; - this.sendResponse(response); - } ); - } - - /** - * Recursive function for retrieving stackframes and their scopes in top to bottom order. - */ - private _getStackFrames(stackframes: Array, frameIx: number, maxLevels: number, done: () => void): void { - - this._node.command('backtrace', { fromFrame: frameIx, toFrame: frameIx+1 }, (backtraceResponse: NodeV8Response) => { - - if (backtraceResponse.success) { - - this.cacheRefs(backtraceResponse); - - let totalFrames = backtraceResponse.body.totalFrames; - if (maxLevels > 0 && totalFrames > maxLevels) { - totalFrames = maxLevels; - } - - if (totalFrames === 0) { - // no stack frames (probably because a 'pause' stops node in non-javascript code) - done(); - return; - } - - const frame = backtraceResponse.body.frames[0]; - - // resolve some refs - this.getValues([ frame.script, frame.func, frame.receiver ], () => { - - let line: number = frame.line; - let column: number = frame.column; - - let src: Source = null; - const script_val = this.getValueFromCache(frame.script); - if (script_val) { - let name = script_val.name; - if (name && Path.isAbsolute(name)) { - // try to map the real path back to a symbolic link - // string path = PathUtilities.MapResolvedBack(name, _realPathMap); - let path = name; - name = Path.basename(path); - - // workaround for column being off in the first line (because of a wrapped anonymous function) - if (line === 0) { - column -= NodeDebugSession.FIRST_LINE_OFFSET; - if (column < 0) - column = 0; - } - - // source mapping - if (this._sourceMaps) { - const mr = this._sourceMaps.MapToSource(path, line, column); - if (mr) { - path = mr.path; - line = mr.line; - column = mr.column; - } - } - - src = new Source(name, this.convertDebuggerPathToClient(path)); - } - - if (src === null) { - const script_id = script_val.id; - if (script_id >= 0) { - src = new Source(name, null, 1000 + script_id); - } - } - } - - let func_name: string; - const func_val = this.getValueFromCache(frame.func); - if (func_val) { - func_name = func_val.inferredName; - if (!func_name || func_name.length === 0) { - func_name = func_val.name; - } - } - if (!func_name || func_name.length === 0) { - func_name = NodeDebugSession.ANON_FUNCTION; - } - - const frameReference = this._frameHandles.create(frame); - const sf = new StackFrame(frameReference, func_name, src, this.convertDebuggerLineToClient(line), this.convertDebuggerColumnToClient(column)); - - stackframes.push(sf); - - if (frameIx+1 < totalFrames) { - // recurse - setImmediate(() => { - this._getStackFrames(stackframes, frameIx+1, maxLevels, done); - }); - } else { - // we are done - done(); - } - }); - - } else { - // error backtrace request - // stackframes.push(new StackFrame(frameIx, NodeDebugSession.LARGE_DATASTRUCTURE_TIMEOUT, null, 0, 0, [])); - done(); - } - }); - } - - //--- scopes request ------------------------------------------------------------------------------------------------------ - - protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { - - const frameReference = args.frameId; - const frame = this._frameHandles.get(frameReference); - const frameIx = frame.index; - const frameThis = this.getValueFromCache(frame.receiver); - - this._node.command('scopes', { frame_index: frameIx, frameNumber: frameIx }, (scopesResponse: NodeV8Response) => { - - if (scopesResponse.success) { - - this.cacheRefs(scopesResponse); - - const scopes = new Array(); - - // exception scope - if (frameIx === 0 && this._exception) { - scopes.push(new Scope("Exception", this._variableHandles.create(new PropertyExpander(this._exception)))); - } - - this._getScope(scopes, 0, scopesResponse.body.scopes, frameThis, () => { - response.body = { - scopes: scopes - }; - this.sendResponse(response); - }); - - } else { - response.body = { - scopes: [] - }; - this.sendResponse(response); - } - }); - } - - /** - * Recursive function for creating scopes in top to bottom order. - */ - private _getScope(scopesResult: Array, scopeIx: number, scopes: any[], this_val: any, done: () => void) { - - const scope = scopes[scopeIx]; - const type: number = scope.type; - const scopeName = (type >= 0 && type < NodeDebugSession.SCOPE_NAMES.length) ? NodeDebugSession.SCOPE_NAMES[type] : ("Unknown Scope:" + type); - const extra = type === 1 ? this_val : null; - const expensive = type === 0; - - this.getValue(scope.object, (scopeObject: any) => { - if (scopeObject) { - scopesResult.push(new Scope(scopeName, this._variableHandles.create(new PropertyExpander(scopeObject, extra)), expensive)); - } - if (scopeIx+1 < scopes.length) { - setImmediate(() => { - // recurse - this._getScope(scopesResult, scopeIx+1, scopes, this_val, done); - }); - } else { - done(); - } - }); - } - - //--- variables request --------------------------------------------------------------------------------------------------- - - protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void { - const reference = args.variablesReference; - const expander = this._variableHandles.get(reference); - if (expander) { - const variables = new Array(); - expander.Expand(this, variables, () => { - variables.sort(NodeDebugSession.compareVariableNames); - response.body = { - variables: variables - }; - this.sendResponse(response); - }); - } else { - response.body = { - variables: [] - }; - this.sendResponse(response); - } - } - - /* - * there are three modes: - * "all": add all properties (indexed and named) - * "range": add only the indexed properties between 'start' and 'end' (inclusive) - * "named": add only the named properties. - */ - public _addProperties(variables: Array, obj: any, mode: string, start: number, end: number, done: (message?) => void): void { - - const type = obj.type; - if (type === 'object' || type === 'function' || type === 'error' || type === 'regexp' || type === 'map' || type === 'set') { - - const properties = obj.properties; - if (!properties) { // if properties are missing, try to use size from vscode node extension - - switch (mode) { - case "range": - case "all": - const size = obj.size; - if (size >= 0) { - const handle = obj.handle; - if (typeof handle === 'number' && handle != 0) { - this._addArrayElements(variables, handle, start, end, done); - return; - } - } - done("array size not found"); - return; - - case "named": - // can't add named properties because we don't have access to them yet. - break; - } - done(); - return; - } - - const selectedProperties = new Array(); - - // first pass: determine properties - let found_proto = false; - for (let property of properties) { - - if ('name' in property) { // bug #19654: only extract properties with a node - - const name = property.name; - - if (name === NodeDebugSession.PROTO) { - found_proto = true; - } - - switch (mode) { - case "all": - selectedProperties.push(property); - break; - case "named": - if (typeof name == 'string') { - selectedProperties.push(property); - } - break; - case "range": - if (typeof name == 'number' && name >= start && name <= end) { - selectedProperties.push(property); - } - break; - } - } - } - - // do we have to add the protoObject to the list of properties? - if (!found_proto && (mode === 'all' || mode === 'named')) { - const h = obj.handle; - if (h > 0) { // only add if not an internal debugger object - obj.protoObject.name = NodeDebugSession.PROTO; - selectedProperties.push(obj.protoObject); - } - } - - // second pass: find properties where additional value lookup is required - const needLookup = new Array(); - for (let property of selectedProperties) { - if (!property.value && property.ref) { - if (needLookup.indexOf(property.ref) < 0) { - needLookup.push(property.ref); - } - } - } - - if (selectedProperties.length > 0) { - // third pass: now lookup all refs at once - this.resolveToCache(needLookup, () => { - // build variables - this._addVariables(variables, selectedProperties, 0, done); - }); - return; - } - } - done(); - } - - /** - * Recursive function for creating variables for the properties. - */ - private _addVariables(variables: Array, properties: Array, ix: number, done: () => void) { - - const property = properties[ix]; - const val = this.getValueFromCache(property); - - let name = property.name; - if (typeof name == 'number') { - name = `[${name}]`; - } - - this._addVariable(variables, name, val, () => { - if (ix+1 < properties.length) { - setImmediate(() => { - // recurse - this._addVariables(variables, properties, ix+1, done); - }); - } else { - done(); - } - }); - } - - private _addArrayElements(variables: Array, array_ref: number, start: number, end: number, done: (message?: string) => void): void { - this._node.command('vscode_range', { handle: array_ref, from: start, to: end }, (resp: NodeV8Response) => { - if (resp.success) { - this._addArrayElement(variables, start, resp.body.result, 0, done); - } else { - done(resp.message); - } - }); - } - - /** - * Recursive function for creating variables for the given array items. - */ - private _addArrayElement(variables: Array, start: number, items: Array, ix: number, done: () => void) { - const name = `[${start+ix}]`; - this._createVariable(name, items[ix], (v: Variable) => { - variables.push(v); - if (ix+1 < items.length) { - setImmediate(() => { - // recurse - this._addArrayElement(variables, start, items, ix+1, done); - }); - } else { - done(); - } - }); - } - - public _addVariable(variables: Array, name: string, val: any, done: () => void): void { - this._createVariable(name, val, (result: Variable) => { - if (result) { - variables.push(result); - } - done(); - }); - } - - private _createVariable(name: string, val: any, done: (result: Variable) => void): void { - if (!val) { - done(null); - return; - } - let str_val = val.value; - const type = val.type; - - switch (type) { - - case 'object': - case 'function': - case 'regexp': - case 'error': - // indirect value - - let value = val.className; - let text = val.text; - - switch (value) { - case 'Array': case 'Buffer': - case 'Int8Array': case 'Uint8Array': case 'Uint8ClampedArray': - case 'Int16Array': case 'Uint16Array': - case 'Int32Array': case 'Uint32Array': - case 'Float32Array': case 'Float64Array': - - if (val.ref) { - //val = this.getRef(val.ref); - } - - let size = val.size; // probe for our own "size" - if (size) { - done(this._createArrayVariable(name, val, value, size)); - } else { - const l = val.properties[0]; - if (l) { - size = l.value; - if (size) { - done(this._createArrayVariable(name, val, value, size)); - } else { - // the first property of arrays is the length - this.getValue(l, (length_val: any) => { - let size = -1; - if (length_val) { - size = length_val.value; - } - done(this._createArrayVariable(name, val, value, size)); - }); - } - } - } - return; - - case 'RegExp': - done(new Variable(name, text, this._variableHandles.create(new PropertyExpander(val)))); - return; - - case 'Object': - this.getValue(val.constructorFunction, (constructor_val) => { - if (constructor_val) { - const constructor_name = constructor_val.name; - if (constructor_name) { - value = constructor_name; - } - } - done(new Variable(name, value, this._variableHandles.create(new PropertyExpander(val)))); - }); - return; - - case 'Function': - case 'Error': - default: - if (text) { - if (text.indexOf('\n') >= 0) { - // replace body of function with '...' - const pos = text.indexOf('{'); - if (pos > 0) { - text = text.substring(0, pos) + '{ … }'; - } - } - value = text; - } - break; - } - done(new Variable(name, value, this._variableHandles.create(new PropertyExpander(val)))); - return; - - case 'string': // direct value - if (str_val) { - str_val = str_val.replace('\n', '\\n').replace('\r', '\\r'); - } - done(new Variable(name, `"${str_val}"`)); - return; - - case 'boolean': - done(new Variable(name, str_val.toString().toLowerCase())); // node returns these boolean values capitalized - return; - - case 'map': - case 'set': - case 'undefined': - case 'null': - // type is only info we have - done(new Variable(name, type)); - return; - - case 'number': - done(new Variable(name, '' + val.value)); - return; - - case 'frame': - default: - done(new Variable(name, str_val ? str_val.toString() : 'undefined')); - return; - } - } - - private _createArrayVariable(name: string, val: any, value: string, size: number): Variable { - value += (size >= 0) ? `[${size}]` : '[]'; - const expander = (size > RANGESIZE) ? new ArrayExpander(val, size) : new PropertyExpander(val); // new PropertyRangeExpander(val, 0, size-1); - return new Variable(name, value, this._variableHandles.create(expander)); - } - - //--- pause request ------------------------------------------------------------------------------------------------------- - - protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) : void { - this._node.command('suspend', null, (nodeResponse) => { - if (nodeResponse.success) { - this._stopped(); - this._lastStoppedEvent = new StoppedEvent(NodeDebugSession.USER_REQUEST_REASON, NodeDebugSession.DUMMY_THREAD_ID); - this.sendResponse(response); - this.sendEvent(this._lastStoppedEvent); - } else { - this.sendNodeResponse(response, nodeResponse); - } - }); - } - - //--- continue request ---------------------------------------------------------------------------------------------------- - - protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { - this._node.command('continue', null, (nodeResponse) => { - this.sendNodeResponse(response, nodeResponse); - }); - } - - //--- step request -------------------------------------------------------------------------------------------------------- - - protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) : void { - this._node.command('continue', { stepaction: 'in' }, (nodeResponse) => { - this.sendNodeResponse(response, nodeResponse); - }); - } - - protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) : void { - this._node.command('continue', { stepaction: 'out' }, (nodeResponse) => { - this.sendNodeResponse(response, nodeResponse); - }); - } - - protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { - this._node.command('continue', { stepaction: 'next' }, (nodeResponse) => { - this.sendNodeResponse(response, nodeResponse); - }); - } - - //--- evaluate request ---------------------------------------------------------------------------------------------------- - - protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { - - const expression = args.expression; - - const evalArgs = { - expression: expression, - disable_break: true, - maxStringLength: 10000 - }; - if (args.frameId > 0) { - const frame = this._frameHandles.get(args.frameId); - const frameIx = frame.index; - (evalArgs).frame = frameIx; - } else { - (evalArgs).global = true; - } - - this._node.command(this._nodeExtensionsAvailable ? 'vscode_evaluate' : 'evaluate', evalArgs, (resp: NodeV8Response) => { - if (resp.success) { - this._createVariable('evaluate', resp.body, (v: Variable) => { - if (v) { - response.body = { - result: v.value, - variablesReference: v.variablesReference - }; - } else { - response.success = false; - response.message = "not available"; - } - this.sendResponse(response); - }); - } else { - response.success = false; - if (resp.message.indexOf('ReferenceError: ') === 0 || resp.message === 'No frames') { - response.message = "not available"; - } else if (resp.message.indexOf('SyntaxError: ') === 0) { - const m = resp.message.substring('SyntaxError: '.length).toLowerCase(); - response.message = `invalid expression: ${m}`; - } else { - response.message = resp.message; - } - this.sendResponse(response); - } - }); - } - - //--- source request ------------------------------------------------------------------------------------------------------ - - protected sourceRequest(response: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments): void { - const sourceId = args.sourceReference; - const sid = sourceId - 1000; - this._node.command('scripts', { types: 1+2+4, includeSource: true, ids: [ sid ] }, (nodeResponse: NodeV8Response) => { - if (nodeResponse.success) { - const content = nodeResponse.body[0].source; - response.body = { - content: content - }; - this.sendResponse(response); - } else { - this.sendNodeResponse(response, nodeResponse); - } - }); - } - - //---- private helpers ---------------------------------------------------------------------------------------------------- - - private sendNodeResponse(response: DebugProtocol.Response, nodeResponse: NodeV8Response): void { - if (nodeResponse.success) { - this.sendResponse(response); - } else { - const errmsg = nodeResponse.message; - if (errmsg.indexOf('unresponsive') >= 0) { - this.sendErrorResponse(response, 2015, "request '{_request}' was cancelled because node is unresponsive", { _request: nodeResponse.command } ); - } else if (errmsg.indexOf('timeout') >= 0) { - this.sendErrorResponse(response, 2016, "node did not repond to request '{_request}' in a reasonable amount of time", { _request: nodeResponse.command } ); - } else { - this.sendErrorResponse(response, 2013, "node request '{_request}' failed (reason: {_error})", { _request: nodeResponse.command, _error: errmsg }, ErrorDestination.Telemetry); - } - } - } - - private _repeater(n: number, done: (success: boolean) => void, asyncwork: (done2: (again: boolean) => void) => void): void { - if (n > 0) { - asyncwork( (again: boolean) => { - if (again) { - setTimeout(() => { - // recurse - this._repeater(n-1, done, asyncwork); - }, 100); // retry after 100 ms - } else { - done(true); - } - }); - } else { - done(false); - } - } - - private cacheRefs(response: NodeV8Response): void { - const refs = response.refs; - for (let r of refs) { - this.cache(r.handle, r); - } - } - - private cache(handle: number, o: any): void { - this._refCache[handle] = o; - } - - private getValues(containers: any[], done: () => void): void { - - const handles = []; - for (let container of containers) { - handles.push(container.ref); - } - - this.resolveToCache(handles, () => { - done(); - }); - } - - private getValue(container: any, done: (result: any) => void): void { - if (container) { - const handle = container.ref; - this.resolveToCache([ handle ], () => { - const value = this._refCache[handle]; - done(value); - }); - } else { - done(null); - } - } - - private getValueFromCache(container: any): any { - const handle = container.ref; - const value = this._refCache[handle]; - if (value) - return value; - // console.error("ref not found cache"); - return null; - } - - private resolveToCache(handles: number[], done: () => void): void { - - const lookup = new Array(); - - for (let handle of handles) { - const val = this._refCache[handle]; - if (!val) { - if (handle >= 0) { - lookup.push(handle); - } else { - // console.error("shouldn't happen: cannot lookup transient objects"); - } - } - } - - if (lookup.length > 0) { - this._node.command(this._nodeExtensionsAvailable ? 'vscode_lookup' : 'lookup', { handles: lookup }, (resp: NodeV8Response) => { - - if (resp.success) { - this.cacheRefs(resp); - - for (let key in resp.body) { - const obj = resp.body[key]; - const handle: number = obj.handle; - this.cache(handle, obj); - } - - } else { - let val: any; - if (resp.message.indexOf('timeout') >= 0) { - val = { type: 'number', value: NodeDebugSession.LARGE_DATASTRUCTURE_TIMEOUT }; - } else { - val = { type: 'number', value: `` }; - } - - // store error value in cache - for (let i = 0; i < handles.length; i++) { - const handle = handles[i]; - const r = this._refCache[handle]; - if (!r) { - this.cache(handle, val); - } - } - } - done(); - }); - } else { - done(); - } - } - - private createStoppedEvent(body: any): DebugProtocol.StoppedEvent { - - // workaround: load sourcemap for this location to populate cache - if (this._sourceMaps) { - const path = body.script.name; - if (path) { - let mr = this._sourceMaps.MapToSource(path, 0, 0); - } - } - - let reason: string; - let exception_text: string; - - // is exception? - if (body.exception) { - this._exception = body.exception; - exception_text = body.exception.text; - reason = NodeDebugSession.EXCEPTION_REASON; - } - - // is breakpoint? - if (!reason) { - const breakpoints = body.breakpoints; - if (isArray(breakpoints) && breakpoints.length > 0) { - const id = breakpoints[0]; - if (!this._gotEntryEvent && id === 1) { // 'stop on entry point' is implemented as a breakpoint with id 1 - reason = NodeDebugSession.ENTRY_REASON; - const path = body.script.name; - const line = body.sourceLine; - const column = body.sourceColumn; - this.rememberEntryLocation(path, line, column); - } else { - reason = NodeDebugSession.BREAKPOINT_REASON; - } - } - } - - // is debugger statement? - if (!reason) { - const sourceLine = body.sourceLineText; - if (sourceLine && sourceLine.indexOf('debugger') >= 0) { - reason = NodeDebugSession.DEBUGGER_REASON; - } - } - - // must be "step"! - if (!reason) { - reason = NodeDebugSession.STEP_REASON; - } - - return new StoppedEvent(reason, NodeDebugSession.DUMMY_THREAD_ID, exception_text); - } - - private rememberEntryLocation(path: string, line: number, column: number): void { - if (path) { - this._entryPath = path; - this._entryLine = line; - this._entryColumn = column; - if (line === 0) { - this._entryColumn -= NodeDebugSession.FIRST_LINE_OFFSET; - if (this._entryColumn < 0) - this._entryColumn = 0; - } - this._gotEntryEvent = true; - } - } - - private findModule(name: string, cb: (id: number) => void): void { - this._node.command('scripts', { types: 1 + 2 + 4, filter: name }, (resp: NodeV8Response) => { - if (resp.success) { - if (resp.body.Count > 0) { - cb(resp.body[0].id); - return; - } - } - cb(-1); - }); - } - - //---- private static --------------------------------------------------------------- - - private static isJavaScript(path: string): boolean { - - const name = Path.basename(path).toLowerCase(); - if (endsWith(name, '.js')) { - return true; - } - - try { - const buffer = new Buffer(30); - const fd = FS.openSync(path, 'r'); - FS.readSync(fd, buffer, 0, buffer.length, 0); - FS.closeSync(fd); - const line = buffer.toString(); - if (NodeDebugSession.NODE_SHEBANG_MATCHER.test(line)) { - return true; - } - } catch(e) { - } - - return false; - } - - private static compareVariableNames(v1: Variable, v2: Variable): number { - let n1 = v1.name; - let n2 = v2.name; - - if (n1 === NodeDebugSession.PROTO) { - return 1; - } - if (n2 === NodeDebugSession.PROTO) { - return -1; - } - - // convert [n], [n..m] -> n - n1 = NodeDebugSession.extractNumber(n1); - n2 = NodeDebugSession.extractNumber(n2); - - const i1 = parseInt(n1); - const i2 = parseInt(n2); - const isNum1 = !isNaN(i1); - const isNum2 = !isNaN(i2); - - if (isNum1 && !isNum2) { - return 1; // numbers after names - } - if (!isNum1 && isNum2) { - return -1; // names before numbers - } - if (isNum1 && isNum2) { - return i1 - i2; - } - return n1.localeCompare(n2); - } - - private static extractNumber(s: string): string { - if (s[0] === '[' && s[s.length-1] === ']') { - s = s.substring(1, s.length - 1); - const p = s.indexOf('..'); - if (p >= 0) { - s = s.substring(0, p); - } - } - return s; - } -} - -function endsWith(str, suffix): boolean { - return str.indexOf(suffix, str.length - suffix.length) !== -1; -} - -function random(low: number, high: number): number { - return Math.floor(Math.random() * (high - low) + low); -} - -function isArray(what: any): boolean { - return Object.prototype.toString.call(what) === '[object Array]'; -} - -function extendObject (objectCopy: T, object: T): T { - - for (let key in object) { - if (object.hasOwnProperty(key)) { - objectCopy[key] = object[key]; - } - } - return objectCopy; -} - - -DebugSession.run(NodeDebugSession); \ No newline at end of file diff --git a/node/nodeV8Protocol.ts b/node/nodeV8Protocol.ts deleted file mode 100644 index 37b5c12d..00000000 --- a/node/nodeV8Protocol.ts +++ /dev/null @@ -1,260 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as EE from 'events'; - -export class NodeV8Message { - seq: number; - type: string; - - public constructor(type: string) { - this.seq = 0; - this.type = type; - } -} - -export class NodeV8Response extends NodeV8Message { - request_seq: number; - success: boolean; - running: boolean; - command: string; - message: string; - body: any; - refs: any; - - public constructor(request: NodeV8Response, message?: string) { - super('response'); - this.request_seq = request.seq; - this.command = request.command; - if (message) { - this.success = false; - this.message = message; - } else { - this.success = true; - } - } -} - -export class NodeV8Event extends NodeV8Message { - event: string; - body: any; - - public constructor(event: string, body?: any) { - super('event'); - this.event = event; - if (body) { - this.body = body; - } - } -} - -export class NodeV8Protocol extends EE.EventEmitter { - - private static TIMEOUT = 3000; - - private _state: string; - private _contentLength: number; - private _bodyStartByteIndex: number; - private _res: any; - private _sequence: number; - private _writableStream: NodeJS.WritableStream; - private _pendingRequests = new Map(); - private _unresponsiveMode: boolean; - - public embeddedHostVersion: number = -1; - - - public startDispatch(inStream: NodeJS.ReadableStream, outStream: NodeJS.WritableStream): void { - this._sequence = 1; - this._writableStream = outStream; - this._newRes(null); - - inStream.setEncoding('utf8'); - - inStream.on('data', (data) => this.execute(data)); - inStream.on('close', () => { - this.emitEvent(new NodeV8Event('close')); - }); - inStream.on('error', (error) => { - this.emitEvent(new NodeV8Event('error')); - }); - - outStream.on('error', (error) => { - this.emitEvent(new NodeV8Event('error')); - }); - - inStream.resume(); - } - - public stop(): void { - if (this._writableStream) { - this._writableStream.end(); - } - } - - public command(command: string, args?: any, cb?: (response: NodeV8Response) => void): void { - - const timeout = NodeV8Protocol.TIMEOUT; - - const request: any = { - command: command - }; - if (args && Object.keys(args).length > 0) { - request.arguments = args; - } - - if (this._unresponsiveMode) { - if (cb) { - cb(new NodeV8Response(request, 'cancelled because node is unresponsive')); - } - return; - } - - this.send('request', request); - - if (cb) { - this._pendingRequests[request.seq] = cb; - - const timer = setTimeout(() => { - clearTimeout(timer); - const clb = this._pendingRequests[request.seq]; - if (clb) { - delete this._pendingRequests[request.seq]; - clb(new NodeV8Response(request, 'timeout after ' + timeout + 'ms')); - - this._unresponsiveMode = true; - this.emitEvent(new NodeV8Event('diagnostic', { reason: 'unresponsive ' + command })); - } - }, timeout); - } - } - - public command2(command: string, args: any, timeout: number = NodeV8Protocol.TIMEOUT): Promise { - return new Promise((completeDispatch, errorDispatch) => { - this.command(command, args, (result: NodeV8Response) => { - if (result.success) { - completeDispatch(result); - } else { - errorDispatch(result); - } - }); - }); - } - - public sendEvent(event: NodeV8Event): void { - this.send('event', event); - } - - public sendResponse(response: NodeV8Response): void { - if (response.seq > 0) { - console.error('attempt to send more than one response for command {0}', response.command); - } else { - this.send('response', response); - } - } - - // ---- private ------------------------------------------------------------ - - private emitEvent(event: NodeV8Event) { - this.emit(event.event, event); - } - - private send(typ: string, message: NodeV8Message): void { - message.type = typ; - message.seq = this._sequence++; - const json = JSON.stringify(message); - const data = 'Content-Length: ' + Buffer.byteLength(json, 'utf8') + '\r\n\r\n' + json; - if (this._writableStream) { - this._writableStream.write(data); - } - } - - private _newRes(raw: string): void { - this._res = { - raw: raw || '', - headers: {} - }; - this._state = 'headers'; - this.execute(''); - } - - private internalDispatch(message: NodeV8Message): void { - switch (message.type) { - case 'event': - const e = message; - this.emitEvent(e); - break; - case 'response': - if (this._unresponsiveMode) { - this._unresponsiveMode = false; - this.emitEvent(new NodeV8Event('diagnostic', { reason: 'responsive' })); - } - const response = message; - const clb = this._pendingRequests[response.request_seq]; - if (clb) { - delete this._pendingRequests[response.request_seq]; - clb(response); - } - break; - default: - break; - } - } - - private execute(d): void { - const res = this._res; - res.raw += d; - - switch (this._state) { - case 'headers': - const endHeaderIndex = res.raw.indexOf('\r\n\r\n'); - if (endHeaderIndex < 0) - break; - - const rawHeader = res.raw.slice(0, endHeaderIndex); - const endHeaderByteIndex = Buffer.byteLength(rawHeader, 'utf8'); - const lines = rawHeader.split('\r\n'); - for (let i = 0; i < lines.length; i++) { - const kv = lines[i].split(/: +/); - res.headers[kv[0]] = kv[1]; - if (kv[0] === 'Embedding-Host') { - const match = kv[1].match(/node\sv(\d+)\.\d+\.\d+/) - if (match && match.length === 2) { - this.embeddedHostVersion = parseInt(match[1]); - } else if (kv[1] === 'Electron') { - this.embeddedHostVersion = 4; - } - } - } - - this._contentLength = +res.headers['Content-Length']; - this._bodyStartByteIndex = endHeaderByteIndex + 4; - - this._state = 'body'; - - const len = Buffer.byteLength(res.raw, 'utf8'); - if (len - this._bodyStartByteIndex < this._contentLength) { - break; - } - // pass thru - - case 'body': - const resRawByteLength = Buffer.byteLength(res.raw, 'utf8'); - if (resRawByteLength - this._bodyStartByteIndex >= this._contentLength) { - const buf = new Buffer(resRawByteLength); - buf.write(res.raw, 0, resRawByteLength, 'utf8'); - res.body = buf.slice(this._bodyStartByteIndex, this._bodyStartByteIndex + this._contentLength).toString('utf8'); - res.body = res.body.length ? JSON.parse(res.body) : {}; - this.internalDispatch(res.body); - this._newRes(buf.slice(this._bodyStartByteIndex + this._contentLength).toString('utf8')); - } - break; - - default: - throw new Error('Unknown state'); - break; - } - } -} diff --git a/node/pathUtilities.ts b/node/pathUtilities.ts deleted file mode 100644 index 54f4e199..00000000 --- a/node/pathUtilities.ts +++ /dev/null @@ -1,58 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as Path from 'path'; -import * as URL from 'url'; - - -export function getPathRoot(p: string) { - if (p) { - if (p.length >= 3 && p[1] === ':' && p[2] === '\\' && ((p[0] >= 'a' && p[0] <= 'z') || (p[0] >= 'A' && p[0] <= 'Z'))) { - return p.substr(0, 3); - } - if (p.length > 0 && p[0] === '/') { - return '/'; - } - } - return null; -} - -export function makePathAbsolute(absPath: string, relPath: string): string { - return Path.resolve(Path.dirname(absPath), relPath); -} - -export function removeFirstSegment(path: string) { - const segments = path.split(Path.sep); - segments.shift(); - if (segments.length > 0) { - return segments.join(Path.sep); - } - return null; -} - -export function makeRelative(target: string, path: string) { - const t = target.split(Path.sep); - const p = path.split(Path.sep); - - let i = 0; - for (; i < Math.min(t.length, p.length) && t[i] === p[i]; i++) { - } - - let result = ''; - for (; i < p.length; i++) { - result = Path.join(result, p[i]); - } - return result; -} - -export function canonicalizeUrl(url: string): string { - let u = URL.parse(url); - let p = u.pathname; - - if (p.length >= 4 && p[0] === '/' && p[2] === ':' && p[3] === '/' && ((p[1] >= 'a' && p[1] <= 'z') || (p[1] >= 'A' && p[1] <= 'Z'))) { - return p.substr(1); - } - return p; -} diff --git a/node/sourceMaps.ts b/node/sourceMaps.ts deleted file mode 100644 index 1c47e745..00000000 --- a/node/sourceMaps.ts +++ /dev/null @@ -1,337 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as Path from 'path'; -import * as FS from 'fs'; -import {SourceMapConsumer} from 'source-map'; -import * as PathUtils from './pathUtilities'; - - -export interface MappingResult { - path: string; - line: number; - column: number; -} - -export interface ISourceMaps { - /* - * Map source language path to generated path. - * Returns null if not found. - */ - MapPathFromSource(path: string): string; - - /* - * Map location in source language to location in generated code. - * line and column are 0 based. - */ - MapFromSource(path: string, line: number, column: number): MappingResult; - - /* - * Map location in generated code to location in source language. - * line and column are 0 based. - */ - MapToSource(path: string, line: number, column: number): MappingResult; -} - - -export class SourceMaps implements ISourceMaps { - - public static TRACE = false; - - private static SOURCE_MAPPING_MATCHER = new RegExp("//[#@] ?sourceMappingURL=(.+)$"); - - private _generatedToSourceMaps: { [id: string] : SourceMap; } = {}; // generated -> source file - private _sourceToGeneratedMaps: { [id: string] : SourceMap; } = {}; // source file -> generated - private _generatedCodeDirectory: string; - - - public constructor(generatedCodeDirectory: string) { - this._generatedCodeDirectory = generatedCodeDirectory; - } - - public MapPathFromSource(pathToSource: string): string { - var map = this._findSourceToGeneratedMapping(pathToSource); - if (map) - return map.generatedPath(); - return null;; - } - - public MapFromSource(pathToSource: string, line: number, column: number): MappingResult { - const map = this._findSourceToGeneratedMapping(pathToSource); - if (map) { - line += 1; // source map impl is 1 based - const mr = map.generatedPositionFor(pathToSource, line, column); - if (typeof mr.line === 'number') { - if (SourceMaps.TRACE) console.error(`${Path.basename(pathToSource)} ${line}:${column} -> ${mr.line}:${mr.column}`); - return { path: map.generatedPath(), line: mr.line-1, column: mr.column}; - } - } - return null; - } - - public MapToSource(pathToGenerated: string, line: number, column: number): MappingResult { - const map = this._findGeneratedToSourceMapping(pathToGenerated); - if (map) { - line += 1; // source map impl is 1 based - const mr = map.originalPositionFor(line, column); - if (mr.source) { - if (SourceMaps.TRACE) console.error(`${Path.basename(pathToGenerated)} ${line}:${column} -> ${mr.line}:${mr.column}`); - return { path: mr.source, line: mr.line-1, column: mr.column}; - } - } - return null; - } - - //---- private ----------------------------------------------------------------------- - - private _findSourceToGeneratedMapping(pathToSource: string): SourceMap { - - if (pathToSource) { - - if (pathToSource in this._sourceToGeneratedMaps) { - return this._sourceToGeneratedMaps[pathToSource]; - } - - for (let key in this._generatedToSourceMaps) { - const m = this._generatedToSourceMaps[key]; - if (m.doesOriginateFrom(pathToSource)) { - this._sourceToGeneratedMaps[pathToSource] = m; - return m; - } - } - // not found in existing maps - - // use heuristic: change extension to ".js" and find a map for it - let pathToGenerated = pathToSource; - const pos = pathToSource.lastIndexOf('.'); - if (pos >= 0) { - pathToGenerated = pathToSource.substr(0, pos) + '.js'; - } - - let map = null; - - // first look into the generated code directory - if (this._generatedCodeDirectory) { - let rest = PathUtils.makeRelative(this._generatedCodeDirectory, pathToGenerated); - while (rest) { - const path = Path.join(this._generatedCodeDirectory, rest); - map = this._findGeneratedToSourceMapping(path); - if (map) { - break; - } - rest = PathUtils.removeFirstSegment(rest) - } - } - - // VSCode extension host support: - // we know that the plugin has an "out" directory next to the "src" directory - if (map === null) { - let srcSegment = Path.sep + 'src' + Path.sep; - if (pathToGenerated.indexOf(srcSegment) >= 0) { - let outSegment = Path.sep + 'out' + Path.sep; - pathToGenerated = pathToGenerated.replace(srcSegment, outSegment); - map = this._findGeneratedToSourceMapping(pathToGenerated); - } - } - - // if not found look in the same directory as the source - if (map === null && pathToGenerated !== pathToSource) { - map = this._findGeneratedToSourceMapping(pathToGenerated); - } - - if (map) { - this._sourceToGeneratedMaps[pathToSource] = map; - return map; - } - } - return null; - } - - private _findGeneratedToSourceMapping(pathToGenerated: string): SourceMap { - - if (pathToGenerated) { - - if (pathToGenerated in this._generatedToSourceMaps) { - return this._generatedToSourceMaps[pathToGenerated]; - } - - let map: SourceMap = null; - - // try to find a source map URL in the generated source - let map_path: string = null; - const uri = this._findSourceMapInGeneratedSource(pathToGenerated); - if (uri) { - if (uri.indexOf("data:application/json;base64,") >= 0) { - const pos = uri.indexOf(','); - if (pos > 0) { - const data = uri.substr(pos+1); - try { - const buffer = new Buffer(data, 'base64'); - const json = buffer.toString(); - if (json) { - map = new SourceMap(pathToGenerated, json); - this._generatedToSourceMaps[pathToGenerated] = map; - return map; - } - } - catch (e) { - console.error(`FindGeneratedToSourceMapping: exception while processing data url (${e})`); - } - } - } else { - map_path = uri; - } - } - - // if path is relative make it absolute - if (map_path && !Path.isAbsolute(map_path)) { - map_path = PathUtils.makePathAbsolute(pathToGenerated, map_path); - } - - if (map_path === null || !FS.existsSync(map_path)) { - // try to find map file next to the generated source - map_path = pathToGenerated + ".map"; - } - - if (FS.existsSync(map_path)) { - map = this._createSourceMap(map_path, pathToGenerated); - if (map) { - this._generatedToSourceMaps[pathToGenerated] = map; - return map; - } - } - } - return null; - } - - private _createSourceMap(map_path: string, path: string): SourceMap { - try { - const contents = FS.readFileSync(Path.join(map_path)).toString(); - return new SourceMap(path, contents); - } - catch (e) { - console.error(`CreateSourceMap: {e}`); - } - return null; - } - - // find "//# sourceMappingURL=" - private _findSourceMapInGeneratedSource(pathToGenerated: string): string { - - try { - const contents = FS.readFileSync(pathToGenerated).toString(); - const lines = contents.split('\n'); - for (let line of lines) { - const matches = SourceMaps.SOURCE_MAPPING_MATCHER.exec(line); - if (matches && matches.length === 2) { - const uri = matches[1].trim(); - return uri; - } - } - } catch (e) { - // ignore exception - } - return null; - } -} - -enum Bias { - GREATEST_LOWER_BOUND = 1, - LEAST_UPPER_BOUND = 2 -} - -class SourceMap { - - private _generatedFile: string; // the generated file for this sourcemap - private _sources: string[]; // the sources of generated file (relative to sourceRoot) - private _sourceRoot: string; // the common prefix for the source (can be a URL) - private _smc: SourceMapConsumer; // the source map - - - public constructor(generatedPath: string, json: string) { - - this._generatedFile = generatedPath; - - const sm = JSON.parse(json); - this._sources = sm.sources; - let sr = sm.sourceRoot; - if (sr) { - sr = PathUtils.canonicalizeUrl(sr); - this._sourceRoot = PathUtils.makePathAbsolute(generatedPath, sr); - } else { - this._sourceRoot = Path.dirname(generatedPath); - } - - if (this._sourceRoot[this._sourceRoot.length-1] !== Path.sep) { - this._sourceRoot += Path.sep; - } - - this._smc = new SourceMapConsumer(sm); - } - - /* - * the generated file of this source map. - */ - public generatedPath(): string { - return this._generatedFile; - } - - /* - * returns true if this source map originates from the given source. - */ - public doesOriginateFrom(absPath: string): boolean { - for (let name of this._sources) { - const p = Path.join(this._sourceRoot, name); - if (p === absPath) { - return true; - } - } - return false; - } - - /* - * finds the nearest source location for the given location in the generated file. - */ - public originalPositionFor(line: number, column: number, bias: Bias = Bias.GREATEST_LOWER_BOUND): SourceMap.MappedPosition { - - const mp = this._smc.originalPositionFor({ - line: line, - column: column, - bias: bias - }); - - if (mp.source) { - mp.source = PathUtils.canonicalizeUrl(mp.source); - mp.source = PathUtils.makePathAbsolute(this._generatedFile, mp.source); - } - - return mp; - } - - /* - * finds the nearest location in the generated file for the given source location. - */ - public generatedPositionFor(src: string, line: number, column: number, bias = Bias.GREATEST_LOWER_BOUND): SourceMap.Position { - - // make input path relative to sourceRoot - if (this._sourceRoot) { - src = Path.relative(this._sourceRoot, src); - } - - // source-maps always use forward slashes - if (process.platform === 'win32') { - src = src.replace(/\\/g, '/'); - } - - const needle = { - source: src, - line: line, - column: column, - bias: bias - }; - - return this._smc.generatedPositionFor(needle); - } -} diff --git a/node/terminal.ts b/node/terminal.ts deleted file mode 100644 index 6ef7eded..00000000 --- a/node/terminal.ts +++ /dev/null @@ -1,258 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as Path from 'path'; -import * as FS from 'fs'; -import * as CP from 'child_process'; - - -export class Terminal -{ - private static _terminalService: ITerminalService; - - public static launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - return this.terminalService().launchInTerminal(dir, args, envVars); - } - - public static killTree(processId: number): Promise { - return this.terminalService().killTree(processId); - } - - /* - * Is the given runtime executable on the PATH. - */ - public static isOnPath(program: string): boolean { - return this.terminalService().isOnPath(program); - } - - - private static terminalService(): ITerminalService { - if (!this._terminalService) { - if (process.platform === 'win32') - this._terminalService = new WindowsTerminalService(); - else if (process.platform === 'darwin') - this._terminalService = new MacTerminalService(); - else if (process.platform === 'linux') - this._terminalService = new LinuxTerminalService(); - else { - this._terminalService = new DefaultTerminalService(); - } - } - return this._terminalService; - } -} - -interface ITerminalService { - launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise; - killTree(pid: number) : Promise; - isOnPath(program: string): boolean; -} - -class DefaultTerminalService implements ITerminalService { - - protected static TERMINAL_TITLE = "VS Code Console"; - private static WHICH = '/usr/bin/which'; - private static WHERE = 'where'; - - public launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - throw new Error('launchInTerminal not implemented'); - } - - public killTree(pid: number): Promise { - - // on linux and OS X we kill all direct and indirect child processes as well - - return new Promise( (resolve, reject) => { - try { - const cmd = Path.join(__dirname, './terminateProcess.sh'); - const result = (CP).spawnSync(cmd, [ pid.toString() ]); - if (result.error) { - reject(result.error); - } else { - resolve(); - } - } catch (err) { - reject(err); - } - }); - } - - public isOnPath(program: string): boolean { - /* - var which = FS.existsSync(DefaultTerminalService.WHICH) ? DefaultTerminalService.WHICH : DefaultTerminalService.WHERE; - var cmd = Utils.format('{0} \'{1}\'', which, program); - - try { - CP.execSync(cmd); - - return process.ExitCode == 0; - } - catch (Exception) { - // ignore - } - - return false; - */ - - return true; - } -} - -class WindowsTerminalService extends DefaultTerminalService { - - private static CMD = 'cmd.exe'; - - public launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - - return new Promise( (resolve, reject) => { - - const title = `"${dir} - ${WindowsTerminalService.TERMINAL_TITLE}"`; - const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code - - const cmdArgs = [ - '/c', 'start', title, '/wait', - 'cmd.exe', '/c', command - ]; - - // merge environment variables into a copy of the process.env - const env = extendObject(extendObject( { }, process.env), envVars); - - const options: any = { - cwd: dir, - env: env, - windowsVerbatimArguments: true - }; - - const cmd = CP.spawn(WindowsTerminalService.CMD, cmdArgs, options); - cmd.on('error', reject); - - resolve(cmd); - }); - } - - public killTree(pid: number): Promise { - - // when killing a process in Windows its child processes are *not* killed but become root processes. - // Therefore we use TASKKILL.EXE - - return new Promise( (resolve, reject) => { - const cmd = `taskkill /F /T /PID ${pid}`; - CP.exec(cmd, (err, stdout, stderr) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } -} - -class LinuxTerminalService extends DefaultTerminalService { - - private static LINUX_TERM = '/usr/bin/gnome-terminal'; //private const string LINUX_TERM = "/usr/bin/x-terminal-emulator"; - private static WAIT_MESSAGE = "Press any key to continue..."; - - public launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - - return new Promise( (resolve, reject) => { - - if (!FS.existsSync(LinuxTerminalService.LINUX_TERM)) { - reject(new Error(`Cannot find '${LinuxTerminalService.LINUX_TERM}' for launching the node program. See http://go.microsoft.com/fwlink/?linkID=534832#_20002`)); - return; - } - - const bashCommand = `cd "${dir}"; "${args.join('" "')}"; echo; read -p "${LinuxTerminalService.WAIT_MESSAGE}" -n1;`; - - const termArgs = [ - '--title', `"${LinuxTerminalService.TERMINAL_TITLE}"`, - '-x', 'bash', '-c', - `\'\'${bashCommand}\'\'` // wrapping argument in two sets of ' because node is so "friendly" that it removes one set... - ]; - - // merge environment variables into a copy of the process.env - const env = extendObject(extendObject( { }, process.env), envVars); - - const options: any = { - env: env - }; - - const cmd = CP.spawn(LinuxTerminalService.LINUX_TERM, termArgs, options); - cmd.on('error', reject); - cmd.on('exit', (code: number) => { - if (code === 0) { // OK - resolve(); // since cmd is not the terminal process but just a launcher, we do not pass it in the resolve to the caller - } else { - reject(new Error("exit code: " + code)); - } - }); - }); - } -} - -class MacTerminalService extends DefaultTerminalService { - - private static OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X - - public launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - - return new Promise( (resolve, reject) => { - - // first fix the PATH so that 'runtimePath' can be found if installed with 'brew' - // Utilities.FixPathOnOSX(); - - // On OS X we do not launch the program directly but we launch an AppleScript that creates (or reuses) a Terminal window - // and then launches the program inside that window. - - const osaArgs = [ - Path.join(__dirname, './terminalHelper.scpt'), - '-t', MacTerminalService.TERMINAL_TITLE, - '-w', dir, - ]; - - for (var a of args) { - osaArgs.push('-pa'); - osaArgs.push(a); - } - - if (envVars) { - for (var key in envVars) { - osaArgs.push('-e'); - osaArgs.push(key + '=' + envVars[key]); - } - } - - var stderr = ''; - const osa = CP.spawn(MacTerminalService.OSASCRIPT, osaArgs); - osa.on('error', reject); - osa.stderr.on('data', (data) => { - stderr += data.toString(); - }); - osa.on('exit', (code: number) => { - if (code === 0) { // OK - resolve(); // since cmd is not the terminal process but just the osa tool, we do not pass it in the resolve to the caller - } else { - if (stderr) - reject(new Error(stderr)); - else - reject(new Error("exit code: " + code)); - } - }); - }); - } -} - -// ---- private utilities ---- - -function extendObject (objectCopy: T, object: T): T { - - for (let key in object) { - if (object.hasOwnProperty(key)) { - objectCopy[key] = object[key]; - } - } - - return objectCopy; -} diff --git a/package.json b/package.json index b0b3f207..8bd0004a 100644 --- a/package.json +++ b/package.json @@ -1,191 +1,500 @@ { - "name": "node-debug", - "version": "0.10.1", - "publisher": "isidor", - "description": "Visual Studio Code debug adapter for node.js", - "author": { - "name": "Microsoft Corporation" - }, - "private": true, - "engines": { - "node": ">= 0.12.0", - "vscode": "^0.10.1" - }, - "dependencies": { - "source-map": "*" - }, - "repository": { - "type": "git", - "url": "https://monacotools.visualstudio.com/DefaultCollection/Monaco/_git/vscode-node-debug" - }, - "devDependencies": { - "gulp": "^3.9.0", - "gulp-util": "^3.0.5", - "gulp-tsb": "git://github.com/jrieken/gulp-tsb.git#bbf53f1d4b503b9080f8c300208b3ffa829a12c7", - "gulp-azure-storage": "*", - "git-rev-sync": "*", - "del": "*", - "run-sequence": "*", - "gulp-vinyl-zip": "*" - }, - "contributes": { + "name": "node-debug", + "displayName": "[Deprecated] Node Debug (legacy)", + "version": "1.45.0", + "publisher": "ms-vscode", + "description": "%extension.description%", + "icon": "images/node-debug-icon.png", + "categories": [ + "Debuggers" + ], + "author": { + "name": "Microsoft Corporation" + }, + "license": "MIT", + "private": true, + "scripts": { + "build": "gulp build", + "watch": "gulp watch", + "test": "gulp compile && mocha --timeout 10000 -u tdd ./out/tests/", + "nodemon": "nodemon --debug --nolazy ./dist/nodeDebug.js --server=4711", + "prepublish": "gulp build", + "vscode:prepublish": "gulp prepare-for-webpack && webpack --mode production --vscode-nls", + "package": "gulp prepare-for-webpack && webpack --mode production --vscode-nls && vsce package --yarn", + "publish": "gulp prepare-for-webpack && webpack --mode production --vscode-nls && vsce publish", + "bump": "npm version patch -m 'bump to %s'", + "tslint": "gulp tslint", + "translations-export": "gulp translations-export" + }, + "keywords": [ + "multi-root ready" + ], + "engines": { + "vscode": "^1.60.0-insider", + "node": ">=10" + }, + "extensionDependencies": [ + "ms-vscode.node-debug2" + ], + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-node-debug.git" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-node-debug/issues" + }, + "dependencies": { + "glob": "7.1.6", + "minimatch": "3.0.4", + "request-light": "0.4.0", + "source-map": "0.6.1", + "vscode-debugadapter": "1.48.0", + "vscode-nls": "5.0.0" + }, + "devDependencies": { + "@types/mocha": "8.2.0", + "@types/node": "^12.7.3", + "@types/vscode": "^1.42.0", + "copy-webpack-plugin": "^6.4.1", + "del": "5.1.0", + "gulp": "4.0.2", + "gulp-filter": "6.0.0", + "gulp-sourcemaps": "2.6.5", + "gulp-tsb": "4.0.5", + "gulp-tslint": "8.1.4", + "gulp-typescript": "5.0.1", + "gulp-uglify": "3.0.2", + "mocha": "8.2.1", + "ts-loader": "~6.2.1", + "tslint": "6.0.0", + "tslint-microsoft-contrib": "6.2.0", + "tsutils": "3.17.1", + "typescript": "~3.7.2", + "vsce": "^1.88.0", + "vscode-debugadapter-testsupport": "1.48.0", + "vscode-debugprotocol": "1.48.0", + "vscode-nls-dev": "3.3.1", + "webpack": "^4.44.1", + "webpack-cli": "~3.3.12", + "yargs-parser": "5.0.0-security.0" + }, + "resolutions": { + "gulp/**/yargs-parser": "^5.0.0-security.0" + }, + "main": "./dist/extension.js", + "activationEvents": [ + "onDebugResolve:legacy-node", + "onCommand:extension.pickNodeProcess", + "onCommand:extension.node-debug.toggleSkippingFile", + "onCommand:extension.node-debug.attachNodeProcess", + "onCommand:extension.node-debug.startAutoAttach" + ], + "capabilities": { + "virtualWorkspaces": false, + "untrustedWorkspaces": { + "supported": true + } + }, + "contributes": { + "configuration": { + "title": "Node debug", + "properties": { + "debug.node.showUseWslIsDeprecatedWarning": { + "scope": "window", + "type": "boolean", + "description": "%debug.node.showUseWslIsDeprecatedWarning.description%", + "default": true + } + } + }, + "menus": { + "debug/callstack/context": [ + { + "command": "extension.node-debug.toggleSkippingFile", + "group": "navigation", + "when": "inDebugMode && debugType == 'legacy-node' && callStackItemType == 'stackFrame'" + } + ] + }, + "commands": [ + { + "command": "extension.node-debug.attachNodeProcess", + "title": "%attach.node.process%", + "category": "Debug" + }, + { + "command": "extension.node-debug.toggleSkippingFile", + "title": "%toggle.skipping.this.file%", + "category": "Debug" + } + ], + "breakpoints": [ + { + "language": "javascript" + }, + { + "language": "javascriptreact" + } + ], "debuggers": [ { - "type": "node", - "label": "Node.js", - "enableBreakpointsFor": { "languageIds": ["javascript"] }, - "program": "./out/node/nodeDebug.js", + "type": "legacy-node", + "label": "%node.label%", + "program": "./dist/nodeDebug.js", "runtime": "node", - "initialConfigurations": [ - { - "name": "Launch", - "type": "node", - "request": "launch", - "program": "app.js", - "stopOnEntry": false, - "args": [], - "cwd": ".", - "runtimeExecutable": null, - "runtimeArgs": ["--nolazy"], - "env": { - "NODE_ENV": "development" - }, - "externalConsole": false, - "sourceMaps": false, - "outDir": null - }, - { - "name": "Attach", - "type": "node", - "request": "attach", - "port": 5858 - } + "variables": { + "PickProcess": "extension.pickNodeProcess" + }, + "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", + "languages": [ + "javascript", + "typescript", + "javascriptreact", + "typescriptreact" ], "configurationAttributes": { "launch": { - "required": ["program"], "properties": { + "protocol": { + "type": "string", + "enum": [ + "auto", + "inspector", + "legacy" + ], + "enumDescriptions": [ + "%node.protocol.auto.description%", + "%node.protocol.inspector.description%", + "%node.protocol.legacy.description%" + ], + "description": "%node.protocol.description%", + "default": "inspector" + }, "program": { "type": "string", - "description": "Workspace relative path to the program." + "description": "%node.launch.program.description%" }, "stopOnEntry": { "type": "boolean", - "description": "Automatically stop program after launch.", + "description": "%node.stopOnEntry.description%", "default": true }, "externalConsole": { "type": "boolean", - "description": "Launch debug target in external console.", + "deprecationMessage": "%node.launch.externalConsole.deprecationMessage%", "default": true }, + "console": { + "type": "string", + "enum": [ + "internalConsole", + "integratedTerminal", + "externalTerminal" + ], + "enumDescriptions": [ + "%node.launch.console.internalConsole.description%", + "%node.launch.console.integratedTerminal.description%", + "%node.launch.console.externalTerminal.description%" + ], + "description": "%node.launch.console.description%", + "default": "internalConsole" + }, "args": { "type": "array", - "description": "Command line arguments passed to the program.", - "items": { "type": "string" }, + "description": "%launch.args.description%", + "items": { + "type": "string" + }, "default": [] }, "cwd": { "type": "string", - "description": "Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace.", - "default": "." + "description": "%node.launch.cwd.description%", + "default": "${workspaceFolder}" }, "runtimeExecutable": { - "type": ["string", "null"], - "description": "Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH.", - "default": null + "type": [ + "string", + "null" + ], + "markdownDescription": "%node.launch.runtimeExecutable.description%", + "default": "node" + }, + "runtimeVersion": { + "type": "string", + "markdownDescription": "%node.launch.runtimeVersion.description%", + "default": "default" }, "runtimeArgs": { "type": "array", - "description": "Optional arguments passed to the runtime executable.", - "items": { "type": "string" }, + "description": "%node.launch.runtimeArgs.description%", + "items": { + "type": "string" + }, "default": [] }, "env": { "type": "object", - "description": "Environment variables passed to the program.", + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "markdownDescription": "%node.launch.env.description%", "default": {} }, + "envFile": { + "type": "string", + "description": "%node.launch.envFile.description%", + "default": "${workspaceFolder}/.env" + }, "sourceMaps": { "type": "boolean", - "description": "Use JavaScript source maps (if they exist).", + "description": "%node.sourceMaps.description%", "default": true }, "outDir": { - "type": ["string", "null"], - "description": "If source maps are enabled, the generated code is expected in this directory. If not specified, the generated code is expected in the same directory as its source.", + "type": [ + "string", + "null" + ], + "deprecationMessage": "%outDir.deprecationMessage%", "default": null + }, + "outFiles": { + "type": "array", + "markdownDescription": "%outFiles.description%", + "items": { + "type": "string" + }, + "default": [] + }, + "port": { + "type": "number", + "description": "%node.port.description%", + "default": 9229 + }, + "address": { + "type": "string", + "description": "%node.address.description%", + "default": "localhost" + }, + "timeout": { + "type": "number", + "description": "%node.timeout.description%", + "default": 10000 + }, + "restart": { + "type": "boolean", + "description": "%node.restart.description%", + "default": true + }, + "localRoot": { + "type": [ + "string", + "null" + ], + "description": "%node.localRoot.description%", + "default": null + }, + "remoteRoot": { + "type": [ + "string", + "null" + ], + "description": "%node.remoteRoot.description%", + "default": null + }, + "smartStep": { + "type": "boolean", + "description": "%smartStep.description%", + "default": true + }, + "skipFiles": { + "type": "array", + "markdownDescription": "%skipFiles.description%", + "items": { + "type": "string" + }, + "default": [] + }, + "showAsyncStacks": { + "type": "boolean", + "description": "%node.showAsyncStacks.description%", + "default": true + }, + "useWSL": { + "type": "boolean", + "description": "%node.launch.useWSL.description%", + "default": true, + "deprecationMessage": "%node.launch.useWSL.deprecation%" + }, + "trace": { + "type": [ + "boolean", + "string" + ], + "description": "%trace.description%", + "default": true + }, + "outputCapture": { + "enum": [ + "console", + "std" + ], + "description": "%node.launch.outputCapture.description%", + "default": "console" + }, + "sourceMapPathOverrides": { + "type": "object", + "description": "%node.sourceMapPathOverrides.description%", + "default": { + "webpack:///./~/*": "${workspaceRoot}/node_modules/*", + "webpack:///./*": "${workspaceRoot}/*", + "webpack:///*": "*" + } + }, + "autoAttachChildProcesses": { + "type": "boolean", + "description": "%node.launch.autoAttachChildProcesses.description%", + "default": true + }, + "disableOptimisticBPs": { + "type": "boolean", + "description": "%node.disableOptimisticBPs.description%", + "default": true } } }, "attach": { - "required": ["port"], "properties": { + "protocol": { + "type": "string", + "enum": [ + "auto", + "inspector", + "legacy" + ], + "enumDescriptions": [ + "%node.protocol.auto.description%", + "%node.protocol.inspector.description%", + "%node.protocol.legacy.description%" + ], + "description": "%node.protocol.description%", + "default": "inspector" + }, + "cwd": { + "type": "string", + "description": "%node.launch.cwd.description%", + "default": "${workspaceFolder}" + }, + "processId": { + "type": "string", + "description": "%node.attach.processId.description%", + "default": "${command:PickProcess}" + }, "port": { "type": "number", - "description": "Port to attach to.", - "default": "undefined" + "description": "%node.port.description%", + "default": 9229 + }, + "address": { + "type": "string", + "description": "%node.address.description%", + "default": "localhost" + }, + "timeout": { + "type": "number", + "description": "%node.timeout.description%", + "default": 10000 + }, + "restart": { + "type": "boolean", + "description": "%node.restart.description%", + "default": true }, "sourceMaps": { "type": "boolean", - "description": "Use JavaScript source maps (if they exist).", + "description": "%node.sourceMaps.description%", "default": true }, "outDir": { - "type": ["string", "null"], - "description": "If source maps are enabled, the generated code is expected in this directory. If not specified, the generated code is expected in the same directory as its source.", + "type": [ + "string", + "null" + ], + "deprecationMessage": "%outDir.deprecationMessage%", "default": null - } - } - } - } - }, - { - "type": "extensionHost", - "label": "Extension Development", - "enableBreakpointsFor": { "languageIds": ["javascript"] }, - "program": "./out/node/nodeDebug.js", - "runtime": "node", - "initialConfigurations": [{ - "name": "Launch Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ "--extensionDevelopmentPath=${workspaceRoot}" ], - "stopOnEntry": false, - "sourceMaps": true, - "outDir": "out", - "preLaunchTask": "npm" - }], - "configurationAttributes": { - "launch": { - "required": ["runtimeExecutable", "args"], - "properties": { - "runtimeExecutable": { - "type": ["string", "null"], - "description": "Absolute path to VS Code.", - "default": "${execPath}" }, - "args": { + "outFiles": { "type": "array", - "description": "Command line arguments passed to VS Code.", - "items": { "type": "string" }, - "default": [ "--extensionDevelopmentPath=${workspaceRoot}" ] + "markdownDescription": "%outFiles.description%", + "items": { + "type": "string" + }, + "default": [] }, "stopOnEntry": { "type": "boolean", - "description": "Automatically stop the extension host after launch.", + "description": "%node.stopOnEntry.description%", "default": true }, - "sourceMaps": { + "localRoot": { + "type": [ + "string", + "null" + ], + "description": "%node.localRoot.description%", + "default": null + }, + "remoteRoot": { + "type": [ + "string", + "null" + ], + "description": "%node.remoteRoot.description%", + "default": null + }, + "smartStep": { "type": "boolean", - "description": "Use JavaScript source maps.", + "description": "%smartStep.description%", "default": true }, - "outDir": { - "type": ["string", "null"], - "description": "If source maps are enabled, the generated code is expected in this directory. If not specified, the generated code is expected in the same directory as its source.", - "default": "out" + "skipFiles": { + "type": "array", + "markdownDescription": "%skipFiles.description%", + "items": { + "type": "string" + }, + "default": [] + }, + "showAsyncStacks": { + "type": "boolean", + "description": "%node.showAsyncStacks.description%", + "default": true + }, + "trace": { + "type": [ + "boolean", + "string" + ], + "description": "%trace.description%", + "default": true + }, + "sourceMapPathOverrides": { + "type": "object", + "description": "%node.sourceMapPathOverrides.description%", + "default": { + "webpack:///./~/*": "${workspaceRoot}/node_modules/*", + "webpack:///./*": "${workspaceRoot}/*", + "webpack:///*": "*" + } + }, + "disableOptimisticBPs": { + "type": "boolean", + "description": "%node.disableOptimisticBPs.description%", + "default": true } } } diff --git a/package.nls.json b/package.nls.json new file mode 100644 index 00000000..eda0c413 --- /dev/null +++ b/package.nls.json @@ -0,0 +1,84 @@ +{ + "extension.description": "Node.js debugging support (versions < 8.0)", + + "node.label": "Node.js (legacy)", + + "open.loaded.script": "Open Loaded Script", + "attach.node.process": "Attach to Node Process (legacy)", + "toggle.skipping.this.file": "Toggle Skipping this File", + "start.with.stop.on.entry": "Start Debugging and Stop on Entry (legacy)", + + "smartStep.description": "Automatically step through generated code that cannot be mapped back to the original source.", + "skipFiles.description": "An array of glob patterns for files to skip when debugging. The pattern `/**` matches all internal Node.js modules.", + "outFiles.description": "If source maps are enabled, these glob patterns specify the generated JavaScript files. If a pattern starts with '!' the files are excluded. If not specified, the generated code is expected in the same directory as its source. Example: `[\"${workspaceFolder}/out/**/*.js\"]`", + "outDir.deprecationMessage": "Attribute 'outDir' is deprecated, use 'outFiles' instead.", + "trace.description": "Produce diagnostic output. Instead of setting this to true you can list one or more selectors separated with commas. The 'verbose' selector enables very detailed output.", + + "launch.args.description": "Command line arguments passed to the program.", + + "debug.node.showUseWslIsDeprecatedWarning.description": "Controls whether to show a warning when the 'useWSL' attribute is used.", + + "node.protocol.description": "Node.js debug protocol to use.", + "node.protocol.auto.description": "Try to detect the best protocol automatically, selecting 'inspector' for launching Node 8.0+", + "node.protocol.inspector.description": "New protocol supported by Node.js versions >= 6.3", + "node.protocol.legacy.description": "Old protocol supported by Node.js versions < 8.0", + + "node.sourceMaps.description": "Use JavaScript source maps (if they exist).", + "node.stopOnEntry.description": "Automatically stop program after launch.", + "node.port.description": "Debug port to attach to. Default is 5858.", + "node.address.description": "TCP/IP address of process to be debugged (for Node.js >= 5.0 only). Default is 'localhost'.", + "node.timeout.description": "Retry for this number of milliseconds to connect to Node.js. Default is 10000 ms.", + "node.restart.description": "Restart session after Node.js has terminated.", + "node.localRoot.description": "Path to the local directory containing the program.", + "node.remoteRoot.description": "Absolute path to the remote directory containing the program.", + "node.showAsyncStacks.description": "Show the async calls that led to the current call stack. 'inspector' protocol only.", + "node.sourceMapPathOverrides.description": "A set of mappings for rewriting the locations of source files from what the sourcemap says, to their locations on disk.", + "node.disableOptimisticBPs.description": "Don't set breakpoints in any file until a sourcemap has been loaded for that file.", + + "node.launch.program.description": "Absolute path to the program. Generated value is guessed by looking at package.json and opened files. Edit this attribute.", + "node.launch.externalConsole.deprecationMessage": "Attribute 'externalConsole' is deprecated, use 'console' instead.", + "node.launch.console.description": "Where to launch the debug target.", + "node.launch.console.internalConsole.description": "VS Code Debug Console (which doesn't support to read input from a program)", + "node.launch.console.integratedTerminal.description": "VS Code's integrated terminal", + "node.launch.console.externalTerminal.description": "External terminal that can be configured via user settings", + + "node.launch.cwd.description": "Absolute path to the working directory of the program being debugged.", + "node.launch.runtimeExecutable.description": "Runtime to use. Either an absolute path or the name of a runtime available on the PATH. If omitted `node` is assumed.", + "node.launch.runtimeArgs.description": "Optional arguments passed to the runtime executable.", + "node.launch.runtimeVersion.description": "Version of `node` runtime to use. Requires `nvm`.", + "node.launch.env.description": "Environment variables passed to the program. The value `null` removes the variable from the environment.", + "node.launch.envFile.description": "Absolute path to a file containing environment variable definitions.", + "node.launch.useWSL.description": "Use Windows Subsystem for Linux.", + "node.launch.useWSL.deprecation": "'useWSL' is deprecated and support for it will be dropped. Use the 'Remote - WSL' extension instead.", + "node.launch.outputCapture.description": "From where to capture output messages: The debug API, or stdout/stderr streams.", + "node.launch.autoAttachChildProcesses.description": "Attach debugger to new child processes automatically.", + + "node.launch.config.name": "Launch", + + "node.attach.processId.description": "ID of process to attach to.", + + "node.attach.config.name": "Attach", + + "node.processattach.config.name": "Attach to Process", + + "node.snippet.launch.label": "Node.js: Launch Program", + "node.snippet.launch.description": "Launch a node program in debug mode", + "node.snippet.npm.label": "Node.js: Launch via NPM", + "node.snippet.npm.description": "Launch a node program through an npm `debug` script", + "node.snippet.attach.label": "Node.js: Attach", + "node.snippet.attach.description": "Attach to a running node program", + "node.snippet.remoteattach.label": "Node.js: Attach to Remote Program", + "node.snippet.remoteattach.description": "Attach to the debug port of a remote node program", + "node.snippet.attachProcess.label": "Node.js: Attach to Process", + "node.snippet.attachProcess.description": "Open process picker to select node process to attach to", + "node.snippet.nodemon.label": "Node.js: Nodemon Setup", + "node.snippet.nodemon.description": "Use nodemon to relaunch a debug session on source changes", + "node.snippet.mocha.label": "Node.js: Mocha Tests", + "node.snippet.mocha.description": "Debug mocha tests", + "node.snippet.yo.label": "Node.js: Yeoman generator", + "node.snippet.yo.description": "Debug yeoman generator (install by running `npm link` in project folder)", + "node.snippet.gulp.label": "Node.js: Gulp task", + "node.snippet.gulp.description": "Debug gulp task (make sure to have a local gulp installed in your project)", + "node.snippet.electron.label": "Node.js: Electron Main", + "node.snippet.electron.description": "Debug the Electron main process" +} diff --git a/src/node/URI.ts b/src/node/URI.ts new file mode 100644 index 00000000..0096708e --- /dev/null +++ b/src/node/URI.ts @@ -0,0 +1,135 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as URL from 'url'; +import * as PathUtils from './pathUtilities'; + +function isWindows(absPath: string): boolean { + return /^[a-zA-Z]\:\\/.test(absPath); +} + +function stripFirst(path: string, c: string) { + return path[0] === c ? path.substr(1) : path; +} + +function stripLast(path: string, c: string) { + return path[path.length-1] === c ? path.substr(0, path.length-1) : path; +} + +export class URI { + private _uri: string; + private _u: URL.Url; + + /** + * Creates a file URI from the given file path. + * If path is relative, an absolute base path must be provided as well. + * If base is missing or if base is not absolute, an exception is thrown. + */ + public static file(path: string, base?: string) { + + if (typeof path !== 'string') { + throw new Error('string expected'); + } + + if (!PathUtils.isAbsolutePath(path)) { + if (base) { + + if (PathUtils.isAbsolutePath(base)) { + if (isWindows(base)) { + path = stripLast(base, '\\') + '\\' + stripFirst(path, '\\'); + } else { + path = stripLast(base, '/') + '/' + stripFirst(path, '/'); + } + } else { + throw new Error('base path not absolute'); + } + + //path = PathUtils.makePathAbsolute(base, path); + } else { + throw new Error('base path missing'); + } + } + + if (isWindows(path)) { // is windows + path = path.replace(/\\/g, '/'); + } + + // simplify '/./' -> '/' + path = path.replace(/\/\.\//g, '/'); + + if (path[0] !== '/') { + path = '/' + path; + } + + path = encodeURI('file://' + path); + + const u = new URI(); + u._uri = path; + try { + u._u = URL.parse(path); + } + catch (e) { + throw new Error(e); + } + return u; + } + + /** + * Creates a URI from the given string. + */ + public static parse(uri: string, base?: string) { + + if (uri.indexOf('http:') === 0 || uri.indexOf('https:') === 0 || uri.indexOf('file:') === 0 || uri.indexOf('data:') === 0 ) { + const u = new URI(); + u._uri = uri; + try { + u._u = URL.parse(uri); + } + catch (e) { + throw new Error(e); + } + return u; + } + return URI.file(uri, base); + } + + constructor() { + } + + uri(): string { + return this._uri; + } + + isFile(): boolean { + return this._u.protocol === 'file:'; + } + + filePath(): string { + let path = this._u.path; + path = decodeURI(path); + + if (/^\/[a-zA-Z]\:\//.test(path)) { + path = path.substr(1); // remove additional '/' + path = path.replace(/\//g, '\\'); // convert slashes to backslashes + } + return path; + } + + isData() { + return this._u.protocol === 'data:' && this._uri.indexOf('application/json') > 0 && this._uri.indexOf('base64') > 0; + } + + data(): string | null { + const pos = this._uri.lastIndexOf(','); + if (pos > 0) { + return this._uri.substr(pos+1); + } + return null; + } + + isHTTP(): boolean { + return this._u.protocol === 'http:' || this._u.protocol === 'https:'; + } +} diff --git a/src/node/debugInjection.js b/src/node/debugInjection.js new file mode 100644 index 00000000..41622a0c --- /dev/null +++ b/src/node/debugInjection.js @@ -0,0 +1,264 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +!function() { + + var CHUNK_SIZE = 100; // break large objects into chunks of this size + var INDEX_PATTERN = /^(0|[1-9][0-9]*)$/; + + // try to load 'vm' even if 'require' isn't available in the current context + var vm = process.mainModule ? process.mainModule.require('vm') : require('vm'); + + // the following objects should be available in all versions of node.js + var LookupMirror = vm.runInDebugContext('LookupMirror'); + var DebugCommandProcessor = vm.runInDebugContext('DebugCommandProcessor'); + + /* + * Retrieving index and named properties of an object requires some work in recent versions of node + * because 'propertyNames' no longer takes a filter argument. + */ + var indexedPropertyCount; + var namedPropertyCount; + var hasManyProperties; + var namedProperties; + try { + var PropertyKind = vm.runInDebugContext('PropertyKind'); + if (!PropertyKind) { + throw new Error("undef"); + } + indexedPropertyCount = function(mirror) { + return mirror.propertyNames(PropertyKind.Indexed).length; + }; + namedPropertyCount = function(mirror) { + return mirror.propertyNames(PropertyKind.Named).length; + }; + hasManyProperties = function(mirror, limit) { + return mirror.propertyNames(PropertyKind.Named | PropertyKind.Indexed, limit).length >= limit; + }; + namedProperties = function(mirror) { + return mirror.propertyNames(PropertyKind.Named); + }; + } catch (error) { + indexedPropertyCount = function(mirror) { + var n = 0; + var names = mirror.propertyNames(); + for (var i = 0; i < names.length; i++) { + if (isIndex(names[i])) { + n++; + } + } + return n; + }; + namedPropertyCount = function(mirror) { + var n = 0; + var names = mirror.propertyNames(); + for (var i = 0; i < names.length; i++) { + if (!isIndex(names[i])) { + n++; + } + } + return n; + }; + hasManyProperties = function(mirror, limit) { + return mirror.propertyNames().length >= limit; + }; + namedProperties = function(mirror) { + var named = []; + var names = mirror.propertyNames(); + for (var i = 0; i < names.length; i++) { + var name = names[i]; + if (!isIndex(name)) { + named.push(name); + } + } + return named; + }; + } + + var isIndex = function(name) { + switch (typeof name) { + case 'number': + return true; + case 'string': + return INDEX_PATTERN.test(name); + default: + return false; + } + }; + + /** + * In old versions of node it was possible to monkey patch the JSON response serializer. + * This made it possible to drop large objects from the 'refs' array (that is part of every protocol response). + */ + try { + var JSONProtocolSerializer = vm.runInDebugContext('JSONProtocolSerializer'); + + JSONProtocolSerializer.prototype.serializeReferencedObjects = function () { + var content = []; + for (var i = 0; i < this.mirrors_.length; i++) { + var m = this.mirrors_[i]; + + if (m.isArray()) continue; + + if (m.isObject()) { + if (m.handle() < 0) { + // we cannot drop transient objects from 'refs' because they cannot be looked up later + } else { + if (hasManyProperties(m, CHUNK_SIZE)) { + continue; + } + } + } + + content.push(this.serialize_(m, false, false)); + } + return content; + }; + } catch (error) { + // since overriding 'serializeReferencedObjects' is optional, we can silently ignore the error. + } + + /** + * This new protocol request makes it possible to retrieve a range of values from a large object. + * 'mode' controls whether 'named' or 'indexed' or 'all' types of properties are returned. + * For 'indexed' or 'all' mode 'start' and 'count' specify the range of properties to return. + */ + DebugCommandProcessor.prototype.dispatch_['vscode_slice'] = function(request, response) { + var handle = request.arguments.handle; + var start = request.arguments.start; + var count = request.arguments.count; + var mode = request.arguments.mode; + var mirror = LookupMirror(handle); + if (!mirror) { + return response.failed('Object #' + handle + '# not found'); + } + var result = []; + if (mode === 'named' || mode === 'all') { + if (mirror.isArray() || mirror.isObject()) { + var names = namedProperties(mirror); + for (var i = 0; i < names.length; i++) { + var name = names[i]; + var p = mirror.property(name); + result.push({ name: name, value: p.value() }); + } + } + } + if (mode === 'indexed' || mode === 'all') { + if (mirror.isArray()) { + var a = mirror.indexedPropertiesFromRange(start, start+count-1); + for (var i = 0; i < a.length; i++) { + result.push({ name: (start+i).toString(), value: a[i].value() }); + } + } else if (mirror.isObject()) { + for (var i = 0, j = start; i < count; i++, j++) { + var p = mirror.property(j.toString()); + result.push({ name: j.toString(), value: p.value() }); + } + } + } + response.body = { + result: result + }; + }; + + /** + * If the passed mirror object is a large array or object this function + * returns the mirror without its properties but with two size attributes ('vscode_namedCnt', 'vscode_indexedCnt') instead. + */ + var dehydrate = function(mirror) { + var namedCnt = -1; + var indexedCnt = -1; + + if (mirror.isArray()) { + namedCnt = namedPropertyCount(mirror); + indexedCnt = mirror.length(); + } else if (mirror.isObject()) { + switch (mirror.className()) { + case 'ArrayBuffer': + case 'Int8Array': + case 'Uint8Array': + case 'Uint8ClampedArray': + case 'Int16Array': + case 'Uint16Array': + case 'Int32Array': + case 'Uint32Array': + case 'Float32Array': + case 'Float64Array': + namedCnt = namedPropertyCount(mirror); + indexedCnt = indexedPropertyCount(mirror); + break; + default: + break; + } + } + + if (indexedCnt > CHUNK_SIZE) { + return { + type: 'object', + handle: mirror.handle(), + className: mirror.className(), + vscode_namedCnt: namedCnt, + vscode_indexedCnt: indexedCnt + }; + } + return mirror; + }; + + /** + * This override removes the properties of large data structures from the lookup response + * and returns the size of the data structure instead. + */ + DebugCommandProcessor.prototype.dispatch_['vscode_lookup'] = function(request, response) { + var result = this.lookupRequest_(request, response); + if (!result && response.body) { + var handles = request.arguments.handles; + for (var i = 0; i < handles.length; i++) { + var handle = handles[i]; + response.body[handle] = dehydrate(response.body[handle]); + } + } + return result; + }; + + /** + * This override removes the properties of large data structures from the lookup response + * and returns the size of the data structure instead. + */ + DebugCommandProcessor.prototype.dispatch_['vscode_evaluate'] = function(request, response) { + var result = this.evaluateRequest_(request, response); + if (!result) { + response.body = dehydrate(response.body); + } + return result; + }; + + /** + * This override trims the maximum number of local variables to 'maxLocals'. + */ + DebugCommandProcessor.prototype.dispatch_['vscode_scopes'] = function(request, response) { + var result = this.scopesRequest_(request, response); + if (!result) { + var maxLocals = request.arguments.maxLocals; + var scopes = response.body.scopes; + for (var i = 0; i < scopes.length-1; i++) { + var details = scopes[i].details_.details_; + if (details && details[0] === 1) { // locals + var locals = details[1]; + var names = Object.keys(locals); + if (names.length > maxLocals) { + var locals2 = {}; + for (var j = 0; j < maxLocals; j++) { + var name = names[j]; + locals2[name] = locals[name]; + } + details[1] = locals2; + response.body.vscode_locals = names.length; // remember original number of locals + } + } + } + } + return result; + }; +}() diff --git a/src/node/extension/autoAttach.ts b/src/node/extension/autoAttach.ts new file mode 100644 index 00000000..692ff802 --- /dev/null +++ b/src/node/extension/autoAttach.ts @@ -0,0 +1,200 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { basename } from 'path'; +import { analyseArguments } from './protocolDetection'; +import { ProcessTreeNode, getProcessTree } from './processTree'; + +const localize = nls.loadMessageBundle(); + +const POLL_INTERVAL = 1000; + +const pids: Promise[] = []; +let autoAttacher: vscode.Disposable | undefined; + + +export function getPidFromSession(session: vscode.DebugSession): Promise { + return new Promise((resolve, e) => { + setTimeout(_ => { + + // wait a maximum of 100 ms for response + const timer = setTimeout(_ => { + resolve(NaN); + }, 100); + + // try to get the process ID from the debuggee + if (session) { + session.customRequest('evaluate', { expression: 'process.pid' }).then(reply => { + clearTimeout(timer); + resolve(parseInt(reply.result)); + }, e => { + clearTimeout(timer); + resolve(NaN); + }); + } else { + clearTimeout(timer); + resolve(NaN); + } + }, session.type === 'legacy-node2' ? 500 : 100); + }); +} + +export function initializeAutoAttach(context: vscode.ExtensionContext) { + + context.subscriptions.push(vscode.debug.onDidStartDebugSession(session => { + if (session.type === 'legacy-node' || session.type === 'legacy-node2') { + // try to get pid from newly started node.js debug session + pids.push(getPidFromSession(session)); + } + })); + + context.subscriptions.push(vscode.commands.registerCommand('extension.node-debug.startAutoAttach', rootPid => { + if (typeof rootPid === 'number') { + autoAttacher = pollProcesses(rootPid, true, (pid, cmdPath, args) => { + const cmdName = basename(cmdPath, '.exe'); + if (cmdName === 'node') { + const name = localize('process.with.pid.label', "Auto attached ({0})", pid); + attachToProcess(undefined, name, pid, args); + } + }); + } + })); + + context.subscriptions.push(vscode.commands.registerCommand('extension.node-debug.stopAutoAttach', () => { + if (autoAttacher) { + autoAttacher.dispose(); + autoAttacher = undefined; + } + })); +} + +function alreadyAttached(pid: number): Promise { + + return Promise.all(pids).then(pids => { + return pids.indexOf(pid) >= 0; + }); +} + +export function attachToProcess(folder: vscode.WorkspaceFolder | undefined, name: string, pid: number, args: string, baseConfig?: vscode.DebugConfiguration, parentSession?: vscode.DebugSession) { + + alreadyAttached(pid).then(isAttached => { + if (isAttached) { + // console.log(`ignore auto attach for ${pid}`); + } else { + + pids.push(Promise.resolve(pid)); + + const config: vscode.DebugConfiguration = { + type: 'node', + request: 'attach', + name: name, + stopOnEntry: false, + __autoAttach: true + }; + + if (baseConfig) { + // selectively copy attributes + if (typeof baseConfig.timeout === 'number') { + config.timeout = baseConfig.timeout; + } + if (typeof baseConfig.sourceMaps === 'boolean') { + config.sourceMaps = baseConfig.sourceMaps; + } + if (baseConfig.outFiles) { + config.outFiles = baseConfig.outFiles; + } + if (baseConfig.sourceMapPathOverrides) { + config.sourceMapPathOverrides = baseConfig.sourceMapPathOverrides; + } + if (typeof baseConfig.smartStep === 'boolean') { + config.smartStep = baseConfig.smartStep; + } + if (baseConfig.skipFiles) { + config.skipFiles = baseConfig.skipFiles; + } + if (typeof baseConfig.showAsyncStacks === 'boolean') { + config.showAsyncStacks = baseConfig.showAsyncStacks; + } + if (typeof baseConfig.trace === 'boolean' || typeof baseConfig.trace === 'string') { + config.trace = baseConfig.trace; + } + if (typeof baseConfig.stopOnEntry === 'boolean') { + config.stopOnEntry = baseConfig.stopOnEntry; + } + } + + let { usePort, protocol, port } = analyseArguments(args); + if (usePort) { + config.processId = `${protocol}${port}`; + } else { + if (protocol && port > 0) { + config.processId = `${pid}${protocol}${port}`; + } else { + config.processId = pid.toString(); + } + } + + vscode.debug.startDebugging(folder, config, parentSession); + } + }); +} + +/** + * Poll for all subprocesses of given root process. + */ +function pollProcesses(rootPid: number, inTerminal: boolean, cb: (pid: number, cmd: string, args: string) => void) : vscode.Disposable { + + let stopped = false; + + function poll() { + //const start = Date.now(); + findChildProcesses(rootPid, inTerminal, cb).then(_ => { + //console.log(`duration: ${Date.now() - start}`); + setTimeout(_ => { + if (!stopped) { + poll(); + } + }, POLL_INTERVAL); + }); + } + + poll(); + + return new vscode.Disposable(() => stopped = true); +} + +function findChildProcesses(rootPid: number, inTerminal: boolean, cb: (pid: number, cmd: string, args: string) => void): Promise { + + function walker(node: ProcessTreeNode, terminal: boolean, terminalPids: (number | undefined)[]) { + + if (terminalPids.indexOf(node.pid) >= 0) { + terminal = true; // found the terminal shell + } + + let { protocol } = analyseArguments(node.args); + if (terminal && protocol) { + cb(node.pid, node.command, node.args); + } + + for (const child of node.children || []) { + walker(child, terminal, terminalPids); + } + } + + return getProcessTree(rootPid).then(tree => { + if (tree) { + const terminals = vscode.window.terminals; + if (terminals.length > 0) { + Promise.all(terminals.map(terminal => terminal.processId)).then(terminalPids => { + walker(tree, !inTerminal, terminalPids); + }); + } + } + }); +} diff --git a/src/node/extension/cluster.ts b/src/node/extension/cluster.ts new file mode 100644 index 00000000..0e5d4c34 --- /dev/null +++ b/src/node/extension/cluster.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { attachToProcess, getPidFromSession } from './autoAttach'; +import { ProcessTreeNode, getProcessTree } from './processTree'; +import { analyseArguments } from './protocolDetection'; + +const localize = nls.loadMessageBundle(); + +const POLL_INTERVAL = 1000; + +export class Cluster { + + static clusters = new Map(); + + private _poller?: vscode.Disposable; + private _subProcesses: Set; // we remember all child process attached to here + private _childCounter: number; + + + public static prepareAutoAttachChildProcesses(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration) { + this.clusters.set(config.name, new Cluster(folder, config)); + } + + static startSession(session: vscode.DebugSession) { + const cluster = this.clusters.get(session.name); + if (cluster) { + cluster.startWatching(session); + } + } + + static stopSession(session: vscode.DebugSession) { + const cluster = this.clusters.get(session.name); + if (cluster) { + cluster.stopWatching(); + this.clusters.delete(session.name); + } + } + + private constructor(private _folder: vscode.WorkspaceFolder | undefined, private _config: vscode.DebugConfiguration) { + this._subProcesses = new Set(); + this._childCounter = 1; + } + + private startWatching(session: vscode.DebugSession) { + // get the process ID from the leader debuggee + getPidFromSession(session).then(leaderPid => { + // start polling for child processes under the leader + this._poller = pollProcesses(leaderPid, false, (pid, cmd, args) => { + // only attach to new child processes + if (!this._subProcesses.has(pid)) { + this._subProcesses.add(pid); + const name = localize('child.process.with.pid.label', "Child process {0}", this._childCounter++); + attachToProcess(this._folder, name, pid, args, this._config, session); + } + }); + }); + } + + private stopWatching() { + if (this._poller) { + this._poller.dispose(); + this._poller = undefined; + } + } +} + +/** + * Poll for all subprocesses of given root process. + */ +function pollProcesses(rootPid: number, inTerminal: boolean, cb: (pid: number, cmd: string, args: string) => void) : vscode.Disposable { + + let stopped = false; + + function poll() { + //const start = Date.now(); + findChildProcesses(rootPid, cb).then(_ => { + //console.log(`duration: ${Date.now() - start}`); + setTimeout(_ => { + if (!stopped) { + poll(); + } + }, POLL_INTERVAL); + }); + } + + poll(); + + return new vscode.Disposable(() => stopped = true); +} + +function findChildProcesses(rootPid: number, cb: (pid: number, cmd: string, args: string) => void): Promise { + + function walker(node: ProcessTreeNode) { + + if (node.pid !== rootPid) { + let { protocol } = analyseArguments(node.args); + if (protocol) { + cb(node.pid, node.command, node.args); + } + } + + for (const child of node.children || []) { + walker(child); + } + } + + return getProcessTree(rootPid).then(tree => { + if (tree) { + walker(tree); + } + }); +} diff --git a/src/node/extension/configurationProvider.ts b/src/node/extension/configurationProvider.ts new file mode 100644 index 00000000..17958ec5 --- /dev/null +++ b/src/node/extension/configurationProvider.ts @@ -0,0 +1,494 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import * as nls from 'vscode-nls'; +import * as vscode from 'vscode'; +import { join, isAbsolute, dirname, relative } from 'path'; +import * as fs from 'fs'; + +import { writeToConsole, mkdirP, Logger } from './utilities'; +import { detectDebugType } from './protocolDetection'; +import { resolveProcessId } from './processPicker'; +import { Cluster } from './cluster'; + +const DEBUG_SETTINGS = 'debug.node'; +const SHOW_USE_WSL_IS_DEPRECATED_WARNING_SETTING = 'showUseWslIsDeprecatedWarning'; +const DEFAULT_JS_PATTERNS: ReadonlyArray = ['*.js', '*.es6', '*.jsx', '*.mjs']; + +const localize = nls.loadMessageBundle(); + +//---- NodeConfigurationProvider + +export class NodeConfigurationProvider implements vscode.DebugConfigurationProvider { + + private _logger: Logger; + + constructor(private _extensionContext: vscode.ExtensionContext) { + this._logger = new Logger(); + } + + /** + * Try to add all missing attributes to the debug configuration being launched. + */ + resolveDebugConfiguration(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): vscode.ProviderResult { + return this.resolveConfigAsync(folder, config).catch(err => { + return vscode.window.showErrorMessage(err.message, { modal: true }).then(_ => undefined); // abort launch + }); + } + + /** + * Try to add all missing attributes to the debug configuration being launched. + */ + private async resolveConfigAsync(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): Promise { + + // if launch.json is missing or empty + if (!config.type && !config.request && !config.name) { + + config = createLaunchConfigFromContext(folder, true, config); + + if (!config.program) { + throw new Error(localize('program.not.found.message', "Cannot find a program to debug")); + } + } + + // make sure that config has a 'cwd' attribute set + if (!config.cwd) { + if (folder) { + config.cwd = folder.uri.fsPath; + } + + // no folder -> config is a user or workspace launch config + if (!config.cwd && vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) { + config.cwd = vscode.workspace.workspaceFolders[0].uri.fsPath; + } + + // no folder case + if (!config.cwd && config.program === '${file}') { + config.cwd = '${fileDirname}'; + } + + // program is some absolute path + if (!config.cwd && config.program && isAbsolute(config.program)) { + // derive 'cwd' from 'program' + config.cwd = dirname(config.program); + } + + // last resort + if (!config.cwd && folder) { + config.cwd = '${workspaceFolder}'; + } + } + + // if a 'remoteRoot' is specified without a corresponding 'localRoot', set 'localRoot' to the workspace folder. + // see https://github.com/Microsoft/vscode/issues/63118 + if (config.remoteRoot && !config.localRoot) { + config.localRoot = '${workspaceFolder}'; + } + + // warn about deprecated 'useWSL' attribute. + if (typeof config.useWSL !== 'undefined') { + this.warnAboutUseWSL(); + } + + // remove 'useWSL' on all platforms but Windows + if (process.platform !== 'win32' && config.useWSL) { + this._logger.debug('useWSL attribute ignored on non-Windows OS.'); + delete config.useWSL; + } + + // "nvm" support + if (config.request === 'launch' && typeof config.runtimeVersion === 'string' && config.runtimeVersion !== 'default') { + await this.nvmSupport(config); + } + + // "auto attach child process" (aka Cluster) support + if (config.autoAttachChildProcesses) { + Cluster.prepareAutoAttachChildProcesses(folder, config); + // if no console is set, use the integrated terminal so that output of all child processes goes to one terminal. See https://github.com/Microsoft/vscode/issues/62420 + if (!config.console) { + config.console = 'integratedTerminal'; + } + } + + // when using "integratedTerminal" ensure that debug console doesn't get activated; see https://github.com/Microsoft/vscode/issues/43164 + if (config.console === 'integratedTerminal' && !config.internalConsoleOptions) { + config.internalConsoleOptions = 'neverOpen'; + } + + // fixup log parameters + if (config.trace && !config.logFilePath) { + const fileName = config.type === 'legacy-node' ? 'debugadapter-legacy.txt' : 'debugadapter.txt'; + + if (this._extensionContext.logPath) { + try { + await mkdirP(this._extensionContext.logPath); + } catch (e) { + // Already exists + } + + config.logFilePath = join(this._extensionContext.logPath, fileName); + } + } + + // tell the extension what file patterns can be debugged + config.__debuggablePatterns = this.getJavaScriptPatterns(); + + // everything ok: let VS Code start the debug session + return config; + } + + /** + * Try to add all missing attributes to the debug configuration being launched. + */ + resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): vscode.ProviderResult { + return this.resolveConfigWithSubstitutedVariablesAsync(folder, config).catch(err => { + return vscode.window.showErrorMessage(err.message, { modal: true }).then(_ => undefined); // abort launch + }); + } + + /** + * Try to add all missing attributes to the debug configuration being launched. + */ + private async resolveConfigWithSubstitutedVariablesAsync(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration): Promise { + + // attach to process by processId + if (config.request === 'attach' && typeof config.processId === 'string') { + await resolveProcessId(config); + } + + // determine which protocol to use after all variables have been substituted (including command variables for process picker) + const debugType = await determineDebugType(config, this._logger); + if (debugType) { + config.type = debugType; + } + + // everything ok: let VS Code start the debug session + return config; + } + + private getJavaScriptPatterns() { + const associations = vscode.workspace.getConfiguration('files.associations'); + const extension = vscode.extensions.getExtension<{}>('ms-vscode.node-debug'); + if (!extension) { + throw new Error('Expected to be able to load extension data'); + } + + const handledLanguages = extension.packageJSON.contributes.breakpoints.map(b => b.language); + return Object.keys(associations) + .filter(pattern => handledLanguages.indexOf(associations[pattern]) !== -1) + .concat(DEFAULT_JS_PATTERNS); + } + + private warnAboutUseWSL() { + + interface MyMessageItem extends vscode.MessageItem { + id: number; + } + + if (vscode.workspace.getConfiguration(DEBUG_SETTINGS).get(SHOW_USE_WSL_IS_DEPRECATED_WARNING_SETTING, true)) { + vscode.window.showWarningMessage( + localize( + 'useWslDeprecationWarning.title', + "Attribute 'useWSL' is deprecated. Please use the 'Remote WSL' extension instead. Click [here]({0}) to learn more.", + 'https://go.microsoft.com/fwlink/?linkid=2097212' + ), { + title: localize('useWslDeprecationWarning.doNotShowAgain', "Don't Show Again"), + id: 1 + } + ).then(selected => { + if (!selected) { + return; + } + switch (selected.id) { + case 1: + vscode.workspace.getConfiguration(DEBUG_SETTINGS).update(SHOW_USE_WSL_IS_DEPRECATED_WARNING_SETTING, false, vscode.ConfigurationTarget.Global); + break; + } + }); + } + } + + /** + * if a runtime version is specified we prepend env.PATH with the folder that corresponds to the version. + * Returns false on error + */ + private async nvmSupport(config: vscode.DebugConfiguration): Promise { + + let bin: string | undefined = undefined; + let versionManagerName: string | undefined = undefined; + + // first try the Node Version Switcher 'nvs' + let nvsHome = process.env['NVS_HOME']; + if (!nvsHome) { + // NVS_HOME is not always set. Probe for 'nvs' directory instead + const nvsDir = process.platform === 'win32' ? join(process.env['LOCALAPPDATA'] || '', 'nvs') : join(process.env['HOME'] || '', '.nvs'); + if (fs.existsSync(nvsDir)) { + nvsHome = nvsDir; + } + } + + const { nvsFormat, remoteName, semanticVersion, arch } = parseVersionString(config.runtimeVersion); + + if (nvsFormat || nvsHome) { + if (nvsHome) { + bin = join(nvsHome, remoteName, semanticVersion, arch); + if (process.platform !== 'win32') { + bin = join(bin, 'bin'); + } + versionManagerName = 'nvs'; + } else { + throw new Error(localize('NVS_HOME.not.found.message', "Attribute 'runtimeVersion' requires Node.js version manager 'nvs'.")); + } + } + + if (!bin) { + + // now try the Node Version Manager 'nvm' + if (process.platform === 'win32') { + const nvmHome = process.env['NVM_HOME']; + if (!nvmHome) { + throw new Error(localize('NVM_HOME.not.found.message', "Attribute 'runtimeVersion' requires Node.js version manager 'nvm-windows' or 'nvs'.")); + } + bin = join(nvmHome, `v${config.runtimeVersion}`); + versionManagerName = 'nvm-windows'; + } else { // macOS and linux + let nvmHome = process.env['NVM_DIR']; + if (!nvmHome) { + // if NVM_DIR is not set. Probe for '.nvm' directory instead + const nvmDir = join(process.env['HOME'] || '', '.nvm'); + if (fs.existsSync(nvmDir)) { + nvmHome = nvmDir; + } + } + if (!nvmHome) { + throw new Error(localize('NVM_DIR.not.found.message', "Attribute 'runtimeVersion' requires Node.js version manager 'nvm' or 'nvs'.")); + } + bin = join(nvmHome, 'versions', 'node', `v${config.runtimeVersion}`, 'bin'); + versionManagerName = 'nvm'; + } + } + + if (fs.existsSync(bin)) { + if (!config.env) { + config.env = {}; + } + if (process.platform === 'win32') { + config.env['Path'] = `${bin};${process.env['Path']}`; + } else { + config.env['PATH'] = `${bin}:${process.env['PATH']}`; + } + } else { + throw new Error(localize('runtime.version.not.found.message', "Node.js version '{0}' not installed for '{1}'.", config.runtimeVersion, versionManagerName)); + } + } +} + +//---- helpers ---------------------------------------------------------------------------------------------------------------- + +function createLaunchConfigFromContext(folder: vscode.WorkspaceFolder | undefined, resolve: boolean, existingConfig?: vscode.DebugConfiguration): vscode.DebugConfiguration { + + const config = { + type: 'legacy-node', + request: 'launch', + name: localize('node.launch.config.name', "Launch Program"), + skipFiles: ['/**'], + }; + + if (existingConfig && existingConfig.noDebug) { + config['noDebug'] = true; + } + + const pkg = loadJSON(folder, 'package.json'); + if (pkg && pkg.name === 'mern-starter') { + + if (resolve) { + writeToConsole(localize({ key: 'mern.starter.explanation', comment: ['argument contains product name without translation'] }, "Launch configuration for '{0}' project created.", 'Mern Starter')); + } + configureMern(config); + + } else { + let program: string | undefined; + let useSourceMaps = false; + + if (pkg) { + // try to find a value for 'program' by analysing package.json + program = guessProgramFromPackage(folder, pkg, resolve); + if (program && resolve) { + writeToConsole(localize('program.guessed.from.package.json.explanation', "Launch configuration created based on 'package.json'.")); + } + } + + if (!program) { + // try to use file open in editor + const editor = vscode.window.activeTextEditor; + if (editor) { + const languageId = editor.document.languageId; + if (languageId === 'javascript' || isTranspiledLanguage(languageId)) { + const wf = vscode.workspace.getWorkspaceFolder(editor.document.uri); + if (wf && wf === folder) { + program = relative(wf.uri.fsPath || '/', editor.document.uri.fsPath || '/'); + if (program && !isAbsolute(program)) { + program = join('${workspaceFolder}', program); + } + } + } + useSourceMaps = isTranspiledLanguage(languageId); + } + } + + // if we couldn't find a value for 'program', we just let the launch config use the file open in the editor + if (!program) { + program = '${file}'; + } + + if (program) { + config['program'] = program; + } + + // prepare for source maps by adding 'outFiles' if typescript or coffeescript is detected + if (useSourceMaps || vscode.workspace.textDocuments.some(document => isTranspiledLanguage(document.languageId))) { + if (resolve) { + writeToConsole(localize('outFiles.explanation', "Adjust glob pattern(s) in the 'outFiles' attribute so that they cover the generated JavaScript.")); + } + + let dir = ''; + const tsConfig = loadJSON(folder, 'tsconfig.json'); + if (tsConfig && tsConfig.compilerOptions && tsConfig.compilerOptions.outDir) { + const outDir = tsConfig.compilerOptions.outDir; + if (!isAbsolute(outDir)) { + dir = outDir; + if (dir.indexOf('./') === 0) { + dir = dir.substr(2); + } + if (dir[dir.length - 1] !== '/') { + dir += '/'; + } + } + config['preLaunchTask'] = 'tsc: build - tsconfig.json'; + } + config['outFiles'] = ['${workspaceFolder}/' + dir + '**/*.js']; + } + } + + return config; +} + +function loadJSON(folder: vscode.WorkspaceFolder | undefined, file: string): any { + if (folder) { + try { + const path = join(folder.uri.fsPath, file); + const content = fs.readFileSync(path, 'utf8'); + return JSON.parse(content); + } catch (error) { + // silently ignore + } + } + return undefined; +} + +function configureMern(config: any) { + config.protocol = 'inspector'; + config.runtimeExecutable = 'nodemon'; + config.program = '${workspaceFolder}/index.js'; + config.restart = true; + config.env = { + BABEL_DISABLE_CACHE: '1', + NODE_ENV: 'development' + }; + config.console = 'integratedTerminal'; + config.internalConsoleOptions = 'neverOpen'; +} + +function isTranspiledLanguage(languagId: string): boolean { + return languagId === 'typescript' || languagId === 'coffeescript'; +} + +/* + * try to find the entry point ('main') from the package.json + */ +function guessProgramFromPackage(folder: vscode.WorkspaceFolder | undefined, packageJson: any, resolve: boolean): string | undefined { + + let program: string | undefined; + + try { + if (packageJson.main) { + program = packageJson.main; + } else if (packageJson.scripts && typeof packageJson.scripts.start === 'string') { + // assume a start script of the form 'node server.js' + program = (packageJson.scripts.start).split(' ').pop(); + } + + if (program) { + let path: string | undefined; + if (isAbsolute(program)) { + path = program; + } else { + path = folder ? join(folder.uri.fsPath, program) : undefined; + program = join('${workspaceFolder}', program); + } + if (resolve && path && !fs.existsSync(path) && !fs.existsSync(path + '.js')) { + return undefined; + } + } + + } catch (error) { + // silently ignore + } + + return program; +} + +//---- debug type ------------------------------------------------------------------------------------------------------------- + +async function determineDebugType(config: any, logger: Logger): Promise { + if (config.protocol === 'legacy') { + return 'legacy-node'; + } else if (config.protocol === 'inspector') { + return 'legacy-node2'; + } else { + // 'auto', or unspecified + return detectDebugType(config, logger); + } +} + + +function nvsStandardArchName(arch) { + switch (arch) { + case '32': + case 'x86': + case 'ia32': + return 'x86'; + case '64': + case 'x64': + case 'amd64': + return 'x64'; + case 'arm': + const arm_version = (process.config.variables as any).arm_version; + return arm_version ? 'armv' + arm_version + 'l' : 'arm'; + default: + return arch; + } +} + +/** + * Parses a node version string into remote name, semantic version, and architecture + * components. Infers some unspecified components based on configuration. + */ +function parseVersionString(versionString) { + const versionRegex = /^(([\w-]+)\/)?(v?(\d+(\.\d+(\.\d+)?)?))(\/((x86)|(32)|((x)?64)|(arm\w*)|(ppc\w*)))?$/i; + + const match = versionRegex.exec(versionString); + if (!match) { + throw new Error('Invalid version string: ' + versionString); + } + + const nvsFormat = !!(match[2] || match[8]); + const remoteName = match[2] || 'node'; + const semanticVersion = match[4] || ''; + const arch = nvsStandardArchName(match[8] || process.arch); + + return { nvsFormat, remoteName, semanticVersion, arch }; +} diff --git a/src/node/extension/extension.ts b/src/node/extension/extension.ts new file mode 100644 index 00000000..3c16c654 --- /dev/null +++ b/src/node/extension/extension.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; + +import { NodeConfigurationProvider } from './configurationProvider'; +import { pickProcess, attachProcess } from './processPicker'; +import { Cluster } from './cluster'; +import { initializeAutoAttach } from './autoAttach'; + +export function activate(context: vscode.ExtensionContext) { + + // register a configuration provider + context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('legacy-node', new NodeConfigurationProvider(context))); + + // auto attach + initializeAutoAttach(context); + + // toggle skipping file action + context.subscriptions.push(vscode.commands.registerCommand('extension.node-debug.toggleSkippingFile', toggleSkippingFile)); + + // process picker command + context.subscriptions.push(vscode.commands.registerCommand('extension.pickNodeProcess', pickProcess)); + + // attach process command + context.subscriptions.push(vscode.commands.registerCommand('extension.node-debug.attachNodeProcess', attachProcess)); + + // cluster + context.subscriptions.push(vscode.debug.onDidStartDebugSession(session => Cluster.startSession(session))); + context.subscriptions.push(vscode.debug.onDidTerminateDebugSession(session => Cluster.stopSession(session))); +} + +export function deactivate() { +} + + +//---- toggle skipped files + +function toggleSkippingFile(res: string | number): void { + + let resource: string | number | undefined = res; + + if (!resource) { + const activeEditor = vscode.window.activeTextEditor; + resource = activeEditor && activeEditor.document.fileName; + } + + if (resource && vscode.debug.activeDebugSession) { + const args = typeof resource === 'string' ? { resource } : { sourceReference: resource }; + vscode.debug.activeDebugSession.customRequest('toggleSkipFileStatus', args); + } +} diff --git a/src/node/extension/processPicker.ts b/src/node/extension/processPicker.ts new file mode 100644 index 00000000..73be9b8c --- /dev/null +++ b/src/node/extension/processPicker.ts @@ -0,0 +1,227 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import * as nls from 'vscode-nls'; +import * as vscode from 'vscode'; +import { basename } from 'path'; +import { getProcesses } from './processTree'; +import { execSync } from 'child_process'; +import { detectProtocolForPid, INSPECTOR_PORT_DEFAULT, LEGACY_PORT_DEFAULT } from './protocolDetection'; +import { analyseArguments } from './protocolDetection'; + +const localize = nls.loadMessageBundle(); + +//---- extension.pickNodeProcess + +interface ProcessItem extends vscode.QuickPickItem { + pidOrPort: string; // picker result + sortKey: number; +} + +/** + * end user action for picking a process and attaching debugger to it + */ +export async function attachProcess() { + + const result = await pickProcess(true); // ask for pids and ports! + if (result) { + + const config = { + type: 'legacy-node', + request: 'attach', + name: 'process', + processId: result + }; + + await resolveProcessId(config); + + return vscode.debug.startDebugging(undefined, config); + } + return undefined; +} + +/** + * Process the special protocol/processId/port patterns that the process picker puts in the "processId" attribute. + */ +export async function resolveProcessId(config: vscode.DebugConfiguration) : Promise { + + let processId = config.processId.trim(); + + const matches = /^(inspector|legacy)?([0-9]+)(inspector|legacy)?([0-9]+)?$/.exec(processId); + if (matches && matches.length === 5) { + + if (matches[2] && matches[3] && matches[4]) { + + // process id and protocol and port + + const pid = Number(matches[2]); + putPidInDebugMode(pid); + + // debug port + config.port = Number(matches[4]); + config.protocol = matches[3]; + delete config.processId; + + } else { + + // protocol and port + if (matches[1]) { + + // debug port + config.port = Number(matches[2]); + config.protocol = matches[1]; + delete config.processId; + + } else { + + // process id + const pid = Number(matches[2]); + putPidInDebugMode(pid); + + const debugType = await determineDebugTypeForPidInDebugMode(config, pid); + if (debugType) { + // processID is handled, so turn this config into a normal port attach configuration + delete config.processId; + config.port = debugType === 'legacy-node2' ? INSPECTOR_PORT_DEFAULT : LEGACY_PORT_DEFAULT; + config.protocol = debugType === 'legacy-node2' ? 'inspector' : 'legacy'; + } else { + throw new Error(localize('pid.error', "Attach to process: cannot put process '{0}' in debug mode.", processId)); + } + } + } + + } else { + throw new Error(localize('process.id.error', "Attach to process: '{0}' doesn't look like a process id.", processId)); + } +} + +/** + * Process picker command (for launch config variable) + * Returns as a string with these formats: + * - "12345": process id + * - "inspector12345": port number and inspector protocol + * - "legacy12345": port number and legacy protocol + * - null: abort launch silently + */ +export function pickProcess(ports?): Promise { + + return listProcesses(ports).then(items => { + let options: vscode.QuickPickOptions = { + placeHolder: localize('pickNodeProcess', "Pick the node.js process to attach to"), + matchOnDescription: true, + matchOnDetail: true + }; + return vscode.window.showQuickPick(items, options).then(item => item ? item.pidOrPort : null); + }).catch(err => { + return vscode.window.showErrorMessage(localize('process.picker.error', "Process picker failed ({0})", err.message), { modal: true }).then(_ => null); + }); +} + +//---- private + +function listProcesses(ports: boolean): Promise { + + const items: ProcessItem[] = []; + + const NODE = new RegExp('^(?:node|iojs)$', 'i'); + + let seq = 0; // default sort key + + return getProcesses((pid: number, ppid: number, command: string, args: string, date: number) => { + + if (process.platform === 'win32' && command.indexOf('\\??\\') === 0) { + // remove leading device specifier + command = command.replace('\\??\\', ''); + } + + const executable_name = basename(command, '.exe'); + + let port = -1; + let protocol: string | undefined = ''; + let usePort = true; + + if (ports) { + const x = analyseArguments(args); + usePort = x.usePort; + protocol = x.protocol; + port = x.port; + } + + let description = ''; + let pidOrPort = ''; + + if (usePort) { + if (protocol === 'inspector') { + description = localize('process.id.port', "process id: {0}, debug port: {1}", pid, port); + } else { + description = localize('process.id.port.legacy', "process id: {0}, debug port: {1} (legacy protocol)", pid, port); + } + pidOrPort = `${protocol}${port}`; + } else { + if (protocol && port > 0) { + description = localize('process.id.port.signal', "process id: {0}, debug port: {1} ({2})", pid, port, 'SIGUSR1'); + pidOrPort = `${pid}${protocol}${port}`; + } else { + // no port given + if (NODE.test(executable_name)) { + description = localize('process.id.signal', "process id: {0} ({1})", pid, 'SIGUSR1'); + pidOrPort = pid.toString(); + } + } + } + + if (description && pidOrPort) { + items.push({ + // render data + label: executable_name, + description: args, + detail: description, + + // picker result + pidOrPort: pidOrPort, + // sort key + sortKey: date ? date : seq++ + }); + } + + }).then(() => items.sort((a, b) => b.sortKey - a.sortKey)); // sort items by process id, newest first +} + +function putPidInDebugMode(pid: number): void { + try { + if (process.platform === 'win32') { + // regular node has an undocumented API function for forcing another node process into debug mode. + // (process)._debugProcess(pid); + // But since we are running on Electron's node, process._debugProcess doesn't work (for unknown reasons). + // So we use a regular node instead: + const command = `node -e process._debugProcess(${pid})`; + execSync(command); + } else { + process.kill(pid, 'SIGUSR1'); + } + } catch (e) { + throw new Error(localize('cannot.enable.debug.mode.error', "Attach to process: cannot enable debug mode for process '{0}' ({1}).", pid, e)); + } +} + +function determineDebugTypeForPidInDebugMode(config: any, pid: number): Promise { + let debugProtocolP: Promise; + if (config.port === INSPECTOR_PORT_DEFAULT) { + debugProtocolP = Promise.resolve('inspector'); + } else if (config.port === LEGACY_PORT_DEFAULT) { + debugProtocolP = Promise.resolve('legacy'); + } else if (config.protocol) { + debugProtocolP = Promise.resolve(config.protocol); + } else { + debugProtocolP = detectProtocolForPid(pid); + } + + return debugProtocolP.then(debugProtocol => { + return debugProtocol === 'inspector' ? 'legacy-node2' : + debugProtocol === 'legacy' ? 'legacy-node' : + null; + }); +} diff --git a/src/node/extension/processTree.ts b/src/node/extension/processTree.ts new file mode 100644 index 00000000..c94cdc7d --- /dev/null +++ b/src/node/extension/processTree.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { spawn, ChildProcessWithoutNullStreams } from 'child_process'; +import { join } from 'path'; + +export class ProcessTreeNode { + children?: ProcessTreeNode[]; + + constructor(public pid: number, public ppid: number, public command: string, public args: string) { + } +} + +export async function getProcessTree(rootPid: number) : Promise { + + const map = new Map(); + + map.set(0, new ProcessTreeNode(0, 0, '???', '')); + + try { + await getProcesses((pid: number, ppid: number, command: string, args: string) => { + if (pid !== ppid) { + map.set(pid, new ProcessTreeNode(pid, ppid, command, args)); + } + }); + } catch (err) { + return undefined; + } + + const values = map.values(); + for (const p of values) { + const parent = map.get(p.ppid); + if (parent && parent !== p) { + if (!parent.children) { + parent.children = []; + } + parent.children.push(p); + } + } + + if (!isNaN(rootPid) && rootPid > 0) { + return map.get(rootPid); + } + return map.get(0); +} + +export function getProcesses(one: (pid: number, ppid: number, command: string, args: string, date?: number) => void) : Promise { + + // returns a function that aggregates chunks of data until one or more complete lines are received and passes them to a callback. + function lines(callback: (a: string) => void) { + let unfinished = ''; // unfinished last line of chunk + return (data: string | Buffer) => { + const lines = data.toString().split(/\r?\n/); + const finishedLines = lines.slice(0, lines.length - 1); + finishedLines[0] = unfinished + finishedLines[0]; // complete previous unfinished line + unfinished = lines[lines.length - 1]; // remember unfinished last line of this chunk for next round + for (const s of finishedLines) { + callback(s); + } + }; + } + + return new Promise((resolve, reject) => { + + let proc: ChildProcessWithoutNullStreams; + + if (process.platform === 'win32') { + + // attributes columns are in alphabetic order! + const CMD_PAT = /^(.*)\s+([0-9]+)\.[0-9]+[+-][0-9]+\s+([0-9]+)\s+([0-9]+)$/; + + const wmic = join(process.env['WINDIR'] || 'C:\\Windows', 'System32', 'wbem', 'WMIC.exe'); + proc = spawn(wmic, [ 'process', 'get', 'CommandLine,CreationDate,ParentProcessId,ProcessId' ]); + proc.stdout.setEncoding('utf8'); + proc.stdout.on('data', lines(line => { + let matches = CMD_PAT.exec(line.trim()); + if (matches && matches.length === 5) { + const pid = Number(matches[4]); + const ppid = Number(matches[3]); + const date = Number(matches[2]); + let args = matches[1].trim(); + if (!isNaN(pid) && !isNaN(ppid) && args) { + let command = args; + if (args[0] === '"') { + const end = args.indexOf('"', 1); + if (end > 0) { + command = args.substr(1, end-1); + args = args.substr(end + 2); + } + } else { + const end = args.indexOf(' '); + if (end > 0) { + command = args.substr(0, end); + args = args.substr(end + 1); + } else { + args = ''; + } + } + one(pid, ppid, command, args, date); + } + } + })); + + } else if (process.platform === 'darwin') { // OS X + + proc = spawn('/bin/ps', [ '-x', '-o', `pid,ppid,comm=${'a'.repeat(256)},command` ]); + proc.stdout.setEncoding('utf8'); + proc.stdout.on('data', lines(line => { + + const pid = Number(line.substr(0, 5)); + const ppid = Number(line.substr(6, 5)); + const command = line.substr(12, 256).trim(); + const args = line.substr(269 + command.length); + + if (!isNaN(pid) && !isNaN(ppid)) { + one(pid, ppid, command, args); + } + })); + + } else { // linux + + proc = spawn('/bin/ps', [ '-ax', '-o', 'pid:6,ppid:6,comm:20,command' ]); // we specify the column width explicitly + proc.stdout.setEncoding('utf8'); + proc.stdout.on('data', lines(line => { + + // the following substr arguments must match the column width specified for the "ps" command above + const pid = Number(line.substr(0, 6)); + const ppid = Number(line.substr(7, 6)); + let command = line.substr(14, 20).trim(); + let args = line.substr(35); + + let pos = args.indexOf(command); + if (pos >= 0) { + pos = pos + command.length; + while (pos < args.length) { + if (args[pos] === ' ') { + break; + } + pos++; + } + command = args.substr(0, pos); + args = args.substr(pos + 1); + } + + if (!isNaN(pid) && !isNaN(ppid)) { + one(pid, ppid, command, args); + } + })); + } + + proc.on('error', err => { + reject(err); + }); + + proc.stderr.setEncoding('utf8'); + proc.stderr.on('data', data => { + const e = data.toString(); + if (e.indexOf('screen size is bogus') >= 0) { + // ignore this error silently; see https://github.com/microsoft/vscode/issues/75932 + } else { + reject(new Error(data.toString())); + } + }); + + proc.on('close', (code, signal) => { + if (code === 0) { + resolve(); + } else if (code > 0) { + reject(new Error(`process terminated with exit code: ${code}`)); + } + if (signal) { + reject(new Error(`process terminated with signal: ${signal}`)); + } + }); + + proc.on('exit', (code, signal) => { + if (typeof code === 'number') { + if (code === 0) { + //resolve(); + } else if (code > 0) { + reject(new Error(`process terminated with exit code: ${code}`)); + } + } + if (signal) { + reject(new Error(`process terminated with signal: ${signal}`)); + } + }); + }); +} + diff --git a/src/node/extension/protocolDetection.ts b/src/node/extension/protocolDetection.ts new file mode 100644 index 00000000..80620699 --- /dev/null +++ b/src/node/extension/protocolDetection.ts @@ -0,0 +1,258 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import * as nls from 'vscode-nls'; +import * as cp from 'child_process'; +import { writeToConsole, extendObject, Logger } from './utilities'; +import * as net from 'net'; +import * as WSL from '../wslSupport'; + +const localize = nls.loadMessageBundle(); + +export const INSPECTOR_PORT_DEFAULT = 9229; +export const LEGACY_PORT_DEFAULT = 5858; + +// For launch, use inspector protocol starting with v8 because it's stable after that version. +const InspectorMinNodeVersionLaunch = 80000; + +export function detectDebugType(config: any, logger: Logger): Promise { + switch (config.request) { + case 'attach': + return detectProtocolForAttach(config, logger).then(protocol => { + return protocol === 'inspector' ? 'legacy-node2' : 'legacy-node'; + }); + + case 'launch': + return Promise.resolve(detectProtocolForLaunch(config, logger) === 'inspector' ? 'legacy-node2' : 'legacy-node'); + default: + // should not happen + break; + } + + return Promise.resolve(null); +} + +/** + * Detect which debug protocol is being used for a running node process. + */ +function detectProtocolForAttach(config: any, logger: Logger): Promise { + const address = config.address || '127.0.0.1'; + const port = config.port; + const socket = new net.Socket(); + const cleanup = () => { + try { + socket.write(`"Content-Length: 50\r\n\r\n{"command":"disconnect","type":"request","seq":2}"`); + socket.end(); + } catch (e) { + // ignore failure + } + }; + + return new Promise<{ reason?: string, protocol: string }>((resolve, reject) => { + socket.once('data', data => { + let reason: string|undefined = undefined; + let protocol: string; + const dataStr = data.toString(); + if (dataStr.indexOf('WebSockets request was expected') >= 0) { + logger.debug('Debugging with inspector protocol because it was detected.'); + protocol = 'inspector'; + } else { + reason = localize('protocol.switch.legacy.detected', "Debugging with legacy protocol because it was detected."); + protocol = 'legacy'; + } + + resolve({ reason, protocol }); + }); + + socket.once('error', err => { + reject(err); + }); + + socket.connect(port, address); + socket.on('connect', () => { + // Send a safe request to trigger a response from the inspector protocol + socket.write(`Content-Length: 102\r\n\r\n{"command":"evaluate","arguments":{"expression":"process.pid","global":true},"type":"request","seq":1}`); + }); + + setTimeout(() => { + // No data or error received? Bail and let the debug adapter handle it. + reject(new Error('timeout')); + }, 2000); + }).catch(err => { + return { + reason: localize('protocol.switch.unknown.error', "Debugging with inspector protocol because Node.js version could not be determined ({0})", err.toString()), + protocol: 'inspector' + }; + }).then(result => { + cleanup(); + if (result.reason) { + writeToConsole(result.reason); + logger.debug(result.reason); + } + + return result.protocol; + }); +} + +function detectProtocolForLaunch(config: any, logger: Logger): 'legacy'|'inspector' { + if (config.runtimeExecutable) { + logger.debug('Debugging with inspector protocol because a runtime executable is set.'); + return 'inspector'; + } else { + // only determine version if no runtimeExecutable is set (and 'node' on PATH is used) + let env = process.env; + if (config.env) { + env = extendObject(extendObject( {} , process.env), config.env); + } + const result = WSL.spawnSync(config.useWSL, 'node', ['--version'], { shell: true, env: env }); + const semVerString = result.stdout ? result.stdout.toString() : undefined; + if (semVerString) { + config.__nodeVersion = semVerString.trim(); + if (semVerStringToInt(config.__nodeVersion) >= InspectorMinNodeVersionLaunch) { + logger.debug(`Debugging with inspector protocol because Node.js ${config.__nodeVersion} was detected.`); + return 'inspector'; + } else { + writeToConsole(localize('protocol.switch.legacy.version', "Debugging with legacy protocol because Node.js {0} was detected.", config.__nodeVersion)); + logger.debug(`Debugging with legacy protocol because Node.js ${config.__nodeVersion} was detected.`); + return 'legacy'; + } + } else { + logger.debug('Debugging with inspector protocol because Node.js version could not be determined.'); + return 'inspector'; + } + } +} + +/** + * convert the 3 parts of a semVer string into a single number + */ +function semVerStringToInt(vString: string): number { + const match = vString.match(/v(\d+)\.(\d+)\.(\d+)/); + if (match && match.length === 4) { + return (parseInt(match[1]) * 100 + parseInt(match[2])) * 100 + parseInt(match[3]); + } + return -1; +} + +export function detectProtocolForPid(pid: number): Promise { + return process.platform === 'win32' ? + detectProtocolForPidWin(pid) : + detectProtocolForPidUnix(pid); +} + +function detectProtocolForPidWin(pid: number): Promise { + return getOpenPortsForPidWin(pid).then(ports => { + return ports.indexOf(INSPECTOR_PORT_DEFAULT) >= 0 ? 'inspector' : + ports.indexOf(LEGACY_PORT_DEFAULT) >= 0 ? 'legacy' : null; + }); +} + +/** + * Netstat output is like: + Proto Local Address Foreign Address State PID + TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 812 + */ +function getOpenPortsForPidWin(pid: number): Promise { + return new Promise(resolve => { + cp.exec('netstat -a -n -o -p TCP', (err, stdout) => { + if (err || !stdout) { + resolve([]); + } + + const ports = stdout + .split(/\r?\n/) + .map(line => line.trim().split(/\s+/)) + .filter(lineParts => { + // Filter to just `pid` rows + return lineParts[4] && lineParts[4] === String(pid); + }) + .map(lineParts => { + const address = lineParts[1]; + return parseInt(address.split(':')[1]); + }); + + resolve(ports); + }); + }); +} + +function detectProtocolForPidUnix(pid: number): Promise { + return getPidListeningOnPortUnix(INSPECTOR_PORT_DEFAULT).then(inspectorProtocolPid => { + if (inspectorProtocolPid === pid) { + return 'inspector'; + } else { + return getPidListeningOnPortUnix(LEGACY_PORT_DEFAULT) + .then(legacyProtocolPid => legacyProtocolPid === pid ? 'legacy' : null); + } + }); +} + +function getPidListeningOnPortUnix(port: number): Promise { + return new Promise(resolve => { + cp.exec(`lsof -i:${port} -F p`, (err, stdout) => { + if (err || !stdout) { + resolve(-1); + return; + } + + const pidMatch = stdout.match(/p(\d+)/); + if (pidMatch && pidMatch[1]) { + resolve(Number(pidMatch[1])); + } else { + resolve(-1); + } + }); + }); +} + +export interface DebugArguments { + usePort: boolean; // if true debug by using the debug port + protocol?: 'legacy' | 'inspector'; // + address?: string; + port: number; +} + +/* + * analyse the given command line arguments and extract debug port and protocol from it. + */ +export function analyseArguments(args: string): DebugArguments { + + const DEBUG_FLAGS_PATTERN = /--(inspect|debug)(-brk)?(=((\[[0-9a-fA-F:]*\]|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+|[a-zA-Z0-9\.]*):)?(\d+))?/; + const DEBUG_PORT_PATTERN = /--(inspect|debug)-port=(\d+)/; + + const result: DebugArguments = { + usePort: false, + port: -1 + }; + + // match --debug, --debug=1234, --debug-brk, debug-brk=1234, --inspect, --inspect=1234, --inspect-brk, --inspect-brk=1234 + let matches = DEBUG_FLAGS_PATTERN.exec(args); + if (matches && matches.length >= 2) { + // attach via port + result.usePort = true; + if (matches.length >= 6 && matches[5]) { + result.address = matches[5]; + } + if (matches.length >= 7 && matches[6]) { + result.port = parseInt(matches[6]); + } + result.protocol = matches[1] === 'debug' ? 'legacy' : 'inspector'; + } + + // a debug-port=1234 or --inspect-port=1234 overrides the port + matches = DEBUG_PORT_PATTERN.exec(args); + if (matches && matches.length === 3) { + // override port + result.port = parseInt(matches[2]); + result.protocol = matches[1] === 'debug' ? 'legacy' : 'inspector'; + } + + if (result.port < 0) { + result.port = result.protocol === 'inspector' ? INSPECTOR_PORT_DEFAULT : LEGACY_PORT_DEFAULT; + } + + return result; +} diff --git a/src/node/extension/utilities.ts b/src/node/extension/utilities.ts new file mode 100644 index 00000000..a6fdea92 --- /dev/null +++ b/src/node/extension/utilities.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as FS from 'fs'; + +export class Logger { + debug(message: string) { + // TODO: enable based on log level + // vscode.debug.activeDebugConsole.appendLine(message); + } +} + +/** + * Send to debug console. + */ +export function writeToConsole(message: string) { + vscode.debug.activeDebugConsole.appendLine(message); +} + +/** + * Copy attributes from fromObject to toObject. + */ +export function extendObject(toObject: T, fromObject: T): T { + + for (let key in fromObject) { + if (fromObject.hasOwnProperty(key)) { + toObject[key] = fromObject[key]; + } + } + return toObject; +} + +export function mkdirP(path: string): Promise { + return new Promise((resolve, reject) => { + FS.mkdir(path, err => { + if (err) { + return reject(err); + } + + resolve(); + }); + }); +} diff --git a/src/node/nodeDebug.ts b/src/node/nodeDebug.ts new file mode 100644 index 00000000..ca30a653 --- /dev/null +++ b/src/node/nodeDebug.ts @@ -0,0 +1,4296 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + LoggingDebugSession, DebugSession, Logger, logger, + Thread, Source, StackFrame, Scope, Variable, Breakpoint, + TerminatedEvent, InitializedEvent, StoppedEvent, OutputEvent, LoadedSourceEvent, + Handles, ErrorDestination, CapabilitiesEvent +} from 'vscode-debugadapter'; +import {DebugProtocol} from 'vscode-debugprotocol'; + +import { + NodeV8Protocol, NodeV8Event, NodeV8Response, + V8SetBreakpointArgs, V8SetVariableValueArgs, V8RestartFrameArgs, V8BacktraceArgs, + V8ScopeResponse, V8EvaluateResponse, V8FrameResponse, + V8EventBody, V8BreakEventBody, V8ExceptionEventBody, + V8Ref, V8Handle, V8Property, V8Object, V8Simple, V8Function, V8Frame, V8Scope, V8Script +} from './nodeV8Protocol'; +import {ISourceMaps, SourceMaps, SourceMap} from './sourceMaps'; +import * as PathUtils from './pathUtilities'; +import * as WSL from './wslSupport'; +import * as CP from 'child_process'; +import * as Net from 'net'; +import * as URL from 'url'; +import * as Path from 'path'; +import * as FS from 'fs'; +import * as nls from 'vscode-nls'; + +let localize = nls.loadMessageBundle(); + +type FilterType = 'named' | 'indexed' | 'all'; + +interface ProcessEnvWithNull { + [key: string]: string | null; +} + +export interface VariableContainer { + Expand(session: NodeDebugSession, filter: FilterType, start: number | undefined, count: number | undefined): Promise; + SetValue(session: NodeDebugSession, name: string, value: string): Promise; +} + +type ExpanderFunction = (start: number, count: number) => Promise; + +export class Expander implements VariableContainer { + + public static SET_VALUE_ERROR = localize('setVariable.error', "Setting value not supported"); + + private _expanderFunction : ExpanderFunction; + + public constructor(func: ExpanderFunction) { + this._expanderFunction = func; + } + + public Expand(session: NodeDebugSession, filter: string, start: number, count: number) : Promise { + return this._expanderFunction(start, count); + } + + public SetValue(session: NodeDebugSession, name: string, value: string) : Promise { + return Promise.reject(new Error(Expander.SET_VALUE_ERROR)); + } +} + +export class PropertyContainer implements VariableContainer { + + private _evalName: string | undefined; + private _object: V8Object; + private _this: V8Object | undefined; + + public constructor(evalName: string | undefined, obj: V8Object, ths?: V8Object) { + this._evalName = evalName; + this._object = obj; + this._this = ths; + } + + public Expand(session: NodeDebugSession, filter: FilterType, start: number, count: number) : Promise { + + if (filter === 'named') { + return session._createProperties(this._evalName, this._object, 'named').then(variables => { + if (this._this) { + return session._createVariable(this._evalName, 'this', this._this).then(variable => { + if (variable) { + variables.push(variable); + } + return variables; + }); + } else { + return variables; + } + }); + } + + if (typeof start === 'number' && typeof count === 'number') { + return session._createProperties(this._evalName, this._object, 'indexed', start, count); + } else { + return session._createProperties(this._evalName, this._object, 'all').then(variables => { + if (this._this) { + return session._createVariable(this._evalName, 'this', this._this).then(variable => { + if (variable) { + variables.push(variable); + } + return variables; + }); + } else { + return variables; + } + }); + } + } + + public SetValue(session: NodeDebugSession, name: string, value: string) : Promise { + return session._setPropertyValue(this._object.handle, name, value); + } +} + +export class SetMapContainer implements VariableContainer { + + private _evalName: string | undefined; + private _object: V8Object; + + public constructor(evalName: string | undefined, obj: V8Object) { + this._evalName = evalName; + this._object = obj; + } + + public Expand(session: NodeDebugSession, filter: FilterType, start: number, count: number) : Promise { + + if (filter === 'named') { + return session._createSetMapProperties(this._evalName, this._object); + } + + if (this._object.type === 'set') { + return session._createSetElements(this._object, start, count); + } else { + return session._createMapElements(this._object, start, count); + } + } + + public SetValue(session: NodeDebugSession, name: string, value: string) : Promise { + return Promise.reject(new Error(Expander.SET_VALUE_ERROR)); + } +} + +export class ScopeContainer implements VariableContainer { + + private _frame: number; + private _scope: number; + private _object: V8Object; + private _this: V8Object | undefined; + + public constructor(scope: V8Scope, obj: V8Object, ths?: V8Object) { + this._frame = scope.frameIndex; + this._scope = scope.index; + this._object = obj; + this._this = ths; + } + + public Expand(session: NodeDebugSession, filter: FilterType, start: number, count: number) : Promise { + return session._createProperties('', this._object, filter).then(variables => { + if (this._this) { + return session._createVariable('', 'this', this._this).then(variable => { + if (variable) { + variables.push(variable); + } + return variables; + }); + } else { + return variables; + } + }); + } + + public SetValue(session: NodeDebugSession, name: string, value: string) : Promise { + return session._setVariableValue(this._frame, this._scope, name, value); + } +} + +type ReasonType = 'step' | 'breakpoint' | 'exception' | 'pause' | 'entry' | 'debugger_statement' | 'frame_entry'; + +class Script { + contents: string; + sourceMap: SourceMap; + + constructor(script: V8Script) { + this.contents = script.source; + } +} + +type HitterFunction = (hitCount: number) => boolean; + +class InternalSourceBreakpoint { + + line: number; + orgLine: number; + column: number; + orgColumn: number; + condition: string | undefined; + hitCount: number; + hitter: HitterFunction | undefined; + verificationMessage: string; + + constructor(line: number, column: number = 0, condition?: string, logMessage?: string, hitter?: HitterFunction) { + this.line = this.orgLine = line; + this.column = this.orgColumn = column; + + if (logMessage) { + this.condition = logMessageToExpression(logMessage); + if (condition) { + this.condition = `(${condition}) && ${this.condition}`; + } + } else if (condition) { + this.condition = condition; + } + + this.hitCount = 0; + this.hitter = hitter; + } +} + +/** + * A SourceSource represents the source contents of an internal module or of a source map with inlined contents. + */ +class SourceSource { + scriptId: number; // if 0 then source contains the file contents of a source map, otherwise a scriptID. + source: string | undefined; + + constructor(sid: number, content?: string) { + this.scriptId = sid; + this.source = content; + } +} + +/** + * Arguments shared between Launch and Attach requests. + */ +interface CommonArguments { + /** comma separated list of trace selectors. Supported: + * 'all': all + * 'la': launch/attach + * 'ls': load scripts + * 'bp': breakpoints + * 'sm': source maps + * 'va': data structure access + * 'ss': smart steps + * 'rc': ref caching + * */ + trace?: boolean | string; + /** The debug port to attach to. */ + port: number; + /** The TCP/IP address of the port (remote addresses only supported for node >= 5.0). */ + address?: string; + /** Retry for this number of milliseconds to connect to the node runtime. */ + timeout?: number; + /** Automatically stop target after launch. If not specified, target does not stop. */ + stopOnEntry?: boolean; + /** Configure source maps. By default source maps are enabled (since v1.9.11). */ + sourceMaps?: boolean; + /** obsolete: Where to look for the generated code. Only used if sourceMaps is true. */ + outDir?: string; + /** output files glob patterns */ + outFiles?: string[]; + /** Try to automatically step over uninteresting source. */ + smartStep?: boolean; + /** automatically skip these files. */ + skipFiles?: string[]; + /** Request frontend to restart session on termination. */ + restart?: boolean; + /** Node's root directory. */ + remoteRoot?: string; + /** VS Code's root directory. */ + localRoot?: string; + + // unofficial flags + + /** Step back supported. */ + stepBack?: boolean; + /** Control mapping of node.js scripts to files on disk. */ + mapToFilesOnDisk?: boolean; + + // internal attributes + + /** Debug session ID */ + __sessionId: string; +} + +type ConsoleType = 'internalConsole' | 'integratedTerminal' | 'externalTerminal'; + +/** + * This interface should always match the schema found in the node-debug extension manifest. + */ +interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments, CommonArguments { + /** An absolute path to the program to debug. */ + program: string; + /** Optional arguments passed to the debuggee. */ + args?: string[]; + /** Launch the debuggee in this working directory (specified as an absolute path). If omitted the debuggee is lauched in its own directory. */ + cwd?: string; + /** Absolute path to the runtime executable to be used. Default is the runtime executable on the PATH. */ + runtimeExecutable?: string; + /** Optional arguments passed to the runtime executable. */ + runtimeArgs?: string[]; + /** Optional environment variables to pass to the debuggee. The string valued properties of the 'environmentVariables' are used as key/value pairs. */ + env?: { [key: string]: string | null; }; + /** Optional path to .env file. */ + envFile?: string; + /** Deprecated: if true launch the target in an external console. */ + externalConsole?: boolean; + /** Where to launch the debug target. */ + console?: ConsoleType; + /** Use Windows Subsystem Linux */ + useWSL?: boolean; +} + +/** + * This interface should always match the schema found in the node-debug extension manifest. + */ +interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments, CommonArguments { + + // currently nothing is 'attach' specific +} + + +export class NodeDebugSession extends LoggingDebugSession { + + private static MAX_STRING_LENGTH = 10000; // max string size to return in 'evaluate' request + private static MAX_JSON_LENGTH = 500000; // max size of stringified object to return in 'evaluate' request + + private static NODE_TERMINATION_POLL_INTERVAL = 3000; + private static ATTACH_TIMEOUT = 10000; + private static RUNINTERMINAL_TIMEOUT = 5000; + + private static PREVIEW_PROPERTIES = 3; // maximum number of properties to show in object/array preview + private static PREVIEW_MAX_STRING_LENGTH = 50; // truncate long strings for object/array preview + + private static NODE = 'node'; + private static DUMMY_THREAD_ID = 1; + private static DUMMY_THREAD_NAME = 'Node'; + private static FIRST_LINE_OFFSET = 62; + private static PROTO = '__proto__'; + private static DEBUG_INJECTION = 'debugInjection.js'; + private static NODE_INTERNALS = ''; + private static NODE_INTERNALS_PREFIX = /^[/\\]/; + private static NODE_INTERNALS_VM = /^[/\\]VM([0-9]+)/; + private static JS_EXTENSIONS = [ '.js', '.es6', '.jsx', '.mjs' ]; + + private static NODE_SHEBANG_MATCHER = new RegExp('#! */usr/bin/env +node'); + private static LONG_STRING_MATCHER = /\.\.\. \(length: [0-9]+\)$/; + private static HITCOUNT_MATCHER = /(>|>=|=|==|<|<=|%)?\s*([0-9]+)/; + private static PROPERTY_NAME_MATCHER = /^[$_\w][$_\w0-9]*$/; + + // tracing + private _trace: string[] | undefined; + private _traceAll = false; + + // options + private _tryToInjectExtension = true; + private _skipRejects = false; // do not stop on rejected promises + private _maxVariablesPerScope = 100; // only load this many variables for a scope + private _smartStep = false; // try to automatically step over uninteresting source + private _skipFiles: string[] | undefined; // skip glob patterns + private _mapToFilesOnDisk = true; // by default try to map node.js scripts to files on disk + private _compareContents = true; // by default verify that script contents is same as file contents + private _supportsRunInTerminalRequest = false; + + // session state + private _node: NodeV8Protocol; + private _attachSuccessful: boolean; + private _processId: number = -1; // pid of the program launched + private _nodeProcessId: number = -1; // pid of the node runtime + private _isWSL = false; + private _functionBreakpoints = new Array(); // node function breakpoint ids + private _scripts = new Map>(); // script cache + private _files = new Map>(); // file cache + private _scriptId2Handle = new Map(); + private _inlinedContentHandle = new Map(); + private _modifiedSources = new Set(); // track edited files + private _hitCounts = new Map(); // breakpoint ID -> ignore count + + // session configurations + private _noDebug = false; + private _attachMode = false; + private _localRoot: string | undefined; + private _remoteRoot: string | undefined; + private _restartMode = false; + private _port: number | undefined; + private _sourceMaps: ISourceMaps; + private _console: ConsoleType = 'internalConsole'; + private _stopOnEntry: boolean; + private _stepBack = false; + + // state valid between stop events + public _variableHandles = new Handles(); + private _frameHandles = new Handles(); + private _sourceHandles = new Handles(); + private _refCache = new Map(); + + // internal state + private _isTerminated: boolean; + private _inShutdown: boolean; + private _pollForNodeProcess = false; + private _exception: V8ExceptionEventBody | undefined; + private _restartFramePending: boolean; + private _stoppedReason: string; + private _nodeInjectionAvailable = false; + private _needContinue: boolean; + private _needBreakpointEvent: boolean; + private _needDebuggerEvent: boolean; + private _gotEntryEvent: boolean; + private _gotDebuggerEvent = false; + private _entryPath: string; + private _entryLine: number; // entry line in *.js file (not in the source file) + private _entryColumn: number; // entry column in *.js file (not in the source file) + private _smartStepCount = 0; + private _catchRejects = false; + private _disableSkipFiles = false; + + public constructor() { + super('node-debug.txt'); + + // this debugger uses zero-based lines and columns which is the default + // so the following two calls are not really necessary. + this.setDebuggerLinesStartAt1(false); + this.setDebuggerColumnsStartAt1(false); + + this._node = new NodeV8Protocol(response => { + // if request successful, cache alls refs + if (response.success && response.refs) { + const oldSize = this._refCache.size; + for (let r of response.refs) { + this._cache(r.handle, r); + } + if (this._refCache.size !== oldSize) { + this.log('rc', `NodeV8Protocol hook: ref cache size: ${this._refCache.size}`); + } + } + }); + + this._node.on('break', (event: NodeV8Event) => { + this._stopped('break'); + this._handleNodeBreakEvent(event.body); + }); + + this._node.on('exception', (event: NodeV8Event) => { + this._stopped('exception'); + this._handleNodeExceptionEvent(event.body); + }); + + /* + this._node.on('beforeCompile', (event: NodeV8Event) => { + //this.outLine(`beforeCompile ${this._scriptToPath(event.body.script)}`); + this.sendEvent(new Event('customScriptLoad', { script: this._scriptToPath(event.body.script) })); + }); + */ + + this._node.on('afterCompile', (event: NodeV8Event) => { + this._scriptToSource(event.body.script).then(source => { + this.sendEvent(new LoadedSourceEvent('new', source)); + }); + }); + + this._node.on('close', (event: NodeV8Event) => { + this._terminated('node v8protocol close'); + }); + + this._node.on('error', (event: NodeV8Event) => { + this._terminated('node v8protocol error'); + }); + + /* + this._node.on('diagnostic', (event: NodeV8Event) => { + this.outLine(`diagnostic event ${event.body.reason}`); + }); + */ + } + + /** + * Analyse why node has stopped and sends StoppedEvent if necessary. + */ + private _handleNodeExceptionEvent(eventBody: V8ExceptionEventBody) : void { + + // should we skip this location? + if (this._skip(eventBody)) { + this._node.command('continue'); + return; + } + + let description: string | undefined; + + // in order to identify rejects extract source at current location + if (eventBody.sourceLineText && typeof eventBody.sourceColumn === 'number') { + let source = eventBody.sourceLineText.substr(eventBody.sourceColumn); + if (source.indexOf('reject(') === 0) { + if (this._skipRejects && !this._catchRejects) { + this._node.command('continue'); + return; + } + description = localize('exception.paused.promise.rejection', "Paused on Promise Rejection"); + if (eventBody.exception.text) { + eventBody.exception.text = localize('exception.promise.rejection.text', "Promise Rejection ({0})", eventBody.exception.text); + } else { + eventBody.exception.text = localize('exception.promise.rejection', "Promise Rejection"); + } + } + } + + // send event + this._exception = eventBody; + this._sendStoppedEvent('exception', description, eventBody.exception.text); + } + + /** + * Analyse why node has stopped and sends StoppedEvent if necessary. + */ + private _handleNodeBreakEvent(eventBody: V8BreakEventBody) : void { + + const breakpoints = eventBody.breakpoints; + + // check for breakpoints + if (Array.isArray(breakpoints) && breakpoints.length > 0) { + + this._disableSkipFiles = this._skip(eventBody); + + const id = breakpoints[0]; + if (!this._gotEntryEvent && id === 1) { // 'stop on entry point' is implemented as a breakpoint with ID 1 + + this.log('la', '_handleNodeBreakEvent: suppressed stop-on-entry event'); + // do not send event now + this._rememberEntryLocation(eventBody.script.name, eventBody.sourceLine, eventBody.sourceColumn); + return; + } + + this._sendBreakpointStoppedEvent(id); + return; + } + + // in order to identify debugger statements extract source at current location + if (eventBody.sourceLineText && typeof eventBody.sourceColumn === 'number') { + let source = eventBody.sourceLineText.substr(eventBody.sourceColumn); + if (source.indexOf('debugger') === 0) { + this._gotDebuggerEvent = true; + this._sendStoppedEvent('debugger_statement'); + return; + } + } + + // must be the result of a 'step' + let reason: ReasonType = 'step'; + if (this._restartFramePending) { + this._restartFramePending = false; + reason = 'frame_entry'; + } + + if (!this._disableSkipFiles) { + // should we continue until we find a better place to stop? + if ((this._smartStep && this._sourceMaps) || this._skipFiles) { + this._skipGenerated(eventBody).then(r => { + if (r) { + this._node.command('continue', { stepaction: 'in' }); + this._smartStepCount++; + } else { + this._sendStoppedEvent(reason); + } + }); + return; + } + } + + this._sendStoppedEvent(reason); + } + + private _sendBreakpointStoppedEvent(breakpointId: number): void { + + // evaluate hit counts + let ibp = this._hitCounts.get(breakpointId); + if (ibp) { + ibp.hitCount++; + if (ibp.hitter && !ibp.hitter(ibp.hitCount)) { + this._node.command('continue'); + return; + } + } + + this._sendStoppedEvent('breakpoint'); + } + + private _sendStoppedEvent(reason: ReasonType, description?: string, exception_text?: string): void { + + if (this._smartStepCount > 0) { + this.log('ss', `_handleNodeBreakEvent: ${this._smartStepCount} steps skipped`); + this._smartStepCount = 0; + } + + const e = new StoppedEvent(reason, NodeDebugSession.DUMMY_THREAD_ID, exception_text); + + if (!description) { + switch (reason) { + case 'step': + description = localize('reason.description.step', "Paused on step"); + break; + case 'breakpoint': + description = localize('reason.description.breakpoint', "Paused on breakpoint"); + break; + case 'exception': + description = localize('reason.description.exception', "Paused on exception"); + break; + case 'pause': + description = localize('reason.description.user_request', "Paused on user request"); + break; + case 'entry': + description = localize('reason.description.entry', "Paused on entry"); + break; + case 'debugger_statement': + description = localize('reason.description.debugger_statement', "Paused on debugger statement"); + break; + case 'frame_entry': + description = localize('reason.description.restart', "Paused on frame entry"); + break; + } + } + (e).body.description = description; + + this.sendEvent(e); + } + + private isSkipped(path: string): boolean { + return this._skipFiles ? PathUtils.multiGlobMatches(this._skipFiles, path) : false; + } + + /** + * Returns true if a source location of the given event should be skipped. + */ + private _skip(event: V8EventBody) : boolean { + + if (this._skipFiles) { + let path = this._scriptToPath(event.script); + + // if launch.json defines localRoot and remoteRoot try to convert remote path back to a local path + let localPath = this._remoteToLocal(path); + + return PathUtils.multiGlobMatches(this._skipFiles, localPath); + } + + return false; + } + + /** + * Returns true if a source location of the given event should be skipped. + */ + private _skipGenerated(event: V8EventBody) : Promise { + + let path = this._scriptToPath(event.script); + + // if launch.json defines localRoot and remoteRoot try to convert remote path back to a local path + let localPath = this._remoteToLocal(path); + + if (this._skipFiles) { + if (PathUtils.multiGlobMatches(this._skipFiles, localPath)) { + return Promise.resolve(true); + } + return Promise.resolve(false); + } + + if (this._smartStep) { + // try to map + let line = event.sourceLine; + let column = this._adjustColumn(line, event.sourceColumn); + + return this._sourceMaps.CannotMapLine(localPath, null, line, column).then(skip => { + return skip; + }); + } + + return Promise.resolve(false); + } + + private toggleSkippingResource(response: DebugProtocol.Response, resource: string) { + + resource = decodeURI(URL.parse(resource).pathname); + if (this.isSkipped(resource)) { + if (!this._skipFiles) { + this._skipFiles = new Array(); + } + this._skipFiles.push('!' + resource); + } else { + if (!this._skipFiles) { + this._skipFiles = new Array(); + } + this._skipFiles.push(resource); + } + this.sendResponse(response); + } + + /** + * create a path for a script following these rules: + * - script name is an absolute path: return name as is + * - script name is an internal module: return " { + let path = script.name; + if (path) { + if (!PathUtils.isAbsolutePath(path)) { + path = `${NodeDebugSession.NODE_INTERNALS}/${path}`; + } + } else { + path = `${NodeDebugSession.NODE_INTERNALS}/VM${script.id}`; + } + const src = new Source(Path.basename(path), path, this._getScriptIdHandle(script.id)); + if (this._sourceMaps) { + return this._sourceMaps.AllSources(path).then(sources => { + if (sources && sources.length > 0) { + (src).sources = sources.map(s => new Source(Path.basename(s), s) ); + } + return src; + }); + } + return Promise.resolve(src); + } + + /** + * Special treatment for internal modules: + * we remove the '/' or '\' prefix and return either the name of the module or its ID + */ + private _pathToScript(path: string): number | string { + + const result = NodeDebugSession.NODE_INTERNALS_VM.exec(path); + if (result && result.length >= 2) { + return + result[1]; + } + return path.replace(NodeDebugSession.NODE_INTERNALS_PREFIX, ''); + } + + /** + * clear everything that is no longer valid after a new stopped event. + */ + private _stopped(reason: string): void { + this._stoppedReason = reason; + this.log('la', `_stopped: got ${reason} event from node`); + this._exception = undefined; + this._variableHandles.reset(); + this._frameHandles.reset(); + this._refCache = new Map(); + this.log('rc', `_stopped: new ref cache`); + } + + /** + * The debug session has terminated. + */ + private _terminated(reason: string): void { + this.log('la', `_terminated: ${reason}`); + + if (!this._isTerminated) { + this._isTerminated = true; + if (this._restartMode && this._attachSuccessful && !this._inShutdown) { + this.sendEvent(new TerminatedEvent({ port: this._port })); + } else { + this.sendEvent(new TerminatedEvent()); + } + } + } + + //---- initialize request ------------------------------------------------------------------------------------------------- + + protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { + + this.log('la', `initializeRequest: adapterID: ${args.adapterID}`); + + if (args.locale) { + localize = nls.config({ locale: args.locale })(); + } + + if (typeof args.supportsRunInTerminalRequest === 'boolean') { + this._supportsRunInTerminalRequest = args.supportsRunInTerminalRequest; + } + + //---- Send back feature and their options + + response.body = response.body || {}; + + // This debug adapter supports the configurationDoneRequest. + response.body.supportsConfigurationDoneRequest = true; + + // This debug adapter supports function breakpoints. + response.body.supportsFunctionBreakpoints = true; + + // This debug adapter supports conditional breakpoints. + response.body.supportsConditionalBreakpoints = true; + + // This debug adapter does not support a side effect free evaluate request for data hovers. + response.body.supportsEvaluateForHovers = false; + + // This debug adapter supports two exception breakpoint filters + response.body.exceptionBreakpointFilters = [ + { + label: localize('exceptions.all', "All Exceptions"), + filter: 'all', + default: false + }, + { + label: localize('exceptions.uncaught', "Uncaught Exceptions"), + filter: 'uncaught', + default: true + } + ]; + if (this._skipRejects) { + response.body.exceptionBreakpointFilters.push({ + label: localize('exceptions.rejects', "Promise Rejects"), + filter: 'rejects', + default: false + }); + } + + // This debug adapter supports setting variables + response.body.supportsSetVariable = true; + + // This debug adapter supports the restartFrame request + response.body.supportsRestartFrame = true; + + // This debug adapter supports the completions request + response.body.supportsCompletionsRequest = true; + + // This debug adapter supports the exception info request + response.body.supportsExceptionInfoRequest = true; + + // This debug adapter supports delayed loading of stackframes + response.body.supportsDelayedStackTraceLoading = true; + + // This debug adapter supports log points + response.body.supportsLogPoints = true; + + // This debug adapter supports terminate request (but not on Windows) + response.body.supportsTerminateRequest = process.platform !== 'win32'; + + // This debug adapter supports loaded sources request + response.body.supportsLoadedSourcesRequest = true; + + this.sendResponse(response); + } + + //---- launch request ----------------------------------------------------------------------------------------------------- + + protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { + + if (this._processCommonArgs(response, args)) { + return; + } + + if (args.__restart && typeof args.__restart.port === 'number') { + this._attach(response, args, args.__restart.port, undefined, args.timeout); + return; + } + + this._noDebug = (typeof args.noDebug === 'boolean') && args.noDebug; + + if (typeof args.console === 'string') { + switch (args.console) { + case 'internalConsole': + case 'integratedTerminal': + case 'externalTerminal': + this._console = args.console; + break; + default: + this.sendErrorResponse(response, 2028, localize('VSND2028', "Unknown console type '{0}'.", args.console)); + return; + } + } else if (typeof args.externalConsole === 'boolean' && args.externalConsole) { + this._console = 'externalTerminal'; + } + + if (args.useWSL) { + if (!WSL.subsystemLinuxPresent()) { + this.sendErrorResponse(response, 2007, localize('attribute.wls.not.exist', "Cannot find Windows Subsystem Linux installation")); + return; + } + this._isWSL = true; + } + + let runtimeExecutable = args.runtimeExecutable; + if (args.useWSL) { + runtimeExecutable = runtimeExecutable || NodeDebugSession.NODE; + } else if (runtimeExecutable) { + if (!Path.isAbsolute(runtimeExecutable)) { + const re = PathUtils.findOnPath(runtimeExecutable, args.env); + if (!re) { + this.sendErrorResponse(response, 2001, localize('VSND2001', "Cannot find runtime '{0}' on PATH. Make sure to have '{0}' installed.", '{_runtime}'), { _runtime: runtimeExecutable }); + return; + } + runtimeExecutable = re; + } else { + const re = PathUtils.findExecutable(runtimeExecutable, args.env); + if (!re) { + this.sendNotExistErrorResponse(response, 'runtimeExecutable', runtimeExecutable); + return; + } + runtimeExecutable = re; + } + } else { + const re = PathUtils.findOnPath(NodeDebugSession.NODE, args.env); + if (!re) { + this.sendErrorResponse(response, 2001, localize('VSND2001', "Cannot find runtime '{0}' on PATH. Make sure to have '{0}' installed.", '{_runtime}'), { _runtime: NodeDebugSession.NODE }); + return; + } + runtimeExecutable = re; + } + + let runtimeArgs = args.runtimeArgs || []; + const programArgs = args.args || []; + + let programPath = args.program; + if (programPath) { + if (!Path.isAbsolute(programPath)) { + this.sendRelativePathErrorResponse(response, 'program', programPath); + return; + } + if (!FS.existsSync(programPath)) { + if (!FS.existsSync(programPath + '.js')) { + this.sendNotExistErrorResponse(response, 'program', programPath); + return; + } + programPath += '.js'; + } + programPath = Path.normalize(programPath); + if (PathUtils.normalizeDriveLetter(programPath) !== PathUtils.realPath(programPath)) { + this.outLine(localize('program.path.case.mismatch.warning', "Program path uses differently cased character as file on disk; this might result in breakpoints not being hit.")); + } + } + + if (!args.runtimeArgs && !this._noDebug) { + runtimeArgs = [ '--nolazy' ]; + } + + if (programPath) { + if (NodeDebugSession.isJavaScript(programPath)) { + if (this._sourceMaps) { + // if programPath is a JavaScript file and sourceMaps are enabled, we don't know whether + // programPath is the generated file or whether it is the source (and we need source mapping). + // Typically this happens if a tool like 'babel' or 'uglify' is used (because they both transpile js to js). + // We use the source maps to find a 'source' file for the given js file. + this._sourceMaps.MapPathFromSource(programPath).then(generatedPath => { + if (generatedPath && generatedPath !== programPath) { + // programPath must be source because there seems to be a generated file for it + this.log('sm', `launchRequest: program '${programPath}' seems to be the source; launch the generated file '${generatedPath}' instead`); + programPath = generatedPath; + } else { + this.log('sm', `launchRequest: program '${programPath}' seems to be the generated file`); + } + this.launchRequest2(response, args, programPath, programArgs, runtimeExecutable, runtimeArgs); + }); + return; + } + } else { + // node cannot execute the program directly + if (!this._sourceMaps) { + this.sendErrorResponse(response, 2002, localize('VSND2002', "Cannot launch program '{0}'; configuring source maps might help.", '{path}'), { path: programPath }); + return; + } + this._sourceMaps.MapPathFromSource(programPath).then(generatedPath => { + if (!generatedPath) { // cannot find generated file + if (args.outFiles || args.outDir) { + this.sendErrorResponse(response, 2009, localize('VSND2009', "Cannot launch program '{0}' because corresponding JavaScript cannot be found.", '{path}'), { path: programPath }); + } else { + this.sendErrorResponse(response, 2003, localize('VSND2003', "Cannot launch program '{0}'; setting the '{1}' attribute might help.", '{path}', 'outFiles'), { path: programPath }); + } + return; + } + this.log('sm', `launchRequest: program '${programPath}' seems to be the source; launch the generated file '${generatedPath}' instead`); + programPath = generatedPath; + this.launchRequest2(response, args, programPath, programArgs, runtimeExecutable, runtimeArgs); + }); + return; + } + } + + this.launchRequest2(response, args, programPath, programArgs, runtimeExecutable, runtimeArgs); + } + + private async launchRequest2(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments, programPath: string, programArgs: string[], runtimeExecutable: string, runtimeArgs: string[]): Promise { + + let program: string | undefined; + let workingDirectory = args.cwd; + + if (workingDirectory) { + if (!Path.isAbsolute(workingDirectory)) { + this.sendRelativePathErrorResponse(response, 'cwd', workingDirectory); + return; + } + if (!FS.existsSync(workingDirectory)) { + this.sendNotExistErrorResponse(response, 'cwd', workingDirectory); + return; + } + // if working dir is given and if the executable is within that folder, we make the executable path relative to the working dir + if (programPath) { + program = Path.relative(workingDirectory, programPath); + } + } + else if (programPath) { // should not happen + // if no working dir given, we use the direct folder of the executable + workingDirectory = Path.dirname(programPath); + program = Path.basename(programPath); + } + + // figure out when to add a '--debug-brk=nnnn' + let port = args.port; + let launchArgs = [ runtimeExecutable ].concat(runtimeArgs); + if (!this._noDebug) { + + if (args.port) { // a port was specified in launch config + + // only if the default runtime 'node' is used without arguments + if (!args.runtimeExecutable && !args.runtimeArgs) { + + // use the specfied port + launchArgs.push(`--debug-brk=${port}`); + } + } else { // no port is specified + + // use a random port + port = await findport(); + launchArgs.push(`--debug-brk=${port}`); + } + } + + if (program) { + launchArgs.push(program); + } + launchArgs = launchArgs.concat(programArgs); + + const address = args.address; + const timeout = args.timeout; + + let envVars = args.env; + + // read env from disk and merge into envVars + if (args.envFile) { + try { + const buffer = PathUtils.stripBOM(FS.readFileSync(args.envFile, 'utf8')); + const env = {}; + buffer.split('\n').forEach( line => { + const r = line.match(/^\s*([\w\.\-]+)\s*=\s*(.*)?\s*$/); + if (r !== null) { + const key = r[1]; + if (!process.env[key]) { // .env variables never overwrite existing variables (see #21169) + let value = r[2] || ''; + if (value.length > 0 && value.charAt(0) === '"' && value.charAt(value.length-1) === '"') { + value = value.replace(/\\n/gm, '\n'); + } + env[key] = value.replace(/(^['"]|['"]$)/g, ''); + } + } + }); + envVars = PathUtils.extendObject(env, args.env); // launch config env vars overwrite .env vars + } catch (e) { + this.sendErrorResponse(response, 2029, localize('VSND2029', "Can't load environment variables from file ({0}).", '{_error}'), { _error: e.message }); + return; + } + } + + const wslLaunchArgs = WSL.createLaunchArg(args.useWSL, + this._supportsRunInTerminalRequest && this._console === 'externalTerminal', + workingDirectory, + launchArgs[0], + launchArgs.slice(1), + program); // workaround for #35249 + + // if using subsystem linux, we use local/remote mapping (if not configured by user) + if (args.useWSL && !args.localRoot && !args.remoteRoot) { + this._localRoot = wslLaunchArgs.localRoot; + this._remoteRoot = wslLaunchArgs.remoteRoot; + } + + if (this._supportsRunInTerminalRequest && (this._console === 'externalTerminal' || this._console === 'integratedTerminal')) { + + const termArgs : DebugProtocol.RunInTerminalRequestArguments = { + kind: this._console === 'integratedTerminal' ? 'integrated' : 'external', + title: localize('node.console.title', "Node Debug Console"), + cwd: wslLaunchArgs.cwd, + args: wslLaunchArgs.combined, + env: envVars + }; + + this.runInTerminalRequest(termArgs, NodeDebugSession.RUNINTERMINAL_TIMEOUT, runResponse => { + if (runResponse.success) { + + // since node starts in a terminal, we cannot track it with an 'exit' handler + // plan for polling after we have gotten the process pid. + this._pollForNodeProcess = !args.runtimeExecutable // only if no 'runtimeExecutable' is specified + && !args.useWSL; // it will not work with WSL either + + if (this._noDebug) { + this.sendResponse(response); + + // since we do not know the process ID we will not be able to terminate it properly + // therefore we end the session + this._terminated('cannot track process'); + + } else { + this._attach(response, args, port, address, timeout); + } + } else { + this.sendErrorResponse(response, 2011, localize('VSND2011', "Cannot launch debug target in terminal ({0}).", '{_error}'), { _error: runResponse.message } ); + this._terminated('terminal error: ' + runResponse.message); + } + }); + + } else { + + this._sendLaunchCommandToConsole(launchArgs); + + // merge environment variables into a copy of the process.env + envVars = PathUtils.extendObject(PathUtils.extendObject( {}, process.env), envVars); + + // delete all variables that have a 'null' value + if (envVars) { + const e = envVars; // without this tsc complains about envVars potentially undefined + Object.keys(e).filter(v => e[v] === null).forEach(key => delete e[key] ); + } + + const options: CP.SpawnOptions = { + cwd: workingDirectory, + env: envVars + }; + + // see bug #45832 + if (process.platform === 'win32' && wslLaunchArgs.executable.indexOf(' ') > 0) { + let foundArgWithSpace = false; + + // check whether there is one arg with a space + const args: string[] = []; + for (const a of wslLaunchArgs.args) { + if (a.indexOf(' ') > 0) { + args.push(`"${a}"`); + foundArgWithSpace = true; + } else { + args.push(a); + } + } + + if (foundArgWithSpace) { + wslLaunchArgs.args = args; + wslLaunchArgs.executable = `"${wslLaunchArgs.executable}"`; + (options).shell = true; + } + } + + const nodeProcess = CP.spawn(wslLaunchArgs.executable, wslLaunchArgs.args, options); + nodeProcess.on('error', (error) => { + // tslint:disable-next-line:no-bitwise + this.sendErrorResponse(response, 2017, localize('VSND2017', "Cannot launch debug target ({0}).", '{_error}'), { _error: error.message }, ErrorDestination.Telemetry | ErrorDestination.User ); + this._terminated(`failed to launch target (${error})`); + }); + nodeProcess.on('exit', () => { + this._terminated('target exited'); + }); + nodeProcess.on('close', (code) => { + this._terminated('target closed'); + }); + + this._processId = nodeProcess.pid; + + this._captureOutput(nodeProcess); + + if (this._noDebug) { + this.sendResponse(response); + } else { + this._attach(response, args, port, address, timeout); + } + } + } + + private _sendLaunchCommandToConsole(args: string[]) { + // print the command to launch the target to the debug console + let cli = ''; + for (let a of args) { + if (a.indexOf(' ') >= 0) { + cli += '\'' + a + '\''; + } else { + cli += a; + } + cli += ' '; + } + this.outLine(cli); + } + + private _captureOutput(process: CP.ChildProcess) { + if (process.stdout) { + process.stdout.on('data', (data: string) => { + this.sendEvent(new OutputEvent(data.toString(), 'stdout')); + }); + } + if (process.stderr) { + process.stderr.on('data', (data: string) => { + this.sendEvent(new OutputEvent(data.toString(), 'stderr')); + }); + } + } + + /** + * returns true on error. + */ + private _processCommonArgs(response: DebugProtocol.Response, args: CommonArguments): boolean { + + let stopLogging = true; + if (typeof args.trace === 'boolean') { + this._trace = args.trace ? [ 'all' ] : undefined; + this._traceAll = args.trace; + } else if (typeof args.trace === 'string') { + this._trace = args.trace.split(','); + this._traceAll = this._trace.indexOf('all') >= 0; + + if (this._trace.indexOf('dap') >= 0) { + logger.setup(Logger.LogLevel.Verbose, /*logToFile=*/false); + stopLogging = false; + } + } + if (stopLogging) { + logger.setup(Logger.LogLevel.Stop, false); + } + + if (typeof args.stepBack === 'boolean') { + this._stepBack = args.stepBack; + } + + if (typeof args.mapToFilesOnDisk === 'boolean') { + this._mapToFilesOnDisk = args.mapToFilesOnDisk; + } + + if (typeof args.smartStep === 'boolean') { + this._smartStep = args.smartStep; + } + + if (Array.isArray(args.skipFiles)) { + this._skipFiles = args.skipFiles; + } + + if (typeof args.stopOnEntry === 'boolean') { + this._stopOnEntry = args.stopOnEntry; + } + + if (typeof args.restart === 'boolean') { + this._restartMode = args.restart; + } + + if (args.localRoot) { + const localRoot = args.localRoot; + if (!Path.isAbsolute(localRoot)) { + this.sendRelativePathErrorResponse(response, 'localRoot', localRoot); + return true; + } + if (!FS.existsSync(localRoot)) { + this.sendNotExistErrorResponse(response, 'localRoot', localRoot); + return true; + } + this._localRoot = localRoot; + } + this._remoteRoot = args.remoteRoot; + + if (!this._sourceMaps) { + if (args.sourceMaps === undefined) { + args.sourceMaps = true; + } + if (typeof args.sourceMaps === 'boolean' && args.sourceMaps) { + const generatedCodeDirectory = args.outDir; + if (generatedCodeDirectory) { + if (!Path.isAbsolute(generatedCodeDirectory)) { + this.sendRelativePathErrorResponse(response, 'outDir', generatedCodeDirectory); + return true; + } + if (!FS.existsSync(generatedCodeDirectory)) { + this.sendNotExistErrorResponse(response, 'outDir', generatedCodeDirectory); + return true; + } + } + this._sourceMaps = new SourceMaps(this, generatedCodeDirectory, args.outFiles); + } + } + + return false; + } + + //---- attach request ----------------------------------------------------------------------------------------------------- + + protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { + + if (this._processCommonArgs(response, args)) { + return; + } + + this._attachMode = true; + + this._attach(response, args, args.port, args.address, args.timeout); + } + + /* + * shared 'attach' code used in launchRequest and attachRequest. + */ + private _attach(response: DebugProtocol.Response, args: CommonArguments, port: number, adr: string | undefined, timeout: number | undefined): void { + + if (!port) { + port = 5858; + } + this._port = port; + + let address: string; + if (!adr || adr === 'localhost') { + address = '127.0.0.1'; + } else { + address = adr; + } + + if (!timeout) { + timeout = NodeDebugSession.ATTACH_TIMEOUT; + } + + this.log('la', `_attach: address: ${address} port: ${port}`); + + let connected = false; + const socket = new Net.Socket(); + socket.connect(port, address); + + socket.on('connect', err => { + this.log('la', '_attach: connected'); + connected = true; + this._node.startDispatch(socket, socket); + + this._isRunning().then(running => { + + if (this._pollForNodeProcess) { + this._pollForNodeTermination(); + } + + setTimeout(() => { + this._injectDebuggerExtensions().then(_ => { + + if (!this._stepBack) { + // does runtime support 'step back'? + const v = this._node.embeddedHostVersion; // x.y.z version represented as (x*100+y)*100+z + if (!this._node.v8Version && v >= 70000) { + this._stepBack = true; + this.sendEvent(new CapabilitiesEvent({ supportsStepBack: true})); + } + } + + this.sendResponse(response); + this._startInitialize(!running); + }); + }, 10); + + }).catch(resp => { + this._sendNodeResponse(response, resp); + }); + }); + + const endTime = new Date().getTime() + timeout; + socket.on('error', err => { + if (connected) { + // since we are connected this error is fatal + this.sendErrorResponse(response, 2010, localize('VSND2010', "Cannot connect to runtime process (reason: {0}).", '{_error}'), { _error: err.message }); + } else { + // we are not yet connected so retry a few times + if ((err).code === 'ECONNREFUSED' || (err).code === 'ECONNRESET') { + const now = new Date().getTime(); + if (now < endTime) { + setTimeout(() => { + this.log('la', '_attach: retry socket.connect'); + socket.connect(port, address); + }, 200); // retry after 200 ms + } else { + if (typeof args.port === 'number') { + this.sendErrorResponse(response, 2033, localize('VSND2033', "Cannot connect to runtime; make sure that runtime is in 'legacy' debug mode.")); + } else { + this.sendErrorResponse(response, 2034, localize('VSND2034', "Cannot connect to runtime via 'legacy' protocol; try to use 'inspector' protocol.")); + } + } + } else { + this.sendErrorResponse(response, 2010, localize('VSND2010', "Cannot connect to runtime process (reason: {0}).", '{_error}'), { _error: err.message }); + } + } + }); + + socket.on('end', err => { + this._terminated('socket end'); + }); + } + + /** + * Determine whether the runtime is running or stopped. + * We do this by running an 'evaluate' request + * (a benevolent side effect of the evaluate is to find the process id and runtime version). + */ + private _isRunning() : Promise { + return new Promise((completeDispatch, errorDispatch) => { + this._isRunningWithRetry(0, completeDispatch, errorDispatch); + }); + } + + private _isRunningWithRetry(retryCount: number, completeDispatch: (value: boolean) => void, errorDispatch: (error: V8EvaluateResponse) => void) : void { + + this._node.command('evaluate', { expression: 'process.pid', global: true }, (resp: V8EvaluateResponse) => { + + if (resp.success && resp.body.value !== undefined) { + this._nodeProcessId = +resp.body.value; + this.log('la', `__initialize: got process id ${this._nodeProcessId} from node`); + this.logNodeVersion(); + } else { + if (resp.message.indexOf('process is not defined') >= 0) { + this.log('la', '__initialize: process not defined error; got no pid'); + resp.success = true; // continue and try to get process.pid later + } + } + + if (resp.success) { + completeDispatch(resp.running); + } else { + this.log('la', '__initialize: retrieving process id from node failed'); + + if (retryCount < 4) { + setTimeout(() => { + // recurse + this._isRunningWithRetry(retryCount+1, completeDispatch, errorDispatch); + }, 100); + return; + } else { + errorDispatch(resp); + } + } + }); + } + + private logNodeVersion(): void { + this._node.command('evaluate', { expression: 'process.version', global: true }, (resp: V8EvaluateResponse) => { + if (resp.success && resp.body.value !== undefined) { + const version = resp.body.value; + /* __GDPR__ + "nodeVersion" : { + "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.sendEvent(new OutputEvent('nodeVersion', 'telemetry', { version })); + this.log('la', `_initialize: target node version: ${version}`); + } + }); + } + + private _pollForNodeTermination() : void { + const id = setInterval(() => { + try { + if (this._nodeProcessId > 0) { + (process).kill(this._nodeProcessId, 0); // node.d.ts doesn't like number argumnent + } else { + clearInterval(id); + } + } catch(e) { + clearInterval(id); + this._terminated('node process kill exception'); + } + }, NodeDebugSession.NODE_TERMINATION_POLL_INTERVAL); + } + + /* + * Inject code into node.js to address slowness issues when inspecting large data structures. + */ + private _injectDebuggerExtensions() : Promise { + + if (this._tryToInjectExtension) { + + const v = this._node.embeddedHostVersion; // x.y.z version represented as (x*100+y)*100+z + + if (this._node.v8Version && ((v >= 1200 && v < 10000) || (v >= 40301 && v < 50000) || (v >= 50600))) { + try { + const contents = FS.readFileSync(Path.join(__dirname, NodeDebugSession.DEBUG_INJECTION), 'utf8'); + + const args = { + expression: contents, + global: true, + disable_break: true + }; + + return this._node.evaluate(args).then(resp => { + this.log('la', `_injectDebuggerExtensions: code injection successful`); + this._nodeInjectionAvailable = true; + return true; + }).catch(resp => { + this.log('la', `_injectDebuggerExtensions: code injection failed with error '${resp.message}'`); + return true; + }); + + } catch(e) { + // fall through + } + } + } + return Promise.resolve(true); + } + + /* + * start the initialization sequence: + * 1. wait for 'break-on-entry' (with timeout) + * 2. send 'inititialized' event in order to trigger setBreakpointEvents request from client + * 3. prepare for sending 'break-on-entry' or 'continue' later in configurationDoneRequest() + */ + private _startInitialize(stopped: boolean, n: number = 0): void { + + if (n === 0) { + this.log('la', `_startInitialize: stopped: ${stopped}`); + } + + // wait at most 500ms for receiving the break on entry event + // (since in attach mode we cannot enforce that node is started with --debug-brk, we cannot assume that we receive this event) + + if (!this._gotEntryEvent && n < 10) { + setTimeout(() => { + // recurse + this._startInitialize(stopped, n+1); + }, 50); + return; + } + + if (this._gotEntryEvent) { + this.log('la', `_startInitialize: got break on entry event after ${n} retries`); + if (this._nodeProcessId <= 0) { + // if we haven't gotten a process pid so far, we try it again + this._node.command('evaluate', { expression: 'process.pid', global: true }, (resp: V8EvaluateResponse) => { + if (resp.success && resp.body.value !== undefined) { + this._nodeProcessId = +resp.body.value; + this.log('la', `_initialize: got process id ${this._nodeProcessId} from node (2nd try)`); + this.logNodeVersion(); + } + this._startInitialize2(stopped); + }); + } else { + this._startInitialize2(stopped); + } + } else { + this.log('la', `_startInitialize: no entry event after ${n} retries; giving up`); + + this._gotEntryEvent = true; // we pretend to got one so that no 'entry' event will show up later... + + this._node.command('frame', null, (resp: V8FrameResponse) => { + if (resp.success) { + const s = this._getValueFromCache(resp.body.script); + this._rememberEntryLocation(s.name, resp.body.line, resp.body.column); + } + + this._startInitialize2(stopped); + }); + } + } + + private _startInitialize2(stopped: boolean): void { + // request UI to send breakpoints + this.log('la', '_startInitialize2: fire initialized event'); + this.sendEvent(new InitializedEvent()); + + this._attachSuccessful = true; + + // in attach-mode we don't know whether the debuggee has been launched in 'stop on entry' mode + // so we use the stopped state of the VM + if (this._attachMode) { + this.log('la', `_startInitialize2: in attach mode we guess stopOnEntry flag to be '${stopped}''`); + if (this._stopOnEntry === undefined) { + this._stopOnEntry = stopped; + } + } + + if (this._stopOnEntry) { + // user has requested 'stop on entry' so send out a stop-on-entry event + this.log('la', '_startInitialize2: fire stop-on-entry event'); + this._sendStoppedEvent('entry'); + } + else { + // since we are stopped but UI doesn't know about this, remember that we later do the right thing in configurationDoneRequest() + if (this._gotDebuggerEvent) { + this._needDebuggerEvent = true; + } else { + this.log('la', `_startInitialize2: remember to do a 'Continue' later`); + this._needContinue = true; + } + } + } + + //---- disconnect request ------------------------------------------------------------------------------------------------- + + protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments): void { + + if (!this._isWSL && this._nodeProcessId > 0) { + process.kill(this._nodeProcessId, 'SIGINT'); + } + + this.log('la', 'terminateRequest: send response'); + this.sendResponse(response); + } + + protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void { + + this.shutdown(); + + this.log('la', 'disconnectRequest: send response'); + this.sendResponse(response); + } + + /** + * Overridden from DebugSession: + * attach: disconnect from node + * launch: kill node & subprocesses + */ + public shutdown(): void { + + if (!this._inShutdown) { + this._inShutdown = true; + + if (this._attachMode) { + + // disconnect only in attach mode since otherwise node continues to run until it is killed + this._node.command('disconnect'); // we don't wait for reponse + + // stop socket connection (otherwise node.js dies with ECONNRESET on Windows) + this._node.stop(); + + } else { + + // stop socket connection (otherwise node.js dies with ECONNRESET on Windows) + this._node.stop(); + + this.log('la', 'shutdown: kill debugee and sub-processes'); + + let pid = this._processId; + this._processId = -1; + + if (this._isWSL) { + + // kill the whole process tree by starting with the launched runtimeExecutable + if (pid > 0) { + NodeDebugSession.killTree(pid); + } + + // under WSL killing the "bash" shell on the Windows side does not automatically kill node.js on the linux side + // so let's kill the node.js process on the linux side explicitly + const node_pid = this._nodeProcessId; + if (node_pid > 0) { + this._nodeProcessId = -1; + try { + WSL.spawnSync(true, '/bin/kill', [ '-9', node_pid.toString() ]); + } catch (err) { + } + } + + } else { + + // backward compatibilty + if (this._nodeProcessId > 0) { + pid = this._nodeProcessId; + this._nodeProcessId = -1; + } + if (pid > 0) { + NodeDebugSession.killTree(pid); + } + } + } + + super.shutdown(); + } + } + + //--- set breakpoints request --------------------------------------------------------------------------------------------- + + protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { + + this.log('bp', `setBreakPointsRequest: ${JSON.stringify(args.source)} ${JSON.stringify(args.breakpoints)}`); + + const sbs = new Array(); + // prefer the new API: array of breakpoints + if (args.breakpoints) { + for (let b of args.breakpoints) { + + let hitter: HitterFunction | undefined; + if (b.hitCondition) { + const result = NodeDebugSession.HITCOUNT_MATCHER.exec(b.hitCondition.trim()); + if (result && result.length >= 3) { + let op = result[1] || '>='; + if (op === '=') { + op = '=='; + } + const value = result[2]; + const expr = op === '%' + ? `return (hitcnt % ${value}) === 0;` + : `return hitcnt ${op} ${value};`; + hitter = Function('hitcnt', expr); + } else { + // error + } + } + + sbs.push(new InternalSourceBreakpoint( + this.convertClientLineToDebugger(b.line), + typeof b.column === 'number' ? this.convertClientColumnToDebugger(b.column) : 0, + b.condition, b.logMessage, hitter) + ); + } + } else if (args.lines) { + // deprecated API: convert line number array + for (let l of args.lines) { + sbs.push(new InternalSourceBreakpoint(this.convertClientLineToDebugger(l))); + } + } + + const source = args.source; + const sourcePath = source.path ? this.convertClientPathToDebugger(source.path) : undefined; + + if (sourcePath) { + // as long as node debug doesn't implement 'hot code replacement' we have to mark all breakpoints as unverified. + + let keepUnverified = false; + + if (this._modifiedSources.has(sourcePath)) { + keepUnverified = true; + } else { + if (typeof args.sourceModified === 'boolean' && args.sourceModified) { + keepUnverified = true; + this._modifiedSources.add(sourcePath); + } + } + + if (keepUnverified) { + const message = localize('file.on.disk.changed', "Unverified because file on disk has changed. Please restart debug session."); + for (let ibp of sbs) { + ibp.verificationMessage = message; + } + } + } + + if (source.adapterData) { + + if (source.adapterData.inlinePath) { + // a breakpoint in inlined source: we need to source map + this._mapSourceAndUpdateBreakpoints(response, source.adapterData.inlinePath, sbs); + return; + } + + if (source.adapterData.remotePath) { + // a breakpoint in a remote file: don't try to source map + this._updateBreakpoints(response, source.adapterData.remotePath, -1, sbs); + return; + } + } + + if (sourcePath && NodeDebugSession.NODE_INTERNALS_PREFIX.test(sourcePath)) { + + // an internal module + this._findScript(this._pathToScript(sourcePath)).then(scriptId => { + if (scriptId >= 0) { + this._updateBreakpoints(response, null, scriptId, sbs); + } else { + this.sendErrorResponse(response, 2019, localize('VSND2019', "Internal module {0} not found.", '{_module}'), { _module: sourcePath }); + } + }); + return; + } + + if (typeof source.sourceReference === 'number' && source.sourceReference > 0) { + const srcSource = this._sourceHandles.get(source.sourceReference); + if (srcSource && srcSource.scriptId) { + this._updateBreakpoints(response, null, srcSource.scriptId, sbs); + return; + } + } + + if (sourcePath) { + this._mapSourceAndUpdateBreakpoints(response, sourcePath, sbs); + return; + } + + this.sendErrorResponse(response, 2012, 'No valid source specified.', null, ErrorDestination.Telemetry); + } + + private _mapSourceAndUpdateBreakpoints(response: DebugProtocol.SetBreakpointsResponse, path: string, lbs: InternalSourceBreakpoint[]) : void { + + let generated = ''; + + Promise.resolve(generated).then(generated => { + + if (this._sourceMaps) { + return this._sourceMaps.MapPathFromSource(path); + } + return generated; + + }).then(generated => { + + if (generated !== null && PathUtils.pathCompare(generated, path)) { // if generated and source are the same we don't need a sourcemap + this.log('bp', `_mapSourceAndUpdateBreakpoints: source and generated are same -> ignore sourcemap`); + generated = ''; + } + + if (generated) { + + // source map line numbers + Promise.all(lbs.map(lbrkpt => this._sourceMaps.MapFromSource(path, lbrkpt.line, lbrkpt.column))).then(mapResults => { + + for (let i = 0; i < lbs.length; i++) { + const lb = lbs[i]; + const mapresult = mapResults[i]; + if (mapresult) { + this.log('sm', `_mapSourceAndUpdateBreakpoints: src: '${path}' ${lb.line}:${lb.column} -> gen: '${mapresult.path}' ${mapresult.line}:${mapresult.column}`); + if (mapresult.path !== generated) { + // this source line maps to a different destination file -> this is not supported, ignore breakpoint by setting line to -1 + lb.line = -1; + } else { + lb.line = mapresult.line; + lb.column = mapresult.column; + } + } else { + this.log('sm', `_mapSourceAndUpdateBreakpoints: src: '${path}' ${lb.line}:${lb.column} -> gen: couldn't be mapped; breakpoint ignored`); + lb.line = -1; + } + } + + path = generated; + path = this._localToRemote(path); + + this._updateBreakpoints(response, path, -1, lbs, true); + }); + + return; + } + + if (!NodeDebugSession.isJavaScript(path)) { + // ignore all breakpoints for this source + for (let lb of lbs) { + lb.line = -1; + } + } + + // try to convert local path to remote path + path = this._localToRemote(path); + + this._updateBreakpoints(response, path, -1, lbs, false); + }); + } + + /* + * clear and set all breakpoints of a given source. + */ + private _updateBreakpoints(response: DebugProtocol.SetBreakpointsResponse, path: string | null, scriptId: number, lbs: InternalSourceBreakpoint[], sourcemap = false): void { + + // clear all existing breakpoints for the given path or script ID + this._node.listBreakpoints().then(nodeResponse => { + + const toClear = new Array(); + + const path_regexp = this._pathToRegexp(path); + + // try to match breakpoints + for (let breakpoint of nodeResponse.body.breakpoints) { + switch (breakpoint.type) { + case 'scriptId': + if (scriptId === breakpoint.script_id) { + toClear.push(breakpoint.number); + } + break; + case 'scriptRegExp': + if (path_regexp === breakpoint.script_regexp) { + toClear.push(breakpoint.number); + } + break; + } + } + + return this._clearBreakpoints(toClear); + + }).then( () => { + + return Promise.all(lbs.map(bp => this._setBreakpoint(scriptId, path, bp, sourcemap))); + + }).then(result => { + + response.body = { + breakpoints: result + }; + this.sendResponse(response); + this.log('bp', `_updateBreakpoints: result ${JSON.stringify(result)}`); + + }).catch(nodeResponse => { + this._sendNodeResponse(response, nodeResponse); + }); + } + + /* + * Clear breakpoints by their ids. + */ + private _clearBreakpoints(ids: Array) : Promise { + return Promise.all(ids.map(id => this._node.clearBreakpoint({ breakpoint: id }))).then(response => { + return; + }).catch(err => { + return; // ignore errors + }); + } + + /* + * register a single breakpoint with node. + */ + private _setBreakpoint(scriptId: number, path: string | null, lb: InternalSourceBreakpoint, sourcemap: boolean) : Promise { + + if (lb.line < 0) { + // ignore this breakpoint because it couldn't be source mapped successfully + const bp: DebugProtocol.Breakpoint = new Breakpoint(false); + bp.message = localize('sourcemapping.fail.message', "Breakpoint ignored because generated code not found (source map problem?)."); + return Promise.resolve(bp); + } + + if (lb.line === 0) { + lb.column += NodeDebugSession.FIRST_LINE_OFFSET; + } + + let args: V8SetBreakpointArgs; + + if (scriptId > 0) { + args = { + type: 'scriptId', + target: scriptId, + line: lb.line, + column: lb.column, + condition: lb.condition + }; + } else { + args = { + type: 'scriptRegExp', + target: this._pathToRegexp(path), + line: lb.line, + column: lb.column, + condition: lb.condition + }; + } + + return this._node.setBreakpoint(args).then(resp => { + + this.log('bp', `_setBreakpoint: ${JSON.stringify(args)}`); + + if (lb.hitter) { + this._hitCounts.set(resp.body.breakpoint, lb); + } + + let actualLine = args.line; + let actualColumn = args.column; + + const al = resp.body.actual_locations; + if (al.length > 0) { + actualLine = al[0].line; + actualColumn = this._adjustColumn(actualLine, al[0].column); + } + + let actualSrcLine = actualLine; + let actualSrcColumn = actualColumn; + + if (path && sourcemap) { + + if (actualLine !== args.line || actualColumn !== args.column) { + // breakpoint location was adjusted by node.js so we have to map the new location back to source + + // first try to map the remote path back to local + const localpath = this._remoteToLocal(path); + + // then try to map js locations back to source locations + return this._sourceMaps.MapToSource(localpath, null, actualLine, actualColumn).then(mapresult => { + + if (mapresult) { + this.log('sm', `_setBreakpoint: bp verification gen: '${localpath}' ${actualLine}:${actualColumn} -> src: '${mapresult.path}' ${mapresult.line}:${mapresult.column}`); + actualSrcLine = mapresult.line; + actualSrcColumn = mapresult.column; + } else { + actualSrcLine = lb.orgLine; + actualSrcColumn = lb.orgColumn; + } + + return this._setBreakpoint2(lb, path, actualSrcLine, actualSrcColumn, actualLine, actualColumn); + }); + + } else { + actualSrcLine = lb.orgLine; + actualSrcColumn = lb.orgColumn; + } + } + + return this._setBreakpoint2(lb, path, actualSrcLine, actualSrcColumn, actualLine, actualColumn); + + }).catch(error => { + return new Breakpoint(false); + }); + } + + private async _setBreakpoint2(ibp: InternalSourceBreakpoint, path: string | null, actualSrcLine: number, actualSrcColumn: number, actualLine: number, actualColumn: number) : Promise { + + // nasty corner case: since we ignore the break-on-entry event we have to make sure that we + // stop in the entry point line if the user has an explicit breakpoint there (or if there is a 'debugger' statement). + // For this we check here whether a breakpoint is at the same location as the 'break-on-entry' location. + // If yes, then we plan for hitting the breakpoint instead of 'continue' over it! + + if (path && PathUtils.pathCompare(this._entryPath, path) && this._entryLine === actualLine && this._entryColumn === actualColumn) { // only relevant if the breakpoints matches entrypoint + + let conditionMet = true; // for regular breakpoints condition is always true + if (ibp.condition) { + // if conditional breakpoint we have to evaluate the condition because node didn't do it (because it stopped on entry). + conditionMet = await this.evaluateCondition(ibp.condition); + } + + if (!this._stopOnEntry && conditionMet) { + // we do not have to 'continue' but we have to generate a stopped event instead + this._needContinue = false; + this._needBreakpointEvent = true; + this.log('la', '_setBreakpoint2: remember to fire a breakpoint event later'); + } + + } + + if (ibp.verificationMessage) { + const bp: DebugProtocol.Breakpoint = new Breakpoint(false, this.convertDebuggerLineToClient(actualSrcLine), this.convertDebuggerColumnToClient(actualSrcColumn)); + bp.message = ibp.verificationMessage; + return bp; + } else { + return new Breakpoint(true, this.convertDebuggerLineToClient(actualSrcLine), this.convertDebuggerColumnToClient(actualSrcColumn)); + } + } + + private evaluateCondition(condition: string): Promise { + + const args = { + expression: condition, + frame: 0, // evaluate always in top frame + disable_break: true + }; + + return this._node.evaluate(args).then(response => { + return !!response.body.value; + }).catch(e => { + return false; + }); + } + + /** + * converts a path into a regular expression for use in the setbreakpoint request + */ + private _pathToRegexp(path: string | null): string | null { + + if (!path) { + return path; + } + + let escPath = path.replace(/([/\\.?*()^${}|[\]])/g, '\\$1'); + + // check for drive letter + if (/^[a-zA-Z]:\\/.test(path)) { + const u = escPath.substring(0, 1).toUpperCase(); + const l = u.toLowerCase(); + escPath = '[' + l + u + ']' + escPath.substring(1); + } + + /* + // support case-insensitive breakpoint paths + const escPathUpper = escPath.toUpperCase(); + const escPathLower = escPath.toLowerCase(); + + escPath = ''; + for (var i = 0; i < escPathUpper.length; i++) { + const u = escPathUpper[i]; + const l = escPathLower[i]; + if (u === l) { + escPath += u; + } else { + escPath += '[' + l + u + ']'; + } + } + */ + + const pathRegex = '^(.*[\\/\\\\])?' + escPath + '$'; // skips drive letters + return pathRegex; + } + + //--- set function breakpoints request ------------------------------------------------------------------------------------ + + protected setFunctionBreakPointsRequest(response: DebugProtocol.SetFunctionBreakpointsResponse, args: DebugProtocol.SetFunctionBreakpointsArguments): void { + + // clear all existing function breakpoints + this._clearBreakpoints(this._functionBreakpoints).then(() => { + + this._functionBreakpoints.length = 0; // clear array + + // set new function breakpoints + return Promise.all(args.breakpoints.map(functionBreakpoint => this._setFunctionBreakpoint(functionBreakpoint))); + + }).then(results => { + + response.body = { + breakpoints: results + }; + this.sendResponse(response); + + this.log('bp', `setFunctionBreakPointsRequest: result ${JSON.stringify(results)}`); + + }).catch(nodeResponse => { + + this._sendNodeResponse(response, nodeResponse); + }); + } + + /* + * Register a single function breakpoint with node. + * Returns verification info about the breakpoint. + */ + private _setFunctionBreakpoint(functionBreakpoint: DebugProtocol.FunctionBreakpoint): Promise { + + let args: V8SetBreakpointArgs = { + type: 'function', + target: functionBreakpoint.name + }; + if (functionBreakpoint.condition) { + args.condition = functionBreakpoint.condition; + } + + return this._node.setBreakpoint(args).then(resp => { + this._functionBreakpoints.push(resp.body.breakpoint); // remember function breakpoint ids + const locations = resp.body.actual_locations; + if (locations && locations.length > 0) { + const actualLine = this.convertDebuggerLineToClient(locations[0].line); + const actualColumn = this.convertDebuggerColumnToClient(this._adjustColumn(actualLine, locations[0].column)); + return new Breakpoint(true, actualLine, actualColumn); // TODO@AW add source + } else { + return new Breakpoint(true); + } + }).catch((resp: NodeV8Response) => { + return { + verified: false, + message: resp.message + }; + }); + } + + //--- set exception request ----------------------------------------------------------------------------------------------- + + protected setExceptionBreakPointsRequest(response: DebugProtocol.SetExceptionBreakpointsResponse, args: DebugProtocol.SetExceptionBreakpointsArguments): void { + + this.log('bp', `setExceptionBreakPointsRequest: ${JSON.stringify(args.filters)}`); + + let all = false; + let uncaught = false; + this._catchRejects = false; + + const filters = args.filters; + if (filters) { + all = filters.indexOf('all') >= 0; + uncaught = filters.indexOf('uncaught') >= 0; + this._catchRejects = filters.indexOf('rejects') >= 0; + } + + Promise.all([ + this._node.setExceptionBreak({ type: 'all', enabled: all }), + this._node.setExceptionBreak({ type: 'uncaught', enabled: uncaught }) + ]).then(r => { + this.sendResponse(response); + }).catch(err => { + this.sendErrorResponse(response, 2024, 'Configuring exception break options failed ({_nodeError}).', { _nodeError: err.message }, ErrorDestination.Telemetry); + }); + } + + //--- configuration done request ------------------------------------------------------------------------------------------ + + protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void { + + // all breakpoints are configured now -> start debugging + + let info = 'nothing to do'; + + if (this._needContinue) { // we do not break on entry + this._needContinue = false; + info = 'do a \'Continue\''; + this._node.command('continue'); + } + + if (this._needBreakpointEvent) { // we have to break on entry + this._needBreakpointEvent = false; + info = 'fire breakpoint event'; + this._sendBreakpointStoppedEvent(1); // we know the ID of the entry point breakpoint + } + + if (this._needDebuggerEvent) { // we have to break on entry + this._needDebuggerEvent = false; + info = 'fire debugger statement event'; + this._sendStoppedEvent('debugger_statement'); + } + + this.log('la', `configurationDoneRequest: ${info}`); + + this.sendResponse(response); + } + + //--- threads request ----------------------------------------------------------------------------------------------------- + + protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { + this._node.command('threads', null, (nodeResponse: NodeV8Response) => { + const threads = new Array(); + if (nodeResponse.success) { + const ths = nodeResponse.body.threads; + if (ths) { + for (let thread of ths) { + const id = thread.id; + if (id >= 0) { + threads.push(new Thread(id, `Thread (id: ${id})`)); + } + } + } + } + if (threads.length === 0) { // always return at least one thread + let name = NodeDebugSession.DUMMY_THREAD_NAME; + if (this._nodeProcessId > 0 && this._node.hostVersion) { + name = `${name} (${this._nodeProcessId}, ${this._node.hostVersion})`; + } else if (this._nodeProcessId > 0) { + name = `${name} (${this._nodeProcessId})`; + } else if (this._node.hostVersion) { + name = `${name} (${this._node.hostVersion})`; + } + threads.push(new Thread(NodeDebugSession.DUMMY_THREAD_ID, name)); + } + response.body = { + threads: threads + }; + this.sendResponse(response); + }); + } + + //--- stacktrace request -------------------------------------------------------------------------------------------------- + + protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { + + const threadReference = args.threadId; + const startFrame = typeof args.startFrame === 'number' ? args.startFrame : 0; + const maxLevels = typeof args.levels === 'number' ? args.levels : 10; + + let totalFrames = 0; + + if (threadReference !== NodeDebugSession.DUMMY_THREAD_ID) { + this.sendErrorResponse(response, 2014, 'Unexpected thread reference {_thread}.', { _thread: threadReference }, ErrorDestination.Telemetry); + return; + } + + const backtraceArgs : V8BacktraceArgs = { + fromFrame: startFrame, + toFrame: startFrame+maxLevels + }; + + this.log('va', `stackTraceRequest: backtrace ${startFrame} ${maxLevels}`); + this._node.backtrace(backtraceArgs).then(response => { + + if (response.body.totalFrames > 0 || response.body.frames) { + const frames = response.body.frames; + totalFrames = response.body.totalFrames; + return Promise.all(frames.map(frame => this._createStackFrame(frame))); + } else { + throw new Error('no stack'); + } + + }).then(stackframes => { + + response.body = { + stackFrames: stackframes, + totalFrames: totalFrames + }; + this.sendResponse(response); + + }).catch(error => { + + if (error.message === 'no stack') { + if (this._stoppedReason === 'pause') { + this.sendErrorResponse(response, 2022, localize('VSND2022', "No call stack because program paused outside of JavaScript.")); + } else { + this.sendErrorResponse(response, 2023, localize('VSND2023', "No call stack available.")); + } + } else { + this.sendErrorResponse(response, 2018, localize('VSND2018', "No call stack available ({_command}: {_error})."), { _command: error.command, _error: error.message } ); + } + + }); + } + + /** + * Create a single stack frame. + */ + private _createStackFrame(frame: V8Frame) : Promise { + + // resolve some refs + return this._resolveValues([ frame.script, frame.func, frame.receiver ]).then(() => { + + let line = frame.line; + let column = this._adjustColumn(line, frame.column); + + let src: Source | undefined; + + let origin = localize('origin.from.node', "read-only content from Node.js"); + + const script_val = this._getValueFromCache(frame.script); + if (script_val) { + let name = script_val.name; + let path: string | undefined; + + if (name) { + + if (this._mapToFilesOnDisk) { + + // try to map the script to a file in the workspace + + // first convert urls to paths + const u = URL.parse(name); + if (u.protocol === 'file:' && u.path) { + // a local file path + name = decodeURI(u.path); + } + + // we can only map absolute paths + if (PathUtils.isAbsolutePath(name)) { + + // with remote debugging path might come from a different OS + let remotePath = name; + + // if launch.json defines localRoot and remoteRoot try to convert remote path back to a local path + let localPath = this._remoteToLocal(remotePath); + if (localPath !== remotePath && this._attachMode) { + // assume attached to remote node process + origin = localize('origin.from.remote.node', "read-only content from remote Node.js"); + } + + // source mapping is enabled + if (this._sourceMaps) { + + // load script to find source reference + return this._loadScript(script_val.id).then(script => { + return this._createStackFrameFromSourceMap(frame, script.contents, name, localPath, remotePath, origin, line, column); + }); + } + + return this._createStackFrameFromPath(frame, name, localPath, remotePath, origin, line, column); + } + + // if we end up here, 'name' is not a path and is an internal module + path = this._scriptToPath(script_val); + origin = localize('origin.core.module', "read-only core module"); + + } else { + // do not map the script to a file in the workspace + // fall through + } + } + + if (!name) { + + if (typeof script_val.id !== 'number') { + // if the script has not ID something is seriously wrong: give up. + throw new Error('no script id'); + } + + // if a function is dynamically created from a string, its script has no name. + path = this._scriptToPath(script_val); + name = Path.basename(path); + } + + // source not found locally -> prepare to stream source content from node backend. + const sourceHandle = this._getScriptIdHandle(script_val.id); + src = this._createSource(false, name, path, sourceHandle, origin); + } + + return this._createStackFrameFromSource(frame, src, line, column); + + }); + } + + private _createSource(hasSource: boolean, name: string, path: string | undefined, sourceHandle: number = 0, origin?: string, data?: any): Source { + + let deemphasize = false; + if (path && this.isSkipped(path)) { + const skipFiles = localize('source.skipFiles', "skipped due to 'skipFiles'"); + deemphasize = true; + origin = origin ? `${origin} (${skipFiles})` : skipFiles; + } else if (!hasSource && this._smartStep && this._sourceMaps) { + const smartStep = localize('source.smartstep', "skipped due to 'smartStep'"); + deemphasize = true; + origin = origin ? `${origin} (${smartStep})` : smartStep; + } + + // make sure to only use the basename of a path + name = Path.basename(name); + + const src = new Source(name, path, sourceHandle, origin, data); + + if (deemphasize) { + (src).presentationHint = 'deemphasize'; + } + + return src; + } + + /** + * Creates a StackFrame when source maps are involved. + */ + private _createStackFrameFromSourceMap(frame: V8Frame, content: string, name: string, localPath: string, remotePath: string, origin: string, line: number, column: number) : Promise { + + return this._sourceMaps.MapToSource(localPath, content, line, column).then(mapresult => { + + if (mapresult) { + this.log('sm', `_createStackFrameFromSourceMap: gen: '${localPath}' ${line}:${column} -> src: '${mapresult.path}' ${mapresult.line}:${mapresult.column}`); + + return this._sameFile(mapresult.path, this._compareContents, 0, mapresult.content).then(same => { + + if (same) { + // use this mapping + const src = this._createSource(true, mapresult.path, this.convertDebuggerPathToClient(mapresult.path)); + return this._createStackFrameFromSource(frame, src, mapresult.line, mapresult.column); + } + + // file doesn't exist at path: if source map has inlined source use it + if (mapresult.content) { + this.log('sm', `_createStackFrameFromSourceMap: source '${mapresult.path}' doesn't exist -> use inlined source`); + const sourceHandle = this._getInlinedContentHandle(mapresult.content); + origin = localize('origin.inlined.source.map', "read-only inlined content from source map"); + const src = this._createSource(true, mapresult.path, undefined, sourceHandle, origin, { inlinePath: mapresult.path }); + return this._createStackFrameFromSource(frame, src, mapresult.line, mapresult.column); + } + + // no source found + this.log('sm', `_createStackFrameFromSourceMap: gen: '${localPath}' ${line}:${column} -> can't find source -> use generated file`); + return this._createStackFrameFromPath(frame, name, localPath, remotePath, origin, line, column); + }); + } + + this.log('sm', `_createStackFrameFromSourceMap: gen: '${localPath}' ${line}:${column} -> couldn't be mapped to source -> use generated file`); + return this._createStackFrameFromPath(frame, name, localPath, remotePath, origin, line, column); + }); + } + + private _getInlinedContentHandle(content: string) { + let handle = this._inlinedContentHandle.get(content); + if (!handle) { + handle = this._sourceHandles.create(new SourceSource(0, content)); + this._inlinedContentHandle.set(content, handle); + } + return handle; + } + + /** + * Creates a StackFrame from the given local path. + * The remote path is used if the local path doesn't exist. + */ + private _createStackFrameFromPath(frame: V8Frame, name: string, localPath: string, remotePath: string, origin: string, line: number, column: number) : Promise { + + const script_val = this._getValueFromCache(frame.script); + const script_id = script_val.id; + + return this._sameFile(localPath, this._compareContents, script_id).then(same => { + let src: Source; + if (same) { + // we use the file on disk + src = this._createSource(false, name, this.convertDebuggerPathToClient(localPath)); + } else { + // we use the script's content streamed from node + const sourceHandle = this._getScriptIdHandle(script_id); + src = this._createSource(false, name, undefined, sourceHandle, origin, { remotePath: remotePath }); // assume it is a remote path + } + return this._createStackFrameFromSource(frame, src, line, column); + }); + } + + private _getScriptIdHandle(scriptId: number) { + let handle = this._scriptId2Handle.get(scriptId); + if (!handle) { + handle = this._sourceHandles.create(new SourceSource(scriptId)); + this._scriptId2Handle.set(scriptId, handle); + } + return handle; + } + + /** + * Creates a StackFrame with the given source location information. + * The name of the frame is extracted from the frame. + */ + private _createStackFrameFromSource(frame: V8Frame, src: Source | undefined, line: number, column: number) : StackFrame { + + const name = this._getFrameName(frame); + const frameReference = this._frameHandles.create(frame); + return new StackFrame(frameReference, name, src, this.convertDebuggerLineToClient(line), this.convertDebuggerColumnToClient(column)); + } + + private _getFrameName(frame: V8Frame) { + let func_name: string | undefined; + const func_val = this._getValueFromCache(frame.func); + if (func_val) { + func_name = func_val.inferredName; + if (!func_name || func_name.length === 0) { + func_name = func_val.name; + } + } + if (!func_name || func_name.length === 0) { + func_name = localize('anonymous.function', "(anonymous function)"); + } + return func_name; + } + + /** + * Returns true if a file exists at path. + * If compareContents is true and a script_id is given, _sameFile verifies that the + * file's content matches the script's content. + */ + private _sameFile(path: string, compareContents: boolean, script_id: number, content?: string) : Promise { + + return this._existsFile(path).then(exists => { + + if (exists) { + + if (compareContents && (script_id || content)) { + + return Promise.all([ + this._readFile(path), + content + ? Promise.resolve(content) + : this._loadScript(script_id).then(script => script.contents) + ]).then(results => { + let fileContents = results[0]; + let contents = results[1]; + + // normalize EOL sequences + contents = contents.replace(/\r\n/g, '\n'); + fileContents = fileContents.replace(/\r\n/g, '\n'); + + // remove an optional shebang + fileContents = fileContents.replace(/^#!.*\n/, ''); + + // try to locate the file contents in the executed contents + const pos = contents.indexOf(fileContents); + return pos >= 0; + + }).catch(err => { + return false; + }); + } + return true; + + } + return false; + }); + } + + /** + * Returns (and caches) the file contents of path. + */ + private _readFile(path: string) : Promise { + + path= PathUtils.normalizeDriveLetter(path); + let file = this._files.get(path); + + if (!file) { + + this.log('ls', `__readFile: ${path}`); + + file = new Promise((completeDispatch, errorDispatch) => { + FS.readFile(path, 'utf8', (err, fileContents) => { + if (err) { + errorDispatch(err); + } else { + completeDispatch(PathUtils.stripBOM(fileContents)); + } + }); + }); + + this._files.set(path, file); + } + + return file; + } + + /** + * a Promise based version of 'exists' + */ + private _existsFile(path: string) : Promise { + return new Promise((completeDispatch, errorDispatch) => { + FS.exists(path, completeDispatch); + }); + } + + //--- scopes request ------------------------------------------------------------------------------------------------------ + + private static SCOPE_NAMES = [ + localize({ key: 'scope.global', comment: ['https://github.com/Microsoft/vscode/issues/4569'] }, "Global"), + localize({ key: 'scope.local', comment: ['https://github.com/Microsoft/vscode/issues/4569'] }, "Local"), + localize({ key: 'scope.with', comment: ['https://github.com/Microsoft/vscode/issues/4569'] }, "With"), + localize({ key: 'scope.closure', comment: ['https://github.com/Microsoft/vscode/issues/4569'] }, "Closure"), + localize({ key: 'scope.catch', comment: ['https://github.com/Microsoft/vscode/issues/4569'] }, "Catch"), + localize({ key: 'scope.block', comment: ['https://github.com/Microsoft/vscode/issues/4569'] }, "Block"), + localize({ key: 'scope.script', comment: ['https://github.com/Microsoft/vscode/issues/4569'] }, "Script") + ]; + + protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { + + const frame = this._frameHandles.get(args.frameId); + if (!frame) { + this.sendErrorResponse(response, 2020, 'stack frame not valid', null, ErrorDestination.Telemetry); + return; + } + const frameIx = frame.index; + const frameThis = this._getValueFromCache(frame.receiver); + + const scopesArgs: any = { + frame_index: frameIx, + frameNumber: frameIx + }; + let cmd = 'scopes'; + + if (this._nodeInjectionAvailable) { + cmd = 'vscode_scopes'; + scopesArgs.maxLocals = this._maxVariablesPerScope; + } + + this.log('va', `scopesRequest: scope ${frameIx}`); + this._node.command2(cmd, scopesArgs).then((scopesResponse: V8ScopeResponse) => { + + const scopes : V8Scope[] = scopesResponse.body.scopes; + + return Promise.all(scopes.map(scope => { + const type = scope.type; + const extra = type === 1 ? frameThis : undefined; + let expensive = type === 0; // global scope is expensive + + let scopeName: string; + if (type >= 0 && type < NodeDebugSession.SCOPE_NAMES.length) { + if (type === 1 && typeof scopesResponse.body.vscode_locals === 'number') { + expensive = true; + scopeName = localize({ key: 'scope.local.with.count', comment: ['https://github.com/Microsoft/vscode/issues/4569'] }, + "Local ({0} of {1})", scopesArgs.maxLocals, scopesResponse.body.vscode_locals); + } else { + scopeName = NodeDebugSession.SCOPE_NAMES[type]; + } + } else { + scopeName = localize('scope.unknown', "Unknown Scope Type: {0}", type); + } + + return this._resolveValues( [ scope.object ] ).then(resolved => { + const x = resolved[0]; + if (x) { + return new Scope(scopeName, this._variableHandles.create(new ScopeContainer(scope, x, extra)), expensive); + } + return new Scope(scopeName, 0); + }).catch(error => { + return new Scope(scopeName, 0); + }); + })); + + }).then(scopes => { + + // exception scope + if (frameIx === 0 && this._exception) { + const scopeName = localize({ key: 'scope.exception', comment: ['https://github.com/Microsoft/vscode/issues/4569'] }, "Exception"); + scopes.unshift(new Scope(scopeName, this._variableHandles.create(new PropertyContainer(undefined, this._exception.exception)))); + } + + response.body = { + scopes: scopes + }; + this.sendResponse(response); + + }).catch(error => { + // in case of error return empty scopes array + response.body = { scopes: [] }; + this.sendResponse(response); + }); + } + + //--- variables request --------------------------------------------------------------------------------------------------- + + protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void { + + const reference = args.variablesReference; + const variablesContainer = this._variableHandles.get(reference); + + if (variablesContainer) { + const filter: FilterType = (args.filter === 'indexed' || args.filter === 'named') ? args.filter : 'all'; + variablesContainer.Expand(this, filter, args.start, args.count).then(variables => { + variables.sort(NodeDebugSession.compareVariableNames); + response.body = { + variables: variables + }; + this.sendResponse(response); + }).catch(err => { + // in case of error return empty variables array + response.body = { + variables: [] + }; + this.sendResponse(response); + }); + } else { + // no container found: return empty variables array + response.body = { + variables: [] + }; + this.sendResponse(response); + } + } + + /* + * Returns indexed or named properties for the given structured object as a variables array. + * There are three modes: + * 'all': add all properties (indexed and named) + * 'indexed': add 'count' indexed properties starting at 'start' + * 'named': add only the named properties. + */ + public _createProperties(evalName: string | undefined, obj: V8Object, mode: FilterType, start = 0, count?: number) : Promise { + + if (obj && !obj.properties) { + + // if properties are missing, this is an indication that we are running injected code which doesn't return the properties for large objects + + if (this._nodeInjectionAvailable) { + const handle = obj.handle; + + if (typeof obj.vscode_indexedCnt === 'number' && typeof handle === 'number' && handle !== 0) { + + if (count === undefined) { + count = obj.vscode_indexedCnt; + } + + const args = { handle, mode, start, count }; + + return this._node.command2('vscode_slice', args).then(resp => { + const items = resp.body.result; + return Promise.all(items.map(item => { + return this._createVariable(evalName, item.name, item.value); + })); + }); + } + } + + // if we end up here, something went wrong... + return Promise.resolve([]); + } + + const selectedProperties = new Array(); + + let found_proto = false; + if (obj.properties) { + count = count || obj.properties.length; + for (let property of obj.properties) { + + if ('name' in property) { // bug #19654: only extract properties with a name + + const name = property.name; + + if (name === NodeDebugSession.PROTO) { + found_proto = true; + } + + switch (mode) { + case 'all': + selectedProperties.push(property); + break; + case 'named': + if (!isIndex(name)) { + selectedProperties.push(property); + } + break; + case 'indexed': + if (isIndex(name)) { + const ix = +name; + if (ix >= start && ix < start+count) { + selectedProperties.push(property); + } + } + break; + } + } + } + } + + // do we have to add the protoObject to the list of properties? + if (!found_proto && (mode === 'all' || mode === 'named')) { + const h = obj.handle; + if (h > 0) { // only add if not an internal debugger object + (obj.protoObject).name = NodeDebugSession.PROTO; + selectedProperties.push(obj.protoObject); + } + } + + return this._createPropertyVariables(evalName, obj, selectedProperties); + } + + /** + * Resolves the given properties and returns them as an array of Variables. + * If the properties are indexed (opposed to named), a value 'start' is added to the index number. + * If a value is undefined it probes for a getter. + */ + private _createPropertyVariables(evalName: string | undefined, obj: V8Object | null, properties: V8Property[], doPreview = true, start = 0) : Promise { + + return this._resolveValues(properties).then(() => { + return Promise.all(properties.map(property => { + const val = this._getValueFromCache(property); + + // create 'name' + let name: string; + if (isIndex(property.name)) { + const ix = +property.name; + name = `${start+ix}`; + } else { + name = property.name; + } + + // if value 'undefined' trigger a getter + if (this._node.v8Version && val.type === 'undefined' && !val.value && obj && obj.handle >= 0) { + + const args = { + expression: `obj['${name}']`, // trigger call to getter + additional_context: [ + { name: 'obj', handle: obj.handle } + ], + disable_break: true, + maxStringLength: NodeDebugSession.MAX_STRING_LENGTH + }; + + this.log('va', `_createPropertyVariables: trigger getter`); + return this._node.evaluate(args).then(response => { + return this._createVariable(evalName, name, response.body, doPreview); + }).catch(err => { + return this._createVar(this._getEvaluateName(evalName, name), name, 'undefined'); + }); + + } else { + return this._createVariable(evalName, name, val, doPreview); + } + })); + }); + } + + /** + * Create a Variable with the given name and value. + * For structured values the variable object will have a corresponding expander. + */ + public _createVariable(evalName: string | undefined, name: string, val: V8Handle, doPreview: boolean = true) : Promise { + + /* + if (!val) { + return Promise.resolve(null); + } + */ + + if (!name) { + name = '""'; + } + + const simple = val; + + const en = this._getEvaluateName(evalName, name); + + switch (val.type) { + + case 'undefined': + case 'null': + return Promise.resolve(this._createVar(en, name, val.type)); + + case 'string': + return this._createStringVariable(evalName, name, val, doPreview ? undefined : NodeDebugSession.PREVIEW_MAX_STRING_LENGTH); + case 'number': + if (typeof simple.value === 'number') { + return Promise.resolve(this._createVar(en, name, simple.value.toString())); + } + break; + case 'boolean': + if (typeof simple.value === 'boolean') { + return Promise.resolve(this._createVar(en, name, simple.value.toString().toLowerCase())); // node returns these boolean values capitalized + } + break; + + case 'set': + case 'map': + if (this._node.v8Version) { + return this._createSetMapVariable(evalName, name, val); + } + // fall through and treat sets and maps as objects + + case 'object': + case 'function': + case 'regexp': + case 'promise': + case 'generator': + case 'error': + + const object = val; + let value = object.className; + + switch (value) { + + case 'Array': + case 'ArrayBuffer': + case 'Int8Array': case 'Uint8Array': case 'Uint8ClampedArray': + case 'Int16Array': case 'Uint16Array': + case 'Int32Array': case 'Uint32Array': + case 'Float32Array': case 'Float64Array': + return this._createArrayVariable(evalName, name, val, doPreview); + + case 'RegExp': + if (typeof object.text === 'string') { + return Promise.resolve(this._createVar(en, name, object.text, this._variableHandles.create(new PropertyContainer(en, val)))); + } + break; + + case 'Generator': + case 'Object': + return this._resolveValues(object.constructorFunction ? [object.constructorFunction] : [] ).then((resolved: V8Function[]) => { + + if (resolved.length > 0 && resolved[0]) { + const constructor_name = resolved[0].name; + if (constructor_name) { + value = constructor_name; + } + } + + if (val.type === 'promise' || val.type === 'generator') { + if (object.status) { // promises and generators have a status attribute + value += ` { ${object.status} }`; + } + } else { + + if (object.properties) { + return this._objectPreview(object, doPreview).then(preview => { + if (preview) { + value = `${value} ${preview}`; + } + return this._createVar(en, name, value, this._variableHandles.create(new PropertyContainer(en, val))); + }); + } + } + + return this._createVar(en, name, value, this._variableHandles.create(new PropertyContainer(en, val))); + }); + //break; + + case 'Function': + case 'Error': + default: + if (object.text) { + let text = object.text; + if (text.indexOf('\n') >= 0) { + // replace body of function with '...' + const pos = text.indexOf('{'); + if (pos > 0) { + text = text.substring(0, pos) + '{ … }'; + } + } + value = text; + } + break; + } + return Promise.resolve(this._createVar(en, name, value, this._variableHandles.create(new PropertyContainer(en, val)))); + + case 'frame': + default: + break; + } + return Promise.resolve(this._createVar(en, name, simple.value ? simple.value.toString() : 'undefined')); + } + + private _createVar(evalName: string | undefined, name: string, value: string, ref?: number, indexedVariables?: number, namedVariables?: number) { + const v: DebugProtocol.Variable = new Variable(name, value, ref, indexedVariables, namedVariables); + if (evalName) { + v.evaluateName = evalName; + } + return v; + } + + private _getEvaluateName(parentEvaluateName: string | undefined, name: string): string | undefined { + + if (parentEvaluateName === undefined) { + return undefined; + } + + if (!parentEvaluateName) { + return name; + } + + let nameAccessor: string; + if (/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) { + nameAccessor = '.' + name; + } else if (/^\d+$/.test(name)) { + nameAccessor = `[${name}]`; + } else { + nameAccessor = `[${JSON.stringify(name)}]`; + } + + return parentEvaluateName + nameAccessor; + } + + /** + * creates something like this: {a: 123, b: "hi", c: true …} + */ + private _objectPreview(object: V8Object, doPreview: boolean): Promise { + + if (doPreview && object && object.properties && object.properties.length > 0) { + + const propcnt = object.properties.length; + + return this._createPropertyVariables(undefined, object, object.properties.slice(0, NodeDebugSession.PREVIEW_PROPERTIES), false).then(props => { + + let preview = '{'; + for (let i = 0; i < props.length; i++) { + + preview += `${props[i].name}: ${props[i].value}`; + + if (i < props.length-1) { + preview += ', '; + } else { + if (propcnt > NodeDebugSession.PREVIEW_PROPERTIES) { + preview += ' …'; + } + } + } + preview += '}'; + + return preview; + }); + } + + return Promise.resolve(null); + } + + /** + * creates something like this: [ 1, 2, 3 …] + */ + private _arrayPreview(array: V8Object, length: number, doPreview: boolean): Promise { + + if (doPreview && array && array.properties && length > 0) { + + const previewProps = new Array(); + for (let i = 0; i < array.properties.length; i++) { + const p = array.properties[i]; + if (isIndex(p.name)) { + const ix = +p.name; + if (ix >= 0 && ix < NodeDebugSession.PREVIEW_PROPERTIES) { + previewProps.push(p); + if (previewProps.length >= NodeDebugSession.PREVIEW_PROPERTIES) { + break; + } + } + } + } + + return this._createPropertyVariables(undefined, array, previewProps, false).then(props => { + + let preview = '['; + for (let i = 0; i < props.length; i++) { + + preview += `${props[i].value}`; + + if (i < props.length-1) { + preview += ', '; + } else { + if (length > NodeDebugSession.PREVIEW_PROPERTIES) { + preview += ' …'; + } + } + } + preview += ']'; + + return preview; + }); + } + + return Promise.resolve(null); + } + + //--- long array support + + private _createArrayVariable(evalName: string | undefined, name: string, array: V8Object, doPreview: boolean) : Promise { + + return this._getArraySize(array).then(pair => { + + let indexedSize = 0; + let namedSize = 0; + let arraySize = ''; + + if (pair.length >= 2) { + indexedSize = pair[0]; + namedSize = pair[1]; + arraySize = indexedSize.toString(); + } + + return this._arrayPreview(array, indexedSize, doPreview).then(preview => { + let v = `${array.className}[${arraySize}]`; + if (preview) { + v = `${v} ${preview}`; + } + const en = this._getEvaluateName(evalName, name); + return this._createVar(en, name, v, this._variableHandles.create(new PropertyContainer(en, array)), indexedSize, namedSize); + }); + }); + } + + private _getArraySize(array: V8Object) : Promise { + + if (typeof array.vscode_indexedCnt === 'number' && typeof array.vscode_namedCnt === 'number') { + return Promise.resolve([ array.vscode_indexedCnt, array.vscode_namedCnt ]); + } + + if (this._node.v8Version) { + + const args = { + expression: array.className === 'ArrayBuffer' ? `JSON.stringify([ array.byteLength, 1 ])` : `JSON.stringify([ array.length, Object.keys(array).length+1-array.length ])`, + disable_break: true, + additional_context: [ + { name: 'array', handle: array.handle } + ] + }; + + this.log('va', `_getArraySize: array.length`); + return this._node.evaluate(args).then(response => { + return JSON.parse(response.body.value); + }); + } + + return Promise.resolve([]); + } + + //--- ES6 Set/Map support + + private _createSetMapVariable(evalName: string | undefined, name: string, obj: V8Handle) : Promise { + + const args = { + // initially we need only the size + expression: `JSON.stringify([ obj.size, Object.keys(obj).length ])`, + disable_break: true, + additional_context: [ + { name: 'obj', handle: obj.handle } + ] + }; + + this.log('va', `_createSetMapVariable: ${obj.type}.size`); + return this._node.evaluate(args).then(response => { + + const pair = JSON.parse(response.body.value); + const indexedSize = pair[0]; + const namedSize = pair[1]; + const typename = (obj.type === 'set') ? 'Set' : 'Map'; + const en = this._getEvaluateName(evalName, name); + return this._createVar(en, name, `${typename}[${indexedSize}]`, this._variableHandles.create(new SetMapContainer(en, obj)), indexedSize, namedSize); + }); + } + + public _createSetMapProperties(evalName: string | undefined, obj: V8Handle) : Promise { + + const args = { + expression: `var r = {}; Object.keys(obj).forEach(k => { r[k] = obj[k] }); r`, + disable_break: true, + additional_context: [ + { name: 'obj', handle: obj.handle } + ] + }; + + return this._node.evaluate(args).then(response => { + return this._createProperties(evalName, response.body, 'named'); + }); + } + + public _createSetElements(set: V8Handle, start: number, count: number) : Promise { + + const args = { + expression: `var r = [], i = 0; set.forEach(v => { if (i >= ${start} && i < ${start+count}) r.push(v); i++; }); r`, + disable_break: true, + additional_context: [ + { name: 'set', handle: set.handle } + ] + }; + + this.log('va', `_createSetElements: set.slice ${start} ${count}`); + return this._node.evaluate(args).then(response => { + + const properties = response.body.properties || []; + const selectedProperties = new Array(); + + for (let property of properties) { + if (isIndex(property.name)) { + selectedProperties.push(property); + } + } + + return this._createPropertyVariables(undefined, null, selectedProperties, true, start); + }); + } + + public _createMapElements(map: V8Handle, start: number, count: number) : Promise { + + // for each slot of the map we create three slots in a helper array: label, key, value + const args = { + expression: `var r=[],i=0; map.forEach((v,k) => { if (i >= ${start} && i < ${start+count}) { r.push(k+' → '+v); r.push(k); r.push(v);} i++; }); r`, + disable_break: true, + additional_context: [ + { name: 'map', handle: map.handle } + ] + }; + + this.log('va', `_createMapElements: map.slice ${start} ${count}`); + return this._node.evaluate(args).then(response => { + + const properties = response.body.properties || []; + const selectedProperties = new Array(); + + for (let property of properties) { + if (isIndex(property.name)) { + selectedProperties.push(property); + } + } + + return this._resolveValues(selectedProperties).then(() => { + const variables = new Array(); + for (let i = 0; i < selectedProperties.length; i += 3) { + + const key = this._getValueFromCache(selectedProperties[i+1]); + const val = this._getValueFromCache(selectedProperties[i+2]); + + const expander = new Expander((start: number, count: number) => { + return Promise.all([ + this._createVariable(undefined, 'key', key), + this._createVariable(undefined, 'value', val) + ]); + }); + + const x = this._getValueFromCache(selectedProperties[i]); + variables.push(this._createVar(undefined, (start + (i/3)).toString(), x.value, this._variableHandles.create(expander))); + } + return variables; + }); + }); + } + + //--- long string support + + private _createStringVariable(evalName: string | undefined, name: string, val: V8Simple, maxLength: number | undefined) : Promise { + + let str_val = val.value; + + const en = this._getEvaluateName(evalName, name); + + if (typeof maxLength === 'number') { + if (str_val.length > maxLength) { + str_val = str_val.substr(0, maxLength) + '…'; + } + return Promise.resolve(this._createVar(en, name, this._escapeStringValue(str_val))); + } + + if (this._node.v8Version && NodeDebugSession.LONG_STRING_MATCHER.exec(str_val)) { + + const args = { + expression: `str`, + disable_break: true, + additional_context: [ + { name: 'str', handle: val.handle } + ], + maxStringLength: NodeDebugSession.MAX_STRING_LENGTH + }; + + this.log('va', `_createStringVariable: get full string`); + return this._node.evaluate(args).then(response => { + str_val = response.body.value; + return this._createVar(en, name, this._escapeStringValue(str_val)); + }); + + } else { + return Promise.resolve(this._createVar(en, name, this._escapeStringValue(str_val))); + } + } + + private _escapeStringValue(s: string) { + /* disabled for now because chrome dev tools doesn't escape quotes either + if (s) { + s = s.replace(/\"/g, '\\"'); // escape quotes because they are used as delimiters for a string + } + */ + return `"${s}"`; + } + + //--- setVariable request ------------------------------------------------------------------------------------------------- + + protected setVariableRequest(response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetVariableArguments): void { + const reference = args.variablesReference; + const name = args.name; + const value = args.value; + const variablesContainer = this._variableHandles.get(reference); + if (variablesContainer) { + variablesContainer.SetValue(this, name, value).then(newVar => { + const v: DebugProtocol.Variable = newVar; + response.body = { + value: v.value + }; + if (v.type) { + response.body.type = v.type; + } + if (v.variablesReference) { + response.body.variablesReference = v.variablesReference; + } + if (typeof v.indexedVariables === 'number') { + response.body.indexedVariables = v.indexedVariables; + } + if (typeof v.namedVariables === 'number') { + response.body.namedVariables = v.namedVariables; + } + this.sendResponse(response); + }).catch(err => { + this.sendErrorResponse(response, 2004, err.message); + }); + } else { + this.sendErrorResponse(response, 2025, Expander.SET_VALUE_ERROR); + } + } + + public _setVariableValue(frame: number, scope: number, name: string, value: string) : Promise { + + // first we are evaluating the new value + + const evalArgs = { + expression: value, + disable_break: true, + maxStringLength: NodeDebugSession.MAX_STRING_LENGTH, + frame: frame + }; + + return this._node.evaluate(evalArgs).then(evalResponse => { + + const args: V8SetVariableValueArgs = { + scope: { + frameNumber: frame, + number: scope + }, + name: name, + newValue: evalResponse.body + }; + + return this._node.setVariableValue(args).then(response => { + return this._createVariable(undefined, '_setVariableValue', response.body.newValue); + }); + }); + } + + public _setPropertyValue(objHandle: number, propName: string, value: string) : Promise { + + if (this._node.v8Version) { + + // we are doing the evaluation of the new value and the assignment to an object property in a single evaluate. + + const args = { + global: true, + expression: `obj['${propName}'] = ${value}`, + disable_break: true, + additional_context: [ + { name: 'obj', handle: objHandle } + ], + maxStringLength: NodeDebugSession.MAX_STRING_LENGTH + }; + + return this._node.evaluate(args).then(response => { + return this._createVariable(undefined, '_setpropertyvalue', response.body); + }); + } + + return Promise.reject(new Error(Expander.SET_VALUE_ERROR)); + } + + //--- pause request ------------------------------------------------------------------------------------------------------- + + protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) : void { + this._node.command('suspend', null, (nodeResponse) => { + if (nodeResponse.success) { + this._stopped('pause'); + this.sendResponse(response); + this._sendStoppedEvent('pause'); + } else { + this._sendNodeResponse(response, nodeResponse); + } + }); + } + + //--- continue request ---------------------------------------------------------------------------------------------------- + + protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { + this._disableSkipFiles = false; + this._node.command('continue', null, nodeResponse => { + this._sendNodeResponse(response, nodeResponse); + }); + } + + //--- step request -------------------------------------------------------------------------------------------------------- + + protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { + this._node.command('continue', { stepaction: 'next' }, nodeResponse => { + this._sendNodeResponse(response, nodeResponse); + }); + } + + protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) : void { + this._node.command('continue', { stepaction: 'in' }, nodeResponse => { + this._sendNodeResponse(response, nodeResponse); + }); + } + + protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) : void { + this._disableSkipFiles = false; + this._node.command('continue', { stepaction: 'out' }, nodeResponse => { + this._sendNodeResponse(response, nodeResponse); + }); + } + + protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments) : void { + this._node.command('continue', { stepaction: 'back' }, (nodeResponse) => { + this._sendNodeResponse(response, nodeResponse); + }); + } + + protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments) : void { + this._disableSkipFiles = false; + this._node.command('continue', { stepaction: 'reverse' }, (nodeResponse) => { + this._sendNodeResponse(response, nodeResponse); + }); + } + + protected restartFrameRequest(response: DebugProtocol.RestartFrameResponse, args: DebugProtocol.RestartFrameArguments) : void { + + const restartFrameArgs: V8RestartFrameArgs = { + frame: undefined + }; + + if (args.frameId > 0) { + const frame = this._frameHandles.get(args.frameId); + if (!frame) { + this.sendErrorResponse(response, 2020, 'stack frame not valid', null, ErrorDestination.Telemetry); + return; + } + restartFrameArgs.frame = frame.index; + } + + this._node.command('restartFrame', restartFrameArgs, restartNodeResponse => { + this._restartFramePending= true; + this._node.command('continue', { stepaction: 'in' }, stepInNodeResponse => { + this._sendNodeResponse(response, stepInNodeResponse); + }); + }); + } + + //--- evaluate request ---------------------------------------------------------------------------------------------------- + + protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { + + const expression = args.expression; + + const evalArgs = { + expression: expression, + disable_break: true, + maxStringLength: NodeDebugSession.MAX_STRING_LENGTH + }; + if (typeof args.frameId === 'number' && args.frameId > 0) { + const frame = this._frameHandles.get(args.frameId); + if (!frame) { + this.sendErrorResponse(response, 2020, 'stack frame not valid', null, ErrorDestination.Telemetry); + return; + } + const frameIx = frame.index; + (evalArgs).frame = frameIx; + } else { + (evalArgs).global = true; + } + + this._node.command(this._nodeInjectionAvailable ? 'vscode_evaluate' : 'evaluate', evalArgs, (resp: V8EvaluateResponse) => { + if (resp.success) { + this._createVariable(undefined, 'evaluate', resp.body).then(v => { + if (v) { + response.body = { + result: v.value, + variablesReference: v.variablesReference, + namedVariables: v.namedVariables, + indexedVariables: v.indexedVariables + }; + } else { + response.success = false; + response.message = localize('eval.not.available', "not available"); + } + this.sendResponse(response); + }); + } else { + response.success = false; + if (resp.message.indexOf('ReferenceError: ') === 0 || resp.message === 'No frames') { + response.message = localize('eval.not.available', "not available"); + } else if (resp.message.indexOf('SyntaxError: ') === 0) { + const m = resp.message.substring('SyntaxError: '.length).toLowerCase(); + response.message = localize('eval.invalid.expression', "invalid expression: {0}", m); + } else { + response.message = resp.message; + } + this.sendResponse(response); + } + }); + } + + //--- source request ------------------------------------------------------------------------------------------------------ + + protected sourceRequest(response: DebugProtocol.SourceResponse, args: DebugProtocol.SourceArguments): void { + + // first try to use 'source.sourceReference' + if (args.source && args.source.sourceReference) { + this.sourceRequest2(response, args.source.sourceReference); + return; + } + + // then try to use 'source.path' + if (args.source && args.source.path) { + + this._loadScript(this._pathToScript(args.source.path)).then(script => { + response.body = { + content: script.contents, + mimeType: 'text/javascript' + }; + this.sendResponse(response); + }).catch(err => { + this.sendErrorResponse(response, 2026, localize('source.not.found', "Could not retrieve content.")); + }); + + return; + } + + // try to use 'sourceReference' + return this.sourceRequest2(response, args.sourceReference); + } + + private sourceRequest2(response: DebugProtocol.SourceResponse, sourceReference: number): void { + + // try to use 'sourceReference' + const srcSource = this._sourceHandles.get(sourceReference); + if (srcSource) { + + if (srcSource.source) { // script content already cached + response.body = { + content: srcSource.source, + mimeType: 'text/javascript' + }; + this.sendResponse(response); + return; + } + + if (srcSource.scriptId) { // load script content + this._loadScript(srcSource.scriptId).then(script => { + srcSource.source = script.contents; // store in cache + response.body = { + content: srcSource.source, + mimeType: 'text/javascript' + }; + this.sendResponse(response); + }).catch(err => { + this.sendErrorResponse(response, 2026, localize('source.not.found', "Could not retrieve content.")); + }); + return; + } + } + + // give up + this.sendErrorResponse(response, 2027, 'sourceRequest error: illegal handle', null, ErrorDestination.Telemetry); + } + + private _loadScript(scriptIdOrPath: number | string) : Promise