!1 代码初始化和反向调试功能上传

Merge pull request !1 from chenhaoyang2019/master
This commit is contained in:
梁珂铭 2022-09-12 02:27:03 +00:00 committed by Gitee
commit 30d66f4c5b
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
38 changed files with 8368 additions and 27 deletions

12
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,12 @@
If submitting a bug please make sure
- [ ] If you are using gdb
- [ ] `gdb --version` >= 7.7.1
- [ ] it works on the command line with `gdb`
- [ ] `cwd` and `target` are properly set
- [ ] If you are using lldb
- [ ] `lldb --version` >= 3.7.1
- [ ] it works on the command line with `lldb-mi`
- [ ] `cwd` and `target` are properly set
Screenshots are helpful but not required

100
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,100 @@
name: Release
on:
release:
types: [published]
jobs:
release:
strategy:
matrix:
os: ['ubuntu-latest']
node-version: ['16.x']
runs-on: ${{ matrix.os }}
steps:
#
# Basic Setup
#
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
#
# Audit Versions for Consistency
#
- name: Query package.json Version
id: package
run: node -e "console.log('::set-output name=version::' + require('./package.json').version)"
- name: Query package-lock.json Version
id: package-lock
run: node -e "console.log('::set-output name=version::' + require('./package-lock.json').version)"
- name: Query Latest Changelog Version
id: changelog
shell: bash
run: echo ::set-output name=version::$(grep --perl-regexp "^# " --max-count=1 CHANGELOG.md | awk '{print $2}')
- name: Audit package.json/package-lock.json/CHANGELOG.md/git tag Version Consistency
shell: bash
run: >
test ${{ steps.package.outputs.version }} = ${{ steps.package-lock.outputs.version }} -a \
${{ steps.package.outputs.version }} = ${{ steps.changelog.outputs.version }} -a \
refs/tags/v${{ steps.package.outputs.version }} = ${{ github.ref }}
#
# Install Dependencies
#
# NOTE:
# Use the `clean-install` instead of just `install` so that the versions identified in the
# package-lock.json file are used instead of attempting to install later versions that might
# exist. This drives consistency between what the developers have been using and what is to
# be released.
#
# NOTE:
# Use the `--no-optional` switch to prevent installation of the `ssh2` optional dependency
# (i.e., `cpu-features`) package which is used to provide accelerated crypto functionality,
# but which is a native add-on and would require platform specific packages.
#
- name: Install Module Dependencies
run: npm clean-install --no-optional
#
# Package and Upload Extension
#
# NOTE:
# The "vscode:prepublish" script in package.json will be executed to compile the extension
# prior to packaging.
#
- name: Package Extension into .vsix file
id: asset
shell: bash
run: >
npx vsce package;
echo ::set-output name=vsix_path::$(ls *.vsix)
- name: Upload .vsix file to Github as release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ${{ steps.asset.outputs.vsix_path }}
asset_name: ${{ steps.asset.outputs.vsix_path }}
asset_content_type: application/zip
#
# Publish Extension
#
- name: Publish to VSCode Extension Marketplace
env:
VSCE_PAT: ${{ secrets.VS_MARKETPLACE_TOKEN }}
run: npx vsce publish --packagePath ${{ steps.asset.outputs.vsix_path }}
- name: Publish to Open VSX Registry
env:
OVSX_PAT: ${{ secrets.OPEN_VSX_TOKEN }}
run: npx ovsx publish ${{ steps.asset.outputs.vsix_path }}

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
out
node_modules
*.vsix
.vscode-test

20
.travis.yml Normal file
View File

@ -0,0 +1,20 @@
sudo: false
language: node_js
node_js:
- stable
os:
- osx
- linux
before_install:
- if [ $TRAVIS_OS_NAME == "linux" ]; then
export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0;
sh -e /etc/init.d/xvfb start;
sleep 3;
fi
script:
- |
npm install
tsc
node out/src/test/runTest.js

50
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,50 @@
// A launch configuration that compiles the extension and then opens it inside a new window
{
"version": "0.1.0",
"configurations": [
{
"name": "Launch Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/out/**/*.js"
],
"preLaunchTask": "npm"
},
{
"name": "code-debug server",
"type": "node",
"request": "launch",
"runtimeArgs": [ "--nolazy" ],
"program": "${workspaceRoot}/src/gdb.ts",
"stopOnEntry": false,
"args": [ "--server=4711" ],
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/out/**/*.js"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "npm"
},
{
"name": "Launch Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}",
"--extensionTestsPath=${workspaceRoot}/out/src/test/suite/index"
],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/out/**/*.js"
],
"preLaunchTask": "npm"
}
]
}

11
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,11 @@
// Place your settings in this file to overwrite default and user settings.
{
"editor.insertSpaces": false,
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
},
"typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version
}

28
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,28 @@
// Available variables which can be used inside of strings.
// ${workspaceRoot}: the root folder of the team
// ${file}: the current opened file
// ${fileBasename}: the current opened file's basename
// ${fileDirname}: the current opened file's dirname
// ${fileExtname}: the current opened file's extension
// ${cwd}: the current working directory of the spawned process
// A task runner that calls a custom npm script that compiles the extension.
{
"version": "2.0.0",
// Run in a shell so "npm" command is properly resolved to "npm.cmd" on Windows systems.
"type": "shell",
// we want to run npm
"command": "npm",
// we run the custom script "compile" as defined in package.json
"args": ["run", "compile", "--loglevel", "silent"],
// The tsc compiler is started in background mode
"isBackground": true,
// use the standard tsc in watch mode problem matcher to find compile problems in the output.
"problemMatcher": "$tsc-watch",
"tasks": []
}

9
.vscodeignore Normal file
View File

@ -0,0 +1,9 @@
.vscode/**
typings/**
out/test/**
test/**
src/**
**/*.map
.gitignore
tsconfig.json
vsc-extension-quickstart.md

82
CHANGELOG.md Normal file
View File

@ -0,0 +1,82 @@
# 0.26.0
* vscode dependency was increased from 1.28 to 1.55 along with the debug-adapter protocol to get rid of some outdated dependencies (@GitMensch)
* SSH2 module updated from deprecated 0.8.9 to current 1.6.0 (@GitMensch),
allowing connections with more modern key algorithms, improved error handling (including user messages passed on) and other improvements.
See [SSH2 Update Notices](https://github.com/mscdex/ssh2/issues/935) for more details.
* Path Substitutions working with attach+ssh configuration #293 (@brownts)
* Path Substitutions working with LLDB #295 (@brownts)
* Path Substitutions working with Windows-Style paths #294 (@brownts)
* Breakpoints may be deleted when not recognized correctly #259 fixing #230 (@kvinwang)
* New `stopAtConnect` configuration #299, #302 (@brownts)
* New `stopAtEntry` configuration to run debugger to application's entry point #306 (@brownts)
* New `ssh.sourceFileMap` configuration to allow multiple substitutions between local and ssh-remote and separate ssh working directory #298 (@GitMensch)
* fix path translation for SSH to Win32 and for extended-remote without executable (attach to process) #323 (@GitMensch)
* fix for race conditions on startup where breakpoints were not hit #304 (@brownts)
* prevent "Not implemented stop reason (assuming exception)" in many cases #316 (@GitMensch),
initial recognition of watchpoints
* fix additional race conditions with setting breakpoints #313 (@brownts)
* fix stack frame expansion in editor via use of the `startFrame` parameter #312 (@brownts)
* allow specification of port/x11port via variable (as numeric string) #265 (@GitMensch)
* Extra debugger arguments now work in all configurations #316, #338 fixing #206 (@GitMensch, @brownts)
* Attaching to local PID now performs initialization prior to attaching #341 fixing #329 (@brownts)
# 0.25.1
* Remove the need for extra trust for debugging workspaces per guidance "for debug extensions" as noted in the [Workspace Trust Extension Guide](https://github.com/microsoft/vscode/issues/120251#issuecomment-825832603) (@GitMensch)
* Fix simple value formatting list parsing with empty string as first argument (@nomtats)
* don't abort if `set target-async` or `cd` fails in attach (brings in line with existing behavior from launch)
# 0.25.0
(released May 2020)
* Add support for path substitutions (`{"fromPath": "toPath"}`) for GDB and LLDB (@karljs)
* Support up to 65535 threads instead of 256 threads (@ColdenCullen)
* Improve thread names on embedded GDB, makes not all threads always have the same name (with @anshulrouthu)
# 0.24.0
* Added zig as supported language.
* Fix example Debug Microcontroller template
* Implement "Jump to Cursor" to skip instructions
* Fix memory dump for theia
# 0.23.1
Fixes:
* Breakpoints in SSH in other working directories properly resolved
* Undefined/null paths don't crash stacktrace
* Added kotlin to language list
# 0.23.0
(released March 2019)
* Normalize file paths in stack trace (fixes duplicate opening of files)
* New Examine memory Location UI
* Breakpoints in SSH on windows fixed (@HaronK)
* Project code improvements (@simark)
* Initial configurations contain valueFormatting now (@Yanpas)
# 0.22.0
(released March 2018)
* Support for using SSH agent
* Support multi-threading (@LeszekSwirski)
* Fixed GDB expansion logic with floats (Marcel Ball)
* Fixed attach to PID template (@gentoo90)
# 0.21.0 / 0.21.1 / 0.21.2
(0.21.2 is pushed without changes to hopefully fix vscode installation)
* Several fixes to variable pretty printers by @gentoo90
* Enabled breakpoints for crystal (@faustinoaq)
# 0.20.0
Added support for pretty printers in variable list (by @gentoo90), enable
with `"valuesFormatting": "prettyPrinters"` if you have a pretty printer
to get potentially improved display of variables.

24
LICENSE Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>

217
README.md
View File

@ -1,39 +1,202 @@
# native-debug
# Debug
#### 介绍
{**以下是 Gitee 平台说明,您可以替换此简介**
Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN。专为开发者提供稳定、高效、安全的云端软件开发协作平台
无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)}
Native VSCode debugger. Supports both GDB and LLDB.
#### 软件架构
软件架构说明
## Installation
Press ctrl-p (cmd+p on OS X) and run `ext install webfreak.debug` in visual studio code and install GDB/LLDB. See `Usage` for details on how to set it up.
#### 安装教程
![Preview](images/preview.png)
1. xxxx
2. xxxx
3. xxxx
## Usage
#### 使用说明
![Image with red circle around a gear and a red arrow pointing at GDB and LLDB](images/tutorial1.png)
1. xxxx
2. xxxx
3. xxxx
Or if you already have an existing debugger in your project setup you can click "Create Configuration" or use the auto completion instead:
#### 参与贡献
![Visual studio code debugger launch.json auto completion showing alternative way to create debuggers](images/tutorial1-alt.png)
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码
4. 新建 Pull Request
Open your project and click the debug button in your sidebar. At the top right press
the little gear icon and select GDB or LLDB. It will automatically generate the configuration
you need.
*Note: for LLDB you need to have `lldb-mi` in your PATH*
#### 特技
If you are on OS X you can add `lldb-mi` to your path using
`ln -s /Applications/Xcode.app/Contents/Developer/usr/bin/lldb-mi /usr/local/bin/lldb-mi` if you have Xcode.
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
![Default config with a red circle around the target](images/tutorial2.png)
Now you need to change `target` to the application you want to debug relative
to the cwd. (Which is the workspace root by default)
Additionally you can set `terminal` if you want to run the program in a separate terminal with
support for input. On Windows set it to an empty string (`""`) to enable this feature. On linux
set it to an empty string (`""`) to use the default terminal emulator specified with `x-terminal-emulator`
or specify a custom one. Note that it must support the `-e` argument.
Before debugging you need to compile your application first, then you can run it using
the green start button in the debug sidebar. For this you could use the `preLaunchTask`
argument vscode allows you to do. Debugging multithreaded applications is currently not
implemented. Adding breakpoints while the program runs will not interrupt it immediately.
For that you need to pause & resume the program once first. However adding breakpoints
while its paused works as expected.
Extending variables is very limited as it does not support child values of variables.
Watching expressions works partially but the result does not get properly parsed and
it shows the raw output of the command. It will run `data-evaluate-expression`
to check for variables.
While running you will get a console where you can manually type GDB/LLDB commands or MI
commands prepended with a hyphen `-`. The console shows all output separated
in `stdout` for the application, `stderr` for errors and `log` for log messages.
Some exceptions/signals like segmentation faults will be catched and displayed but
it does not support for example most D exceptions.
Support exists for stopping at the entry point of the application. This is controlled
through the `stopAtEntry` setting. This value may be either a boolean or a string. In
the case of a boolean value of `false` (the default), this setting is disabled. In the
case of a boolean value of `true`, if this is a launch configuration and the debugger
supports the `start` (or `exec-run --start` MI feature, more specifically), than this
will be used to run to the entry point of the application. Note that this appears to
work fine for GDB, but LLDB doesn't necessarily seem to adhere to this, even though it may
indicate that it supports this feature. The alternative configuration option for the
`stopAtEntry` setting is to specify a string where the string represents the entry point
itself. In this situation a temporary breakpoint will be set at the specified entry point
and a normal run will occur for a launch configuration. This (setting a temporary
breakpoint) is also the behavior that occurs when the debugger does not support the
`start` feature and the `stopAtEntry` was set to `true`. In that case the entry point will
default to "main". Thus, the most portable way to use this configuration is to explicitly
specify the entry point of the application. In the case of an attach configuration, similar
behavior will occur, however since there is no equivalent of the `start` command for
attaching, a boolean value of `true` for the `stopAtEntry` setting in a launch configuration
will automatically default to an entry point of "main", while a string value for this
setting will be interpreted as the entry point, causing a temporary breakpoint to be set at
that location prior to continuing execution. Note that stopping at the entry point for the
attach configuration assumes that the entry point has not yet been entered at the time of
attach, otherwise this will have no affect.
### Attaching to existing processes
Attaching to existing processes currently only works by specifying the PID in the
`launch.json` and setting `request` to `"attach"`. You also need to specify the executable
path for the debugger to find the debug symbols.
```
"request": "attach",
"executable": "./bin/executable",
"target": "4285"
```
This will attach to PID 4285 which should already run. GDB will pause the program on entering and LLDB will keep it running.
### Using `gdbserver` for remote debugging (GDB only)
You can also connect to a gdbserver instance and debug using that. For that modify the
`launch.json` by setting `request` to `"attach"` and `remote` to `true` and specifing the
port and optionally hostname in `target`.
```
"request": "attach",
"executable": "./bin/executable",
"target": ":2345",
"cwd": "${workspaceRoot}",
"remote": true
```
This will attach to the running process managed by gdbserver on localhost:2345. You might
need to hit the start button in the debug bar at the top first to start the program.
Control over whether the debugger should continue executing on connect can be configured
by setting `stopAtConnect`. The default value is `false` so that execution will continue
after connecting.
### Using ssh for debugging on remote
Debugging using ssh automatically converts all paths between client & server and also optionally
redirects X11 output from the server to the client.
Simply add a `ssh` object in your `launch` request.
```
"request": "launch",
"target": "./executable",
"cwd": "${workspaceRoot}",
"ssh": {
"forwardX11": true,
"host": "192.168.178.57",
"cwd": "/home/remoteUser/project/",
"keyfile": "/path/to/.ssh/key", // OR
"password": "password123",
"user": "remoteUser",
"x11host": "localhost",
// x11port may also be specified as string containing only numbers (useful to use configuration variables)
"x11port": 6000,
// Optional, content will be executed on the SSH host before the debugger call.
"bootstrap": "source /home/remoteUser/some-env"
}
```
`ssh.sourceFileMap` will be used to trim off local paths and map them to the server. This is
required for basically everything except watched variables or user commands to work.
For backward compatibility you can also use `cwd` and `ssh.cwd` for the mapping, this is only used
if the newer `ssh.sourceFileMap` is not configured.
For X11 forwarding to work you first need to enable it in your Display Manager and allow the
connections. To allow connections you can either add an entry for applications or run `xhost +`
in the console while you are debugging and turn it off again when you are done using `xhost -`.
Because some builds requires one or more environment files to be sourced before running any
command, you can use the `ssh.bootstrap` option to add some extra commands which will be prepended
to the debugger call (using `&&` to join both).
### Extra Debugger Arguments
Additional arguments can be supplied to the debugger if needed. These will be added when
the debugger executable (e.g., gdb, lldb-mi, etc.) is launched. Extra debugger arguments
are supplied via the `debugger_args` setting. Note that the behavior of escaping these
options depends on the environment in which the debugger is started. For non-SSH
debugging, the options are passed directly to the application and therefore no escaping is
necessary (other than what is necessary for the JSON configuration). However, as a result
of the options being passed directly to the application, care must be taken to place
switches and switch values as separate entities in `debugger_args`, if they would normally
be separated by a space. For example, supplying the option and value
`-iex "set $foo = \"bar\""` would consist of the following `debugger_args`:
```json
"debugger_args" : ["-iex", "set $foo = \"bar\""]
```
If `=` is used to associate switches with their values, than the switch and value should
be placed together instead. In fact, the following example shows 4 different ways in
which to specify the same switch and value, using both short and long format, as well as
switch values supplied as a separate parameter or supplied via the `=`:
- ```json
"debugger_args" : ["-iex", "set $foo = \"bar\""]
```
- ```json
"debugger_args" : ["-iex=set $foo = \"bar\""]
```
- ```json
"debugger_args" : ["--init-eval-command", "set $foo = \"bar\""]
```
- ```json
"debugger_args" : ["--init-eval-command=set $foo = \"bar\""]
```
Where escaping is really necessary is when running the debugger over SSH. In this case,
the options are not passed directly to the application, but are instead combined with the
application name, joined together with any other options, and sent to the remote system to
be parsed and executed. Thus, depending on the remote system, different escaping may be
necessary. The following shows how the same command as above needs to be escaped
differently based on whether the remote system is a POSIX or a Windows system.
- SSH to Linux machine:
```json
"debugger_args": ["-iex", "'set $foo = \"bar\"'"]
```
- SSH to Windows machine:
```json
"debugger_args": ["-iex", "\"set $foo = \\\"bar\\\"\""]
```
You may need to experiment to find the correct escaping necessary for the command to be
sent to the debugger as you intended.
## [Issues](https://github.com/WebFreak001/code-debug)

136
images/icon-plain.svg Normal file
View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="svg2"
viewBox="0 0 128 128"
height="128"
width="128">
<defs
id="defs4">
<filter
id="filter3470"
style="color-interpolation-filters:sRGB">
<feFlood
id="feFlood3472"
result="flood"
flood-color="rgb(0,0,0)"
flood-opacity="0.501961" />
<feComposite
id="feComposite3474"
result="composite1"
operator="in"
in2="SourceGraphic"
in="flood" />
<feGaussianBlur
id="feGaussianBlur3476"
result="blur"
stdDeviation="5"
in="composite1" />
<feOffset
id="feOffset3478"
result="offset"
dy="3"
dx="0" />
<feComposite
id="feComposite3480"
result="composite2"
operator="over"
in2="offset"
in="SourceGraphic" />
</filter>
</defs>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(0,-924.36216)"
id="layer1">
<circle
r="64"
cy="988.36212"
cx="64"
id="path3338"
style="fill:#00897b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.53892212" />
<g
transform="matrix(1.2,0,0,1.2,-12.542854,-203.58019)"
id="g4300">
<g
id="text3340"
style="font-style:normal;font-weight:normal;font-size:8.38301659px;line-height:100%;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1">
<path
id="path4148"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:20.04474068px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
d="m 41.965515,1014.8288 -2.47623,0 -6.352069,-10.1105 0,10.1105 -2.47623,0 0,-14.2506 2.47623,0 6.371644,10.1496 0,-10.1496 2.456655,0 0,14.2506 z" />
<path
id="path4150"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:20.04474068px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
d="m 53.084082,1011.5108 -5.520133,0 -1.154922,3.318 -2.574105,0 5.383109,-14.2506 2.221756,0 5.392897,14.2506 -2.583893,0 -1.164709,-3.318 z m -4.825223,-1.9966 4.130313,0 -2.065157,-5.9116 -2.065156,5.9116 z" />
<path
id="path4152"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:20.04474068px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
d="m 67.579327,1002.5749 -4.443512,0 0,12.2539 -2.456655,0 0,-12.2539 -4.404362,0 0,-1.9967 11.304529,0 0,1.9967 z" />
<path
id="path4154"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:20.04474068px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
d="m 72.071776,1014.8288 -2.466443,0 0,-14.2506 2.466443,0 0,14.2506 z" />
<path
id="path4156"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:20.04474068px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
d="m 80.156229,1011.7262 3.621364,-11.148 2.720917,0 -5.128635,14.2506 -2.39793,0 -5.10906,-14.2506 2.711129,0 3.582215,11.148 z" />
<path
id="path4158"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:20.04474068px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
d="m 96.423004,1008.4669 -5.852907,0 0,4.3848 6.841442,0 0,1.9771 -9.317672,0 0,-14.2506 9.24916,0 0,1.9967 -6.77293,0 0,3.9345 5.852907,0 0,1.9575 z" />
<path
id="path4160"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16.76603317px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
d="m 38.112729,1032.2779 0,-11.9196 3.520212,0 q 1.580002,0 2.799797,0.704 1.227981,0.7041 1.899277,1.9975 0.671296,1.2935 0.671296,2.9636 l 0,0.5976 q 0,1.6946 -0.679482,2.9799 -0.671297,1.2853 -1.923837,1.9811 -1.244354,0.6959 -2.857103,0.6959 l -3.43016,0 z m 2.071195,-10.2496 0,8.5959 1.350779,0 q 1.629121,0 2.496894,-1.0151 0.87596,-1.0233 0.892333,-2.9308 l 0,-0.6631 q 0,-1.9402 -0.843213,-2.9635 -0.843214,-1.0234 -2.447776,-1.0234 l -1.449017,0 z" />
<path
id="path4162"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16.76603317px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
d="m 56.049438,1026.9566 -4.895551,0 0,3.6676 5.722391,0 0,1.6537 -7.793586,0 0,-11.9196 7.73628,0 0,1.67 -5.665085,0 0,3.291 4.895551,0 0,1.6373 z" />
<path
id="path4164"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16.76603317px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
d="m 58.579078,1032.2779 0,-11.9196 4.085083,0 q 2.022076,0 3.078139,0.8105 1.056064,0.8104 1.056064,2.415 0,0.8186 -0.442073,1.4736 -0.442074,0.6549 -1.293474,1.0151 0.966012,0.262 1.489951,0.9906 0.532125,0.7204 0.532125,1.7355 0,1.6783 -1.080624,2.5788 -1.072436,0.9005 -3.078139,0.9005 l -4.347052,0 z m 2.071194,-5.3786 0,3.7249 2.300418,0 q 0.974198,0 1.522696,-0.483 0.548499,-0.483 0.548499,-1.3426 0,-1.8583 -1.899278,-1.8993 l -2.472335,0 z m 0,-1.5227 2.030262,0 q 0.966012,0 1.506324,-0.4338 0.548498,-0.4421 0.548498,-1.2444 0,-0.8841 -0.507566,-1.2771 -0.499379,-0.393 -1.563629,-0.393 l -2.013889,0 0,3.3483 z" />
<path
id="path4166"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16.76603317px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
d="m 77.874752,1020.3583 0,7.9655 q 0,1.8993 -1.219795,3.0126 -1.211607,1.1052 -3.233683,1.1052 -2.046635,0 -3.250056,-1.0888 -1.203421,-1.097 -1.203421,-3.0372 l 0,-7.9573 2.063008,0 0,7.9737 q 0,1.1952 0.605804,1.8256 0.605804,0.6303 1.784665,0.6303 2.39047,0 2.39047,-2.5214 l 0,-7.9082 2.063008,0 z" />
<path
id="path4168"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16.76603317px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
d="m 89.229483,1030.7306 q -0.646737,0.8433 -1.792852,1.2771 -1.146116,0.4339 -2.60332,0.4339 -1.498136,0 -2.652439,-0.6795 -1.154302,-0.6794 -1.784665,-1.9402 -0.622177,-1.2689 -0.646737,-2.9553 l 0,-0.9333 q 0,-2.7015 1.293473,-4.216 1.293474,-1.5227 3.610264,-1.5227 1.98933,0 3.160005,0.9823 1.170675,0.9824 1.408085,2.8326 l -2.030262,0 q -0.343835,-2.1531 -2.496895,-2.1531 -1.391712,0 -2.120314,1.007 -0.720415,0.9987 -0.744975,2.9389 l 0,0.9169 q 0,1.9321 0.810468,3.0045 0.818654,1.0642 2.267671,1.0642 1.588189,0 2.259485,-0.7204 l 0,-2.3331 -2.455962,0 0,-1.5719 4.51897,0 0,4.5681 z" />
</g>
<g
id="g4170">
<path
style="fill:#ffffff"
d="m 54.812555,949.43739 -3.622346,3.62235 4.161634,4.18806 c -1.901223,1.3103 -3.493878,3.03165 -4.650029,5.03565 l -7.219236,0 0,5.13842 5.369407,0 C 48.72352,968.26972 48.621,969.11755 48.621,969.99108 l 0,2.56922 -5.138422,0 0,5.13842 5.138422,0 0,2.56921 c 0,0.20262 0.0065,0.40371 0.01664,0.60389 l 10.260204,-10.26021 0,-3.18974 3.18974,0 14.578435,-14.57844 -3.406037,-3.40604 -5.574944,5.57495 c -1.181843,-0.28261 -2.364157,-0.43652 -3.648769,-0.43652 -1.284613,0 -2.466195,0.15391 -3.622346,0.43652 l -5.601367,-5.57495 z m 29.7774,13.02126 -4.963223,4.96322 4.963223,0 0,-4.96322 z m -5.323402,5.3234 -10.091861,10.09186 0,4.96323 -4.963228,0 -9.607385,9.60738 c 2.604379,2.02784 5.872939,3.23868 9.432189,3.23868 5.703686,0 10.662454,-3.10872 13.334453,-7.70764 l 7.219234,0 0,-5.13842 -5.369405,0 c 0.128467,-0.84784 0.230986,-1.69567 0.230986,-2.56921 l 0,-2.56921 5.138419,0 0,-5.13842 -5.138419,0 0,-2.56922 c 0,-0.75018 -0.08221,-1.48046 -0.184983,-2.20903 z m -35.783975,15.05509 0,3.18974 3.189737,-3.18974 -3.189737,0 z"
id="path4310" />
<rect
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.53892212"
id="rect4319"
width="62.010242"
height="5.3151774"
x="-674.97266"
y="731.51654"
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

181
images/icon.svg Normal file
View File

@ -0,0 +1,181 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
viewBox="0 0 128 128"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="icon.svg">
<defs
id="defs4">
<filter
style="color-interpolation-filters:sRGB"
inkscape:label="Drop Shadow"
id="filter3470">
<feFlood
flood-opacity="0.501961"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood3472" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite3474" />
<feGaussianBlur
in="composite1"
stdDeviation="5"
result="blur"
id="feGaussianBlur3476" />
<feOffset
dx="0"
dy="3"
result="offset"
id="feOffset3478" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite3480" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.959798"
inkscape:cx="45.957504"
inkscape:cy="50.316562"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1034"
inkscape:window-x="0"
inkscape:window-y="46"
inkscape:window-maximized="0">
<inkscape:grid
type="xygrid"
id="grid3336"
enabled="false" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-924.36216)">
<circle
style="fill:#00897b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.53892212"
id="path3338"
cx="64"
cy="988.36212"
r="64" />
<g
id="g4300"
transform="matrix(1.2,0,0,1.2,-12.542854,-203.58019)">
<g
style="font-style:normal;font-weight:normal;font-size:8.38301659px;line-height:100%;font-family:sans-serif;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="text3340">
<path
d="m 41.965515,1014.8288 -2.47623,0 -6.352069,-10.1105 0,10.1105 -2.47623,0 0,-14.2506 2.47623,0 6.371644,10.1496 0,-10.1496 2.456655,0 0,14.2506 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:20.04474068px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
id="path4148"
inkscape:connector-curvature="0" />
<path
d="m 53.084082,1011.5108 -5.520133,0 -1.154922,3.318 -2.574105,0 5.383109,-14.2506 2.221756,0 5.392897,14.2506 -2.583893,0 -1.164709,-3.318 z m -4.825223,-1.9966 4.130313,0 -2.065157,-5.9116 -2.065156,5.9116 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:20.04474068px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
id="path4150"
inkscape:connector-curvature="0" />
<path
d="m 67.579327,1002.5749 -4.443512,0 0,12.2539 -2.456655,0 0,-12.2539 -4.404362,0 0,-1.9967 11.304529,0 0,1.9967 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:20.04474068px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
id="path4152"
inkscape:connector-curvature="0" />
<path
d="m 72.071776,1014.8288 -2.466443,0 0,-14.2506 2.466443,0 0,14.2506 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:20.04474068px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
id="path4154"
inkscape:connector-curvature="0" />
<path
d="m 80.156229,1011.7262 3.621364,-11.148 2.720917,0 -5.128635,14.2506 -2.39793,0 -5.10906,-14.2506 2.711129,0 3.582215,11.148 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:20.04474068px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
id="path4156"
inkscape:connector-curvature="0" />
<path
d="m 96.423004,1008.4669 -5.852907,0 0,4.3848 6.841442,0 0,1.9771 -9.317672,0 0,-14.2506 9.24916,0 0,1.9967 -6.77293,0 0,3.9345 5.852907,0 0,1.9575 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:20.04474068px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
id="path4158"
inkscape:connector-curvature="0" />
<path
d="m 38.112729,1032.2779 0,-11.9196 3.520212,0 q 1.580002,0 2.799797,0.704 1.227981,0.7041 1.899277,1.9975 0.671296,1.2935 0.671296,2.9636 l 0,0.5976 q 0,1.6946 -0.679482,2.9799 -0.671297,1.2853 -1.923837,1.9811 -1.244354,0.6959 -2.857103,0.6959 l -3.43016,0 z m 2.071195,-10.2496 0,8.5959 1.350779,0 q 1.629121,0 2.496894,-1.0151 0.87596,-1.0233 0.892333,-2.9308 l 0,-0.6631 q 0,-1.9402 -0.843213,-2.9635 -0.843214,-1.0234 -2.447776,-1.0234 l -1.449017,0 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16.76603317px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
id="path4160"
inkscape:connector-curvature="0" />
<path
d="m 56.049438,1026.9566 -4.895551,0 0,3.6676 5.722391,0 0,1.6537 -7.793586,0 0,-11.9196 7.73628,0 0,1.67 -5.665085,0 0,3.291 4.895551,0 0,1.6373 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16.76603317px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
id="path4162"
inkscape:connector-curvature="0" />
<path
d="m 58.579078,1032.2779 0,-11.9196 4.085083,0 q 2.022076,0 3.078139,0.8105 1.056064,0.8104 1.056064,2.415 0,0.8186 -0.442073,1.4736 -0.442074,0.6549 -1.293474,1.0151 0.966012,0.262 1.489951,0.9906 0.532125,0.7204 0.532125,1.7355 0,1.6783 -1.080624,2.5788 -1.072436,0.9005 -3.078139,0.9005 l -4.347052,0 z m 2.071194,-5.3786 0,3.7249 2.300418,0 q 0.974198,0 1.522696,-0.483 0.548499,-0.483 0.548499,-1.3426 0,-1.8583 -1.899278,-1.8993 l -2.472335,0 z m 0,-1.5227 2.030262,0 q 0.966012,0 1.506324,-0.4338 0.548498,-0.4421 0.548498,-1.2444 0,-0.8841 -0.507566,-1.2771 -0.499379,-0.393 -1.563629,-0.393 l -2.013889,0 0,3.3483 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16.76603317px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
id="path4164"
inkscape:connector-curvature="0" />
<path
d="m 77.874752,1020.3583 0,7.9655 q 0,1.8993 -1.219795,3.0126 -1.211607,1.1052 -3.233683,1.1052 -2.046635,0 -3.250056,-1.0888 -1.203421,-1.097 -1.203421,-3.0372 l 0,-7.9573 2.063008,0 0,7.9737 q 0,1.1952 0.605804,1.8256 0.605804,0.6303 1.784665,0.6303 2.39047,0 2.39047,-2.5214 l 0,-7.9082 2.063008,0 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16.76603317px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
id="path4166"
inkscape:connector-curvature="0" />
<path
d="m 89.229483,1030.7306 q -0.646737,0.8433 -1.792852,1.2771 -1.146116,0.4339 -2.60332,0.4339 -1.498136,0 -2.652439,-0.6795 -1.154302,-0.6794 -1.784665,-1.9402 -0.622177,-1.2689 -0.646737,-2.9553 l 0,-0.9333 q 0,-2.7015 1.293473,-4.216 1.293474,-1.5227 3.610264,-1.5227 1.98933,0 3.160005,0.9823 1.170675,0.9824 1.408085,2.8326 l -2.030262,0 q -0.343835,-2.1531 -2.496895,-2.1531 -1.391712,0 -2.120314,1.007 -0.720415,0.9987 -0.744975,2.9389 l 0,0.9169 q 0,1.9321 0.810468,3.0045 0.818654,1.0642 2.267671,1.0642 1.588189,0 2.259485,-0.7204 l 0,-2.3331 -2.455962,0 0,-1.5719 4.51897,0 0,4.5681 z"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:16.76603317px;font-family:Roboto;-inkscape-font-specification:'Roboto Medium';fill:#ffffff"
id="path4168"
inkscape:connector-curvature="0" />
</g>
<g
id="g4170">
<path
inkscape:connector-curvature="0"
id="path4310"
d="m 54.812555,949.43739 -3.622346,3.62235 4.161634,4.18806 c -1.901223,1.3103 -3.493878,3.03165 -4.650029,5.03565 l -7.219236,0 0,5.13842 5.369407,0 C 48.72352,968.26972 48.621,969.11755 48.621,969.99108 l 0,2.56922 -5.138422,0 0,5.13842 5.138422,0 0,2.56921 c 0,0.20262 0.0065,0.40371 0.01664,0.60389 l 10.260204,-10.26021 0,-3.18974 3.18974,0 14.578435,-14.57844 -3.406037,-3.40604 -5.574944,5.57495 c -1.181843,-0.28261 -2.364157,-0.43652 -3.648769,-0.43652 -1.284613,0 -2.466195,0.15391 -3.622346,0.43652 l -5.601367,-5.57495 z m 29.7774,13.02126 -4.963223,4.96322 4.963223,0 0,-4.96322 z m -5.323402,5.3234 -10.091861,10.09186 0,4.96323 -4.963228,0 -9.607385,9.60738 c 2.604379,2.02784 5.872939,3.23868 9.432189,3.23868 5.703686,0 10.662454,-3.10872 13.334453,-7.70764 l 7.219234,0 0,-5.13842 -5.369405,0 c 0.128467,-0.84784 0.230986,-1.69567 0.230986,-2.56921 l 0,-2.56921 5.138419,0 0,-5.13842 -5.138419,0 0,-2.56922 c 0,-0.75018 -0.08221,-1.48046 -0.184983,-2.20903 z m -35.783975,15.05509 0,3.18974 3.189737,-3.18974 -3.189737,0 z"
style="fill:#ffffff" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
y="731.51654"
x="-674.97266"
height="5.3151774"
width="62.010242"
id="rect4319"
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.53892212" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

BIN
images/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
images/tutorial1-alt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
images/tutorial1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
images/tutorial2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

2632
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

1068
package.json Normal file

File diff suppressed because it is too large Load Diff

165
src/backend/backend.ts Normal file
View File

@ -0,0 +1,165 @@
import { MINode } from "./mi_parse";
import { DebugProtocol } from "vscode-debugprotocol/lib/debugProtocol";
export type ValuesFormattingMode = "disabled" | "parseText" | "prettyPrinters";
export interface Breakpoint {
file?: string;
line?: number;
raw?: string;
condition: string;
countCondition?: string;
}
export interface Thread {
id: number;
targetId: string;
name?: string;
}
export interface Stack {
level: number;
address: string;
function: string;
fileName: string;
file: string;
line: number;
}
export interface Variable {
name: string;
valueStr: string;
type: string;
raw?: any;
}
export interface SSHArguments {
forwardX11: boolean;
host: string;
keyfile: string;
password: string;
useAgent: boolean;
cwd: string;
port: number;
user: string;
remotex11screen: number;
x11port: number;
x11host: string;
bootstrap: string;
sourceFileMap: { [index: string]: string };
}
export interface IBackend {
load(cwd: string, target: string, procArgs: string, separateConsole: string): Thenable<any>;
ssh(args: SSHArguments, cwd: string, target: string, procArgs: string, separateConsole: string, attach: boolean): Thenable<any>;
attach(cwd: string, executable: string, target: string): Thenable<any>;
connect(cwd: string, executable: string, target: string): Thenable<any>;
start(runToStart: boolean): Thenable<boolean>;
stop();
detach();
interrupt(): Thenable<boolean>;
continue(): Thenable<boolean>;
next(): Thenable<boolean>;
step(): Thenable<boolean>;
stepOut(): Thenable<boolean>;
loadBreakPoints(breakpoints: Breakpoint[]): Thenable<[boolean, Breakpoint][]>;
addBreakPoint(breakpoint: Breakpoint): Thenable<[boolean, Breakpoint]>;
removeBreakPoint(breakpoint: Breakpoint): Thenable<boolean>;
clearBreakPoints(source?: string): Thenable<any>;
getThreads(): Thenable<Thread[]>;
getStack(startFrame: number, maxLevels: number, thread: number): Thenable<Stack[]>;
getStackVariables(thread: number, frame: number): Thenable<Variable[]>;
evalExpression(name: string, thread: number, frame: number): Thenable<any>;
isReady(): boolean;
changeVariable(name: string, rawValue: string): Thenable<any>;
examineMemory(from: number, to: number): Thenable<any>;
}
export class VariableObject {
name: string;
exp: string;
numchild: number;
type: string;
value: string;
threadId: string;
frozen: boolean;
dynamic: boolean;
displayhint: string;
hasMore: boolean;
id: number;
constructor(node: any) {
this.name = MINode.valueOf(node, "name");
this.exp = MINode.valueOf(node, "exp");
this.numchild = parseInt(MINode.valueOf(node, "numchild"));
this.type = MINode.valueOf(node, "type");
this.value = MINode.valueOf(node, "value");
this.threadId = MINode.valueOf(node, "thread-id");
this.frozen = !!MINode.valueOf(node, "frozen");
this.dynamic = !!MINode.valueOf(node, "dynamic");
this.displayhint = MINode.valueOf(node, "displayhint");
// TODO: use has_more when it's > 0
this.hasMore = !!MINode.valueOf(node, "has_more");
}
public applyChanges(node: MINode) {
this.value = MINode.valueOf(node, "value");
if (!!MINode.valueOf(node, "type_changed")) {
this.type = MINode.valueOf(node, "new_type");
}
this.dynamic = !!MINode.valueOf(node, "dynamic");
this.displayhint = MINode.valueOf(node, "displayhint");
this.hasMore = !!MINode.valueOf(node, "has_more");
}
public isCompound(): boolean {
return this.numchild > 0 ||
this.value === "{...}" ||
(this.dynamic && (this.displayhint === "array" || this.displayhint === "map"));
}
public toProtocolVariable(): DebugProtocol.Variable {
const res: DebugProtocol.Variable = {
name: this.exp,
evaluateName: this.name,
value: (this.value === void 0) ? "<unknown>" : this.value,
type: this.type,
variablesReference: this.id
};
return res;
}
}
// from https://gist.github.com/justmoon/15511f92e5216fa2624b#gistcomment-1928632
export interface MIError extends Error {
readonly name: string;
readonly message: string;
readonly source: string;
}
export interface MIErrorConstructor {
new (message: string, source: string): MIError;
readonly prototype: MIError;
}
export const MIError: MIErrorConstructor = <any> class MIError {
readonly name: string;
readonly message: string;
readonly source: string;
public constructor(message: string, source: string) {
Object.defineProperty(this, 'name', {
get: () => (this.constructor as any).name,
});
Object.defineProperty(this, 'message', {
get: () => message,
});
Object.defineProperty(this, 'source', {
get: () => source,
});
Error.captureStackTrace(this, this.constructor);
}
public toString() {
return `${this.message} (from ${this.source})`;
}
};
Object.setPrototypeOf(MIError as any, Object.create(Error.prototype));
MIError.prototype.constructor = MIError;

View File

@ -0,0 +1,258 @@
import { MINode } from "./mi_parse";
const resultRegex = /^([a-zA-Z_\-][a-zA-Z0-9_\-]*|\[\d+\])\s*=\s*/;
const variableRegex = /^[a-zA-Z_\-][a-zA-Z0-9_\-]*/;
const errorRegex = /^\<.+?\>/;
const referenceStringRegex = /^(0x[0-9a-fA-F]+\s*)"/;
const referenceRegex = /^0x[0-9a-fA-F]+/;
const cppReferenceRegex = /^@0x[0-9a-fA-F]+/;
const nullpointerRegex = /^0x0+\b/;
const charRegex = /^(\d+) ['"]/;
const numberRegex = /^\d+(\.\d+)?/;
const pointerCombineChar = ".";
export function isExpandable(value: string): number {
let match;
value = value.trim();
if (value.length == 0) return 0;
else if (value.startsWith("{...}")) return 2; // lldb string/array
else if (value[0] == '{') return 1; // object
else if (value.startsWith("true")) return 0;
else if (value.startsWith("false")) return 0;
else if (match = nullpointerRegex.exec(value)) return 0;
else if (match = referenceStringRegex.exec(value)) return 0;
else if (match = referenceRegex.exec(value)) return 2; // reference
else if (match = charRegex.exec(value)) return 0;
else if (match = numberRegex.exec(value)) return 0;
else if (match = variableRegex.exec(value)) return 0;
else if (match = errorRegex.exec(value)) return 0;
else return 0;
}
export function expandValue(variableCreate: Function, value: string, root: string = "", extra: any = undefined): any {
const parseCString = () => {
value = value.trim();
if (value[0] != '"' && value[0] != '\'')
return "";
let stringEnd = 1;
let inString = true;
const charStr = value[0];
let remaining = value.substr(1);
let escaped = false;
while (inString) {
if (escaped)
escaped = false;
else if (remaining[0] == '\\')
escaped = true;
else if (remaining[0] == charStr)
inString = false;
remaining = remaining.substr(1);
stringEnd++;
}
const str = value.substr(0, stringEnd).trim();
value = value.substr(stringEnd).trim();
return str;
};
const stack = [root];
let parseValue, parseCommaResult, parseCommaValue, parseResult, createValue;
let variable = "";
const getNamespace = (variable) => {
let namespace = "";
let prefix = "";
stack.push(variable);
stack.forEach(name => {
prefix = "";
if (name != "") {
if (name.startsWith("["))
namespace = namespace + name;
else {
if (namespace) {
while (name.startsWith("*")) {
prefix += "*";
name = name.substr(1);
}
namespace = namespace + pointerCombineChar + name;
} else
namespace = name;
}
}
});
stack.pop();
return prefix + namespace;
};
const parseTupleOrList = () => {
value = value.trim();
if (value[0] != '{')
return undefined;
const oldContent = value;
value = value.substr(1).trim();
if (value[0] == '}') {
value = value.substr(1).trim();
return [];
}
if (value.startsWith("...")) {
value = value.substr(3).trim();
if (value[0] == '}') {
value = value.substr(1).trim();
return <any> "<...>";
}
}
const eqPos = value.indexOf("=");
const newValPos1 = value.indexOf("{");
const newValPos2 = value.indexOf(",");
let newValPos = newValPos1;
if (newValPos2 != -1 && newValPos2 < newValPos1)
newValPos = newValPos2;
if (newValPos != -1 && eqPos > newValPos || eqPos == -1) { // is value list
const values = [];
stack.push("[0]");
let val = parseValue();
stack.pop();
values.push(createValue("[0]", val));
const remaining = value;
let i = 0;
while (true) {
stack.push("[" + (++i) + "]");
if (!(val = parseCommaValue())) {
stack.pop();
break;
}
stack.pop();
values.push(createValue("[" + i + "]", val));
}
value = value.substr(1).trim(); // }
return values;
}
let result = parseResult(true);
if (result) {
const results = [];
results.push(result);
while (result = parseCommaResult(true))
results.push(result);
value = value.substr(1).trim(); // }
return results;
}
return undefined;
};
const parsePrimitive = () => {
let primitive: any;
let match;
value = value.trim();
if (value.length == 0)
primitive = undefined;
else if (value.startsWith("true")) {
primitive = "true";
value = value.substr(4).trim();
} else if (value.startsWith("false")) {
primitive = "false";
value = value.substr(5).trim();
} else if (match = nullpointerRegex.exec(value)) {
primitive = "<nullptr>";
value = value.substr(match[0].length).trim();
} else if (match = referenceStringRegex.exec(value)) {
value = value.substr(match[1].length).trim();
primitive = parseCString();
} else if (match = referenceRegex.exec(value)) {
primitive = "*" + match[0];
value = value.substr(match[0].length).trim();
} else if (match = cppReferenceRegex.exec(value)) {
primitive = match[0];
value = value.substr(match[0].length).trim();
} else if (match = charRegex.exec(value)) {
primitive = match[1];
value = value.substr(match[0].length - 1);
primitive += " " + parseCString();
} else if (match = numberRegex.exec(value)) {
primitive = match[0];
value = value.substr(match[0].length).trim();
} else if (match = variableRegex.exec(value)) {
primitive = match[0];
value = value.substr(match[0].length).trim();
} else if (match = errorRegex.exec(value)) {
primitive = match[0];
value = value.substr(match[0].length).trim();
} else {
primitive = value;
}
return primitive;
};
parseValue = () => {
value = value.trim();
if (value[0] == '"')
return parseCString();
else if (value[0] == '{')
return parseTupleOrList();
else
return parsePrimitive();
};
parseResult = (pushToStack: boolean = false) => {
value = value.trim();
const variableMatch = resultRegex.exec(value);
if (!variableMatch)
return undefined;
value = value.substr(variableMatch[0].length).trim();
const name = variable = variableMatch[1];
if (pushToStack)
stack.push(variable);
const val = parseValue();
if (pushToStack)
stack.pop();
return createValue(name, val);
};
createValue = (name, val) => {
let ref = 0;
if (typeof val == "object") {
ref = variableCreate(val);
val = "Object";
} else if (typeof val == "string" && val.startsWith("*0x")) {
if (extra && MINode.valueOf(extra, "arg") == "1") {
ref = variableCreate(getNamespace("*(" + name), { arg: true });
val = "<args>";
} else {
ref = variableCreate(getNamespace("*" + name));
val = "Object@" + val;
}
} else if (typeof val == "string" && val.startsWith("@0x")) {
ref = variableCreate(getNamespace("*&" + name.substr));
val = "Ref" + val;
} else if (typeof val == "string" && val.startsWith("<...>")) {
ref = variableCreate(getNamespace(name));
val = "...";
}
return {
name: name,
value: val,
variablesReference: ref
};
};
parseCommaValue = () => {
value = value.trim();
if (value[0] != ',')
return undefined;
value = value.substr(1).trim();
return parseValue();
};
parseCommaResult = (pushToStack: boolean = false) => {
value = value.trim();
if (value[0] != ',')
return undefined;
value = value.substr(1).trim();
return parseResult(pushToStack);
};
value = value.trim();
return parseValue();
}

View File

@ -0,0 +1,21 @@
import * as ChildProcess from "child_process";
import * as fs from "fs";
export function spawnTerminalEmulator(preferedEmulator: string): Thenable<string> {
return new Promise((resolve, reject) => {
const ttyFileOutput = "/tmp/vscode-gdb-tty-0" + Math.floor(Math.random() * 100000000).toString(36);
ChildProcess.spawn(preferedEmulator || "x-terminal-emulator", ["-e", "sh -c \"tty > " + ttyFileOutput + " && sleep 4294967294\""]);
let it = 0;
const interval = setInterval(() => {
if (fs.existsSync(ttyFileOutput)) {
clearInterval(interval);
const tty = fs.readFileSync(ttyFileOutput).toString("utf8");
fs.unlinkSync(ttyFileOutput);
return resolve(tty);
}
it++;
if (it > 500)
reject();
}, 10);
});
}

917
src/backend/mi2/mi2.ts Normal file
View File

@ -0,0 +1,917 @@
import { Breakpoint, IBackend, Thread, Stack, SSHArguments, Variable, VariableObject, MIError } from "../backend";
import * as ChildProcess from "child_process";
import { EventEmitter } from "events";
import { parseMI, MINode } from '../mi_parse';
import * as linuxTerm from '../linux/console';
import * as net from "net";
import * as fs from "fs";
import * as path from "path";
import { Client } from "ssh2";
export function escape(str: string) {
return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
}
const nonOutput = /^(?:\d*|undefined)[\*\+\=]|[\~\@\&\^]/;
const gdbMatch = /(?:\d*|undefined)\(gdb\)/;
const numRegex = /\d+/;
function couldBeOutput(line: string) {
if (nonOutput.exec(line))
return false;
return true;
}
const trace = false;
export class MI2 extends EventEmitter implements IBackend {
constructor(public application: string, public preargs: string[], public extraargs: string[], procEnv: any, public extraCommands: string[] = []) {
super();
if (procEnv) {
const env = {};
// Duplicate process.env so we don't override it
for (const key in process.env)
if (process.env.hasOwnProperty(key))
env[key] = process.env[key];
// Overwrite with user specified variables
for (const key in procEnv) {
if (procEnv.hasOwnProperty(key)) {
if (procEnv === null)
delete env[key];
else
env[key] = procEnv[key];
}
}
this.procEnv = env;
}
}
load(cwd: string, target: string, procArgs: string, separateConsole: string): Thenable<any> {
if (!path.isAbsolute(target))
target = path.join(cwd, target);
return new Promise((resolve, reject) => {
this.isSSH = false;
const args = this.preargs.concat(this.extraargs || []);
this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv });
this.process.stdout.on("data", this.stdout.bind(this));
this.process.stderr.on("data", this.stderr.bind(this));
this.process.on("exit", (() => { this.emit("quit"); }).bind(this));
this.process.on("error", ((err) => { this.emit("launcherror", err); }).bind(this));
const promises = this.initCommands(target, cwd);
if (procArgs && procArgs.length)
promises.push(this.sendCommand("exec-arguments " + procArgs));
if (process.platform == "win32") {
if (separateConsole !== undefined)
promises.push(this.sendCommand("gdb-set new-console on"));
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
} else {
if (separateConsole !== undefined) {
linuxTerm.spawnTerminalEmulator(separateConsole).then(tty => {
promises.push(this.sendCommand("inferior-tty-set " + tty));
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
});
} else {
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
}
}
});
}
ssh(args: SSHArguments, cwd: string, target: string, procArgs: string, separateConsole: string, attach: boolean): Thenable<any> {
return new Promise((resolve, reject) => {
this.isSSH = true;
this.sshReady = false;
this.sshConn = new Client();
if (separateConsole !== undefined)
this.log("stderr", "WARNING: Output to terminal emulators are not supported over SSH");
if (args.forwardX11) {
this.sshConn.on("x11", (info, accept, reject) => {
const xserversock = new net.Socket();
xserversock.on("error", (err) => {
this.log("stderr", "Could not connect to local X11 server! Did you enable it in your display manager?\n" + err);
});
xserversock.on("connect", () => {
const xclientsock = accept();
xclientsock.pipe(xserversock).pipe(xclientsock);
});
xserversock.connect(args.x11port, args.x11host);
});
}
const connectionArgs: any = {
host: args.host,
port: args.port,
username: args.user
};
if (args.useAgent) {
connectionArgs.agent = process.env.SSH_AUTH_SOCK;
} else if (args.keyfile) {
if (require("fs").existsSync(args.keyfile))
connectionArgs.privateKey = require("fs").readFileSync(args.keyfile);
else {
this.log("stderr", "SSH key file does not exist!");
this.emit("quit");
reject();
return;
}
} else {
connectionArgs.password = args.password;
}
this.sshConn.on("ready", () => {
this.log("stdout", "Running " + this.application + " over ssh...");
const execArgs: any = {};
if (args.forwardX11) {
execArgs.x11 = {
single: false,
screen: args.remotex11screen
};
}
let sshCMD = this.application + " " + this.preargs.concat(this.extraargs || []).join(" ");
if (args.bootstrap) sshCMD = args.bootstrap + " && " + sshCMD;
this.sshConn.exec(sshCMD, execArgs, (err, stream) => {
if (err) {
this.log("stderr", "Could not run " + this.application + "(" + sshCMD +") over ssh!");
if (err === undefined) {
err = "<reason unknown>"
}
this.log("stderr", err.toString());
this.emit("quit");
reject();
return;
}
this.sshReady = true;
this.stream = stream;
stream.on("data", this.stdout.bind(this));
stream.stderr.on("data", this.stderr.bind(this));
stream.on("exit", (() => {
this.emit("quit");
this.sshConn.end();
}).bind(this));
const promises = this.initCommands(target, cwd, attach);
promises.push(this.sendCommand("environment-cd \"" + escape(cwd) + "\""));
if (attach) {
// Attach to local process
promises.push(this.sendCommand("target-attach " + target));
} else if (procArgs && procArgs.length)
promises.push(this.sendCommand("exec-arguments " + procArgs));
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
});
}).on("error", (err) => {
this.log("stderr", "Error running " + this.application + " over ssh!");
if (err === undefined) {
err = "<reason unknown>"
}
this.log("stderr", err.toString());
this.emit("quit");
reject();
}).connect(connectionArgs);
});
}
protected initCommands(target: string, cwd: string, attach: boolean = false) {
// We need to account for the possibility of the path type used by the debugger being different
// than the path type where the extension is running (e.g., SSH from Linux to Windows machine).
// Since the CWD is expected to be an absolute path in the debugger's environment, we can test
// that to determine the path type used by the debugger and use the result of that test to
// select the correct API to check whether the target path is an absolute path.
const debuggerPath = path.posix.isAbsolute(cwd) ? path.posix : path.win32;
if (!debuggerPath.isAbsolute(target))
target = debuggerPath.join(cwd, target);
const cmds = [
this.sendCommand("gdb-set target-async on", true),
new Promise(resolve => {
this.sendCommand("list-features").then(done => {
this.features = done.result("features");
resolve(undefined);
}, () => {
// Default to no supported features on error
this.features = [];
resolve(undefined);
});
}),
this.sendCommand("environment-directory \"" + escape(cwd) + "\"", true)
];
if (!attach)
cmds.push(this.sendCommand("file-exec-and-symbols \"" + escape(target) + "\""));
if (this.prettyPrint)
cmds.push(this.sendCommand("enable-pretty-printing"));
for (let cmd of this.extraCommands) {
cmds.push(this.sendCommand(cmd));
}
return cmds;
}
attach(cwd: string, executable: string, target: string): Thenable<any> {
return new Promise((resolve, reject) => {
let args = [];
if (executable && !path.isAbsolute(executable))
executable = path.join(cwd, executable);
args = this.preargs.concat(this.extraargs || []);
this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv });
this.process.stdout.on("data", this.stdout.bind(this));
this.process.stderr.on("data", this.stderr.bind(this));
this.process.on("exit", (() => { this.emit("quit"); }).bind(this));
this.process.on("error", ((err) => { this.emit("launcherror", err); }).bind(this));
const promises = this.initCommands(target, cwd, true);
if (target.startsWith("extended-remote")) {
promises.push(this.sendCommand("target-select " + target));
if (executable)
promises.push(this.sendCommand("file-symbol-file \"" + escape(executable) + "\""));
} else {
// Attach to local process
if (executable)
promises.push(this.sendCommand("file-exec-and-symbols \"" + escape(executable) + "\""));
promises.push(this.sendCommand("target-attach " + target));
}
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
});
}
connect(cwd: string, executable: string, target: string): Thenable<any> {
return new Promise((resolve, reject) => {
let args = [];
if (executable && !path.isAbsolute(executable))
executable = path.join(cwd, executable);
args = this.preargs.concat(this.extraargs || []);
if (executable)
args = args.concat([executable]);
this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv });
this.process.stdout.on("data", this.stdout.bind(this));
this.process.stderr.on("data", this.stderr.bind(this));
this.process.on("exit", (() => { this.emit("quit"); }).bind(this));
this.process.on("error", ((err) => { this.emit("launcherror", err); }).bind(this));
const promises = this.initCommands(target, cwd, true);
promises.push(this.sendCommand("target-select remote " + target));
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
});
}
stdout(data) {
if (trace)
this.log("stderr", "stdout: " + data);
if (typeof data == "string")
this.buffer += data;
else
this.buffer += data.toString("utf8");
const end = this.buffer.lastIndexOf('\n');
if (end != -1) {
this.onOutput(this.buffer.substr(0, end));
this.buffer = this.buffer.substr(end + 1);
}
if (this.buffer.length) {
if (this.onOutputPartial(this.buffer)) {
this.buffer = "";
}
}
}
stderr(data) {
if (typeof data == "string")
this.errbuf += data;
else
this.errbuf += data.toString("utf8");
const end = this.errbuf.lastIndexOf('\n');
if (end != -1) {
this.onOutputStderr(this.errbuf.substr(0, end));
this.errbuf = this.errbuf.substr(end + 1);
}
if (this.errbuf.length) {
this.logNoNewLine("stderr", this.errbuf);
this.errbuf = "";
}
}
onOutputStderr(lines) {
lines = <string[]> lines.split('\n');
lines.forEach(line => {
this.log("stderr", line);
});
}
onOutputPartial(line) {
if (couldBeOutput(line)) {
this.logNoNewLine("stdout", line);
return true;
}
return false;
}
onOutput(lines) {
lines = <string[]> lines.split('\n');
lines.forEach(line => {
if (couldBeOutput(line)) {
if (!gdbMatch.exec(line))
this.log("stdout", line);
} else {
const parsed = parseMI(line);
if (this.debugOutput)
this.log("log", "GDB -> App: " + JSON.stringify(parsed));
let handled = false;
if (parsed.token !== undefined) {
if (this.handlers[parsed.token]) {
this.handlers[parsed.token](parsed);
delete this.handlers[parsed.token];
handled = true;
}
}
if (!handled && parsed.resultRecords && parsed.resultRecords.resultClass == "error") {
this.log("stderr", parsed.result("msg") || line);
}
if (parsed.outOfBandRecord) {
parsed.outOfBandRecord.forEach(record => {
if (record.isStream) {
this.log(record.type, record.content);
} else {
if (record.type == "exec") {
this.emit("exec-async-output", parsed);
if (record.asyncClass == "running") {
this.status = 'running';
this.emit("running", parsed);
}
else if (record.asyncClass == "stopped") {
this.status = 'stopped';
const reason = parsed.record("reason");
if (reason === undefined) {
if (trace)
this.log("stderr", "stop (no reason given)");
// attaching to a process stops, but does not provide a reason
// also python generated interrupt seems to only produce this
this.emit("step-other", parsed);
} else {
if (trace)
this.log("stderr", "stop: " + reason);
switch (reason) {
case "breakpoint-hit":
this.emit("breakpoint", parsed);
break;
case "watchpoint-trigger":
case "read-watchpoint-trigger":
case "access-watchpoint-trigger":
this.emit("watchpoint", parsed);
break;
case "function-finished":
// identical result -> send step-end
// this.emit("step-out-end", parsed);
// break;
case "location-reached":
case "end-stepping-range":
this.emit("step-end", parsed);
break;
case "watchpoint-scope":
case "solib-event":
case "syscall-entry":
case "syscall-return":
// TODO: inform the user
this.emit("step-end", parsed);
break;
case "fork":
case "vfork":
case "exec":
// TODO: inform the user, possibly add second inferiour
this.emit("step-end", parsed);
break;
case "signal-received":
this.emit("signal-stop", parsed);
break;
case "exited-normally":
this.emit("exited-normally", parsed);
break;
case "exited": // exit with error code != 0
this.log("stderr", "Program exited with code " + parsed.record("exit-code"));
this.emit("exited-normally", parsed);
break;
// case "exited-signalled": // consider handling that explicit possible
// this.log("stderr", "Program exited because of signal " + parsed.record("signal"));
// this.emit("stoped", parsed);
// break;
default:
this.log("console", "Not implemented stop reason (assuming exception): " + reason);
this.emit("stopped", parsed);
break;
}
}
} else
this.log("log", JSON.stringify(parsed));
} else if (record.type == "notify") {
if (record.asyncClass == "thread-created") {
this.emit("thread-created", parsed);
} else if (record.asyncClass == "thread-exited") {
this.emit("thread-exited", parsed);
}
}
}
});
handled = true;
}
if (parsed.token == undefined && parsed.resultRecords == undefined && parsed.outOfBandRecord.length == 0)
handled = true;
if (!handled)
this.log("log", "Unhandled: " + JSON.stringify(parsed));
}
});
}
start(runToStart: boolean): Thenable<boolean> {
const options: string[] = [];
if (runToStart)
options.push("--start");
const startCommand: string = ["exec-run"].concat(options).join(" ");
return new Promise((resolve, reject) => {
this.log("console", "Running executable");
this.sendCommand(startCommand).then((info) => {
if (info.resultRecords.resultClass == "running")
resolve(undefined);
else
reject();
}, reject);
});
}
stop() {
if (this.isSSH) {
const proc = this.stream;
const to = setTimeout(() => {
proc.signal("KILL");
}, 1000);
this.stream.on("exit", function (code) {
clearTimeout(to);
});
this.sendRaw("-gdb-exit");
} else {
const proc = this.process;
const to = setTimeout(() => {
process.kill(-proc.pid);
}, 1000);
this.process.on("exit", function (code) {
clearTimeout(to);
});
this.sendRaw("-gdb-exit");
}
}
detach() {
const proc = this.process;
const to = setTimeout(() => {
process.kill(-proc.pid);
}, 1000);
this.process.on("exit", function (code) {
clearTimeout(to);
});
this.sendRaw("-target-detach");
}
interrupt(): Thenable<boolean> {
if (trace)
this.log("stderr", "interrupt");
return new Promise((resolve, reject) => {
this.sendCommand("exec-interrupt").then((info) => {
resolve(info.resultRecords.resultClass == "done");
}, reject);
});
}
continue(reverse: boolean = false): Thenable<boolean> {
if (trace)
this.log("stderr", "continue");
return new Promise((resolve, reject) => {
this.sendCommand("exec-continue" + (reverse ? " --reverse" : "")).then((info) => {
resolve(info.resultRecords.resultClass == "running");
}, reject);
});
}
next(reverse: boolean = false): Thenable<boolean> {
if (trace)
this.log("stderr", "next");
return new Promise((resolve, reject) => {
this.sendCommand("exec-next" + (reverse ? " --reverse" : "")).then((info) => {
resolve(info.resultRecords.resultClass == "running");
}, reject);
});
}
step(reverse: boolean = false): Thenable<boolean> {
if (trace)
this.log("stderr", "step");
return new Promise((resolve, reject) => {
this.sendCommand("exec-step" + (reverse ? " --reverse" : "")).then((info) => {
resolve(info.resultRecords.resultClass == "running");
}, reject);
});
}
stepOut(reverse: boolean = false): Thenable<boolean> {
if (trace)
this.log("stderr", "stepOut");
return new Promise((resolve, reject) => {
this.sendCommand("exec-finish" + (reverse ? " --reverse" : "")).then((info) => {
resolve(info.resultRecords.resultClass == "running");
}, reject);
});
}
goto(filename: string, line: number): Thenable<Boolean> {
if (trace)
this.log("stderr", "goto");
return new Promise((resolve, reject) => {
const target: string = '"' + (filename ? escape(filename) + ":" : "") + line + '"';
this.sendCommand("break-insert -t " + target).then(() => {
this.sendCommand("exec-jump " + target).then((info) => {
resolve(info.resultRecords.resultClass == "running");
}, reject);
}, reject);
});
}
changeVariable(name: string, rawValue: string): Thenable<any> {
if (trace)
this.log("stderr", "changeVariable");
return this.sendCommand("gdb-set var " + name + "=" + rawValue);
}
loadBreakPoints(breakpoints: Breakpoint[]): Thenable<[boolean, Breakpoint][]> {
if (trace)
this.log("stderr", "loadBreakPoints");
const promisses = [];
breakpoints.forEach(breakpoint => {
promisses.push(this.addBreakPoint(breakpoint));
});
return Promise.all(promisses);
}
setBreakPointCondition(bkptNum, condition): Thenable<any> {
if (trace)
this.log("stderr", "setBreakPointCondition");
return this.sendCommand("break-condition " + bkptNum + " " + condition);
}
setEntryBreakPoint(entryPoint: string): Thenable<any> {
return this.sendCommand("break-insert -t -f " + entryPoint);
}
addBreakPoint(breakpoint: Breakpoint): Thenable<[boolean, Breakpoint]> {
if (trace)
this.log("stderr", "addBreakPoint");
return new Promise((resolve, reject) => {
if (this.breakpoints.has(breakpoint))
return resolve([false, undefined]);
let location = "";
if (breakpoint.countCondition) {
if (breakpoint.countCondition[0] == ">")
location += "-i " + numRegex.exec(breakpoint.countCondition.substr(1))[0] + " ";
else {
const match = numRegex.exec(breakpoint.countCondition)[0];
if (match.length != breakpoint.countCondition.length) {
this.log("stderr", "Unsupported break count expression: '" + breakpoint.countCondition + "'. Only supports 'X' for breaking once after X times or '>X' for ignoring the first X breaks");
location += "-t ";
} else if (parseInt(match) != 0)
location += "-t -i " + parseInt(match) + " ";
}
}
if (breakpoint.raw)
location += '"' + escape(breakpoint.raw) + '"';
else
location += '"' + escape(breakpoint.file) + ":" + breakpoint.line + '"';
this.sendCommand("break-insert -f " + location).then((result) => {
if (result.resultRecords.resultClass == "done") {
const bkptNum = parseInt(result.result("bkpt.number"));
const newBrk = {
file: breakpoint.file ? breakpoint.file : result.result("bkpt.file"),
raw: breakpoint.raw,
line: parseInt(result.result("bkpt.line")),
condition: breakpoint.condition
};
if (breakpoint.condition) {
this.setBreakPointCondition(bkptNum, breakpoint.condition).then((result) => {
if (result.resultRecords.resultClass == "done") {
this.breakpoints.set(newBrk, bkptNum);
resolve([true, newBrk]);
} else {
resolve([false, undefined]);
}
}, reject);
} else {
this.breakpoints.set(newBrk, bkptNum);
resolve([true, newBrk]);
}
} else {
reject(result);
}
}, reject);
});
}
removeBreakPoint(breakpoint: Breakpoint): Thenable<boolean> {
if (trace)
this.log("stderr", "removeBreakPoint");
return new Promise((resolve, reject) => {
if (!this.breakpoints.has(breakpoint))
return resolve(false);
this.sendCommand("break-delete " + this.breakpoints.get(breakpoint)).then((result) => {
if (result.resultRecords.resultClass == "done") {
this.breakpoints.delete(breakpoint);
resolve(true);
} else resolve(false);
});
});
}
clearBreakPoints(source?: string): Thenable<any> {
if (trace)
this.log("stderr", "clearBreakPoints");
return new Promise((resolve, reject) => {
const promises = [];
const breakpoints = this.breakpoints;
this.breakpoints = new Map();
breakpoints.forEach((k, index) => {
if (index.file === source) {
promises.push(this.sendCommand("break-delete " + k).then((result) => {
if (result.resultRecords.resultClass == "done") resolve(true);
else resolve(false);
}));
} else {
this.breakpoints.set(index, k);
}
});
Promise.all(promises).then(resolve, reject);
});
}
async getThreads(): Promise<Thread[]> {
if (trace) this.log("stderr", "getThreads");
const command = "thread-info";
const result = await this.sendCommand(command);
const threads = result.result("threads");
const ret: Thread[] = [];
return threads.map(element => {
const ret: Thread = {
id: parseInt(MINode.valueOf(element, "id")),
targetId: MINode.valueOf(element, "target-id")
};
ret.name = MINode.valueOf(element, "details")
|| undefined;
return ret;
});
}
async getStack(startFrame: number, maxLevels: number, thread: number): Promise<Stack[]> {
if (trace) this.log("stderr", "getStack");
const options: string[] = [];
if (thread != 0)
options.push("--thread " + thread);
const depth: number = (await this.sendCommand(["stack-info-depth"].concat(options).join(" "))).result("depth").valueOf();
const lowFrame: number = startFrame ? startFrame : 0;
const highFrame: number = (maxLevels ? Math.min(depth, lowFrame + maxLevels) : depth) - 1;
if (highFrame < lowFrame)
return [];
options.push(lowFrame.toString());
options.push(highFrame.toString());
const result = await this.sendCommand(["stack-list-frames"].concat(options).join(" "));
const stack = result.result("stack");
return stack.map(element => {
const level = MINode.valueOf(element, "@frame.level");
const addr = MINode.valueOf(element, "@frame.addr");
const func = MINode.valueOf(element, "@frame.func");
const filename = MINode.valueOf(element, "@frame.file");
let file: string = MINode.valueOf(element, "@frame.fullname");
if (file) {
if (this.isSSH)
file = path.posix.normalize(file);
else
file = path.normalize(file);
}
let line = 0;
const lnstr = MINode.valueOf(element, "@frame.line");
if (lnstr)
line = parseInt(lnstr);
const from = parseInt(MINode.valueOf(element, "@frame.from"));
return {
address: addr,
fileName: filename,
file: file,
function: func || from,
level: level,
line: line
};
});
}
async getStackVariables(thread: number, frame: number): Promise<Variable[]> {
if (trace)
this.log("stderr", "getStackVariables");
const result = await this.sendCommand(`stack-list-variables --thread ${thread} --frame ${frame} --simple-values`);
const variables = result.result("variables");
const ret: Variable[] = [];
for (const element of variables) {
const key = MINode.valueOf(element, "name");
const value = MINode.valueOf(element, "value");
const type = MINode.valueOf(element, "type");
ret.push({
name: key,
valueStr: value,
type: type,
raw: element
});
}
return ret;
}
examineMemory(from: number, length: number): Thenable<any> {
if (trace)
this.log("stderr", "examineMemory");
return new Promise((resolve, reject) => {
this.sendCommand("data-read-memory-bytes 0x" + from.toString(16) + " " + length).then((result) => {
resolve(result.result("memory[0].contents"));
}, reject);
});
}
record(): Thenable<any> {
if (trace)
this.log("stderr", "record");
return new Promise((resolve, reject) => {
if (this.status != 'stopped') {
resolve(this.isRecord ? {result:'0', oldValue:'1'} : { result:'0', oldValue:'0'});
this.log("stderr", `WARNING: The program is ${this.status} and cannot ${this.isRecord ? 'stop' : 'start'} reverse debugging.`);
return;
}
this.isRecord = !this.isRecord;
this.sendCliCommand(this.isRecord ? "record" : "record stop").then((result) => {
if (result.resultRecords.resultClass == "done") {
resolve(this.isRecord ? {result:'1', oldValue:'0'} : { result:'1', oldValue:'1'});
}
else {
this.isRecord = !this.isRecord; //restore
resolve(this.isRecord ? {result:'0', oldValue:'1'} : { result:'0', oldValue:'0'});
}
}, reject);
});
}
async evalExpression(name: string, thread: number, frame: number): Promise<MINode> {
if (trace)
this.log("stderr", "evalExpression");
let command = "data-evaluate-expression ";
if (thread != 0) {
command += `--thread ${thread} --frame ${frame} `;
}
command += name;
return await this.sendCommand(command);
}
async varCreate(expression: string, name: string = "-", frame: string = "@"): Promise<VariableObject> {
if (trace)
this.log("stderr", "varCreate");
const res = await this.sendCommand(`var-create ${this.quote(name)} ${frame} "${expression}"`);
return new VariableObject(res.result(""));
}
async varEvalExpression(name: string): Promise<MINode> {
if (trace)
this.log("stderr", "varEvalExpression");
return this.sendCommand(`var-evaluate-expression ${this.quote(name)}`);
}
async varListChildren(name: string): Promise<VariableObject[]> {
if (trace)
this.log("stderr", "varListChildren");
//TODO: add `from` and `to` arguments
const res = await this.sendCommand(`var-list-children --all-values ${this.quote(name)}`);
const children = res.result("children") || [];
const omg: VariableObject[] = children.map(child => new VariableObject(child[1]));
return omg;
}
async varUpdate(name: string = "*"): Promise<MINode> {
if (trace)
this.log("stderr", "varUpdate");
return this.sendCommand(`var-update --all-values ${this.quote(name)}`);
}
async varAssign(name: string, rawValue: string): Promise<MINode> {
if (trace)
this.log("stderr", "varAssign");
return this.sendCommand(`var-assign ${this.quote(name)} ${rawValue}`);
}
logNoNewLine(type: string, msg: string) {
this.emit("msg", type, msg);
}
log(type: string, msg: string) {
this.emit("msg", type, msg[msg.length - 1] == '\n' ? msg : (msg + "\n"));
}
sendUserInput(command: string, threadId: number = 0, frameLevel: number = 0): Thenable<MINode> {
if (command.startsWith("-")) {
return this.sendCommand(command.substr(1));
} else {
return this.sendCliCommand(command, threadId, frameLevel);
}
}
sendRaw(raw: string) {
if (this.printCalls)
this.log("log", raw);
if (this.isSSH)
this.stream.write(raw + "\n");
else
this.process.stdin.write(raw + "\n");
}
sendCliCommand(command: string, threadId: number = 0, frameLevel: number = 0): Thenable<MINode> {
let miCommand = "interpreter-exec ";
if (threadId != 0) {
miCommand += `--thread ${threadId} --frame ${frameLevel} `;
}
miCommand += `console "${command.replace(/[\\"']/g, '\\$&')}"`;
return this.sendCommand(miCommand);
}
sendCommand(command: string, suppressFailure: boolean = false): Thenable<MINode> {
const sel = this.currentToken++;
return new Promise((resolve, reject) => {
this.handlers[sel] = (node: MINode) => {
if (node && node.resultRecords && node.resultRecords.resultClass === "error") {
if (suppressFailure) {
this.log("stderr", `WARNING: Error executing command '${command}'`);
resolve(node);
} else
reject(new MIError(node.result("msg") || "Internal error", command));
} else
resolve(node);
};
this.sendRaw(sel + "-" + command);
});
}
isReady(): boolean {
return this.isSSH ? this.sshReady : !!this.process;
}
protected quote(text: string): string {
// only escape if text contains non-word or non-path characters such as whitespace or quotes
return /^-|[^\w\d\/_\-\.]/g.test(text) ? ('"' + escape(text) + '"') : text;
}
isRecord:boolean = false;
status: 'running' | 'stopped' | 'none' = 'none';
prettyPrint: boolean = true;
printCalls: boolean;
debugOutput: boolean;
features: string[];
public procEnv: any;
protected isSSH: boolean;
protected sshReady: boolean;
protected currentToken: number = 1;
protected handlers: { [index: number]: (info: MINode) => any } = {};
protected breakpoints: Map<Breakpoint, Number> = new Map();
protected buffer: string;
protected errbuf: string;
protected process: ChildProcess.ChildProcess;
protected stream;
protected sshConn;
}

View File

@ -0,0 +1,72 @@
import { MI2, escape } from "./mi2";
import { Breakpoint } from "../backend";
import * as ChildProcess from "child_process";
import * as path from "path";
export class MI2_LLDB extends MI2 {
protected initCommands(target: string, cwd: string, attach: boolean = false) {
// We need to account for the possibility of the path type used by the debugger being different
// than the path type where the extension is running (e.g., SSH from Linux to Windows machine).
// Since the CWD is expected to be an absolute path in the debugger's environment, we can test
// that to determine the path type used by the debugger and use the result of that test to
// select the correct API to check whether the target path is an absolute path.
const debuggerPath = path.posix.isAbsolute(cwd) ? path.posix : path.win32;
if (!debuggerPath.isAbsolute(target))
target = debuggerPath.join(cwd, target);
const cmds = [
this.sendCommand("gdb-set target-async on"),
new Promise(resolve => {
this.sendCommand("list-features").then(done => {
this.features = done.result("features");
resolve(undefined);
}, err => {
this.features = [];
resolve(undefined);
});
})
];
if (!attach)
cmds.push(this.sendCommand("file-exec-and-symbols \"" + escape(target) + "\""));
for (let cmd of this.extraCommands) {
cmds.push(this.sendCliCommand(cmd));
}
return cmds;
}
attach(cwd: string, executable: string, target: string): Thenable<any> {
return new Promise((resolve, reject) => {
const args = this.preargs.concat(this.extraargs || []);
this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv });
this.process.stdout.on("data", this.stdout.bind(this));
this.process.stderr.on("data", this.stderr.bind(this));
this.process.on("exit", (() => { this.emit("quit"); }).bind(this));
this.process.on("error", ((err) => { this.emit("launcherror", err); }).bind(this));
const promises = this.initCommands(target, cwd, true);
promises.push(this.sendCommand("file-exec-and-symbols \"" + escape(executable) + "\""));
promises.push(this.sendCommand("target-attach " + target));
Promise.all(promises).then(() => {
this.emit("debug-ready");
resolve(undefined);
}, reject);
});
}
setBreakPointCondition(bkptNum, condition): Thenable<any> {
return this.sendCommand("break-condition " + bkptNum + " \"" + escape(condition) + "\" 1");
}
goto(filename: string, line: number): Thenable<Boolean> {
return new Promise((resolve, reject) => {
// LLDB parses the file differently than GDB...
// GDB doesn't allow quoting only the file but only the whole argument
// LLDB doesn't allow quoting the whole argument but rather only the file
const target: string = (filename ? '"' + escape(filename) + '":' : "") + line;
this.sendCliCommand("jump " + target).then(() => {
this.emit("step-other", null);
resolve(true);
}, reject);
});
}
}

View File

@ -0,0 +1,45 @@
import { MI2_LLDB } from "./mi2lldb";
import { Stack } from "../backend";
import { MINode } from "../mi_parse";
export class MI2_Mago extends MI2_LLDB {
getStack(startFrame: number, maxLevels: number, thread: number): Promise<Stack[]> {
return new Promise((resolve, reject) => {
const command = "stack-list-frames";
this.sendCommand(command).then((result) => {
const stack = result.resultRecords.results;
const ret: Stack[] = [];
const remaining = [];
const addToStack = (element) => {
const level = MINode.valueOf(element, "frame.level");
const addr = MINode.valueOf(element, "frame.addr");
const func = MINode.valueOf(element, "frame.func");
const filename = MINode.valueOf(element, "file");
const file = MINode.valueOf(element, "fullname");
let line = 0;
const lnstr = MINode.valueOf(element, "line");
if (lnstr)
line = parseInt(lnstr);
const from = parseInt(MINode.valueOf(element, "from"));
ret.push({
address: addr,
fileName: filename || "",
file: file || "<unknown>",
function: func || from || "<unknown>",
level: level,
line: line
});
};
stack.forEach(element => {
if (element)
if (element[0] == "stack") {
addToStack(element[1]);
} else remaining.push(element);
});
if (remaining.length)
addToStack(remaining);
resolve(ret);
}, reject);
});
}
}

313
src/backend/mi_parse.ts Normal file
View File

@ -0,0 +1,313 @@
export interface MIInfo {
token: number;
outOfBandRecord: { isStream: boolean, type: string, asyncClass: string, output: [string, any][], content: string }[];
resultRecords: { resultClass: string, results: [string, any][] };
}
const octalMatch = /^[0-7]{3}/;
function parseString(str: string): string {
const ret = new Buffer(str.length * 4);
let bufIndex = 0;
if (str[0] != '"' || str[str.length - 1] != '"')
throw new Error("Not a valid string");
str = str.slice(1, -1);
let escaped = false;
for (let i = 0; i < str.length; i++) {
if (escaped) {
let m;
if (str[i] == '\\')
bufIndex += ret.write('\\', bufIndex);
else if (str[i] == '"')
bufIndex += ret.write('"', bufIndex);
else if (str[i] == '\'')
bufIndex += ret.write('\'', bufIndex);
else if (str[i] == 'n')
bufIndex += ret.write('\n', bufIndex);
else if (str[i] == 'r')
bufIndex += ret.write('\r', bufIndex);
else if (str[i] == 't')
bufIndex += ret.write('\t', bufIndex);
else if (str[i] == 'b')
bufIndex += ret.write('\b', bufIndex);
else if (str[i] == 'f')
bufIndex += ret.write('\f', bufIndex);
else if (str[i] == 'v')
bufIndex += ret.write('\v', bufIndex);
else if (str[i] == '0')
bufIndex += ret.write('\0', bufIndex);
else if (m = octalMatch.exec(str.substr(i))) {
ret.writeUInt8(parseInt(m[0], 8), bufIndex++);
i += 2;
} else
bufIndex += ret.write(str[i], bufIndex);
escaped = false;
} else {
if (str[i] == '\\')
escaped = true;
else if (str[i] == '"')
throw new Error("Not a valid string");
else
bufIndex += ret.write(str[i], bufIndex);
}
}
return ret.slice(0, bufIndex).toString("utf8");
}
export class MINode implements MIInfo {
token: number;
outOfBandRecord: { isStream: boolean, type: string, asyncClass: string, output: [string, any][], content: string }[];
resultRecords: { resultClass: string, results: [string, any][] };
constructor(token: number, info: { isStream: boolean, type: string, asyncClass: string, output: [string, any][], content: string }[], result: { resultClass: string, results: [string, any][] }) {
this.token = token;
this.outOfBandRecord = info;
this.resultRecords = result;
}
record(path: string): any {
if (!this.outOfBandRecord)
return undefined;
return MINode.valueOf(this.outOfBandRecord[0].output, path);
}
result(path: string): any {
if (!this.resultRecords)
return undefined;
return MINode.valueOf(this.resultRecords.results, path);
}
static valueOf(start: any, path: string): any {
if (!start)
return undefined;
const pathRegex = /^\.?([a-zA-Z_\-][a-zA-Z0-9_\-]*)/;
const indexRegex = /^\[(\d+)\](?:$|\.)/;
path = path.trim();
if (!path)
return start;
let current = start;
do {
let target = pathRegex.exec(path);
if (target) {
path = path.substr(target[0].length);
if (current.length && typeof current != "string") {
const found = [];
for (const element of current) {
if (element[0] == target[1]) {
found.push(element[1]);
}
}
if (found.length > 1) {
current = found;
} else if (found.length == 1) {
current = found[0];
} else return undefined;
} else return undefined;
} else if (path[0] == '@') {
current = [current];
path = path.substr(1);
} else {
target = indexRegex.exec(path);
if (target) {
path = path.substr(target[0].length);
const i = parseInt(target[1]);
if (current.length && typeof current != "string" && i >= 0 && i < current.length) {
current = current[i];
} else if (i == 0) {
} else return undefined;
} else return undefined;
}
path = path.trim();
} while (path);
return current;
}
}
const tokenRegex = /^\d+/;
const outOfBandRecordRegex = /^(?:(\d*|undefined)([\*\+\=])|([\~\@\&]))/;
const resultRecordRegex = /^(\d*)\^(done|running|connected|error|exit)/;
const newlineRegex = /^\r\n?/;
const endRegex = /^\(gdb\)\r\n?/;
const variableRegex = /^([a-zA-Z_\-][a-zA-Z0-9_\-]*)/;
const asyncClassRegex = /^[^,\r\n]+/;
export function parseMI(output: string): MINode {
/*
output ==>
(
exec-async-output = [ token ] "*" ("stopped" | others) ( "," variable "=" (const | tuple | list) )* \n
status-async-output = [ token ] "+" ("stopped" | others) ( "," variable "=" (const | tuple | list) )* \n
notify-async-output = [ token ] "=" ("stopped" | others) ( "," variable "=" (const | tuple | list) )* \n
console-stream-output = "~" c-string \n
target-stream-output = "@" c-string \n
log-stream-output = "&" c-string \n
)*
[
[ token ] "^" ("done" | "running" | "connected" | "error" | "exit") ( "," variable "=" (const | tuple | list) )* \n
]
"(gdb)" \n
*/
let token = undefined;
const outOfBandRecord = [];
let resultRecords = undefined;
const asyncRecordType = {
"*": "exec",
"+": "status",
"=": "notify"
};
const streamRecordType = {
"~": "console",
"@": "target",
"&": "log"
};
const parseCString = () => {
if (output[0] != '"')
return "";
let stringEnd = 1;
let inString = true;
let remaining = output.substr(1);
let escaped = false;
while (inString) {
if (escaped)
escaped = false;
else if (remaining[0] == '\\')
escaped = true;
else if (remaining[0] == '"')
inString = false;
remaining = remaining.substr(1);
stringEnd++;
}
let str;
try {
str = parseString(output.substr(0, stringEnd));
} catch (e) {
str = output.substr(0, stringEnd);
}
output = output.substr(stringEnd);
return str;
};
let parseValue, parseCommaResult, parseCommaValue, parseResult;
const parseTupleOrList = () => {
if (output[0] != '{' && output[0] != '[')
return undefined;
const oldContent = output;
const canBeValueList = output[0] == '[';
output = output.substr(1);
if (output[0] == '}' || output[0] == ']') {
output = output.substr(1); // ] or }
return [];
}
if (canBeValueList) {
let value = parseValue();
if (value !== undefined) { // is value list
const values = [];
values.push(value);
const remaining = output;
while ((value = parseCommaValue()) !== undefined)
values.push(value);
output = output.substr(1); // ]
return values;
}
}
let result = parseResult();
if (result) {
const results = [];
results.push(result);
while (result = parseCommaResult())
results.push(result);
output = output.substr(1); // }
return results;
}
output = (canBeValueList ? '[' : '{') + output;
return undefined;
};
parseValue = () => {
if (output[0] == '"')
return parseCString();
else if (output[0] == '{' || output[0] == '[')
return parseTupleOrList();
else
return undefined;
};
parseResult = () => {
const variableMatch = variableRegex.exec(output);
if (!variableMatch)
return undefined;
output = output.substr(variableMatch[0].length + 1);
const variable = variableMatch[1];
return [variable, parseValue()];
};
parseCommaValue = () => {
if (output[0] != ',')
return undefined;
output = output.substr(1);
return parseValue();
};
parseCommaResult = () => {
if (output[0] != ',')
return undefined;
output = output.substr(1);
return parseResult();
};
let match = undefined;
while (match = outOfBandRecordRegex.exec(output)) {
output = output.substr(match[0].length);
if (match[1] && token === undefined && match[1] !== "undefined") {
token = parseInt(match[1]);
}
if (match[2]) {
const classMatch = asyncClassRegex.exec(output);
output = output.substring(classMatch[0].length);
const asyncRecord = {
isStream: false,
type: asyncRecordType[match[2]],
asyncClass: classMatch[0],
output: []
};
let result;
while (result = parseCommaResult())
asyncRecord.output.push(result);
outOfBandRecord.push(asyncRecord);
} else if (match[3]) {
const streamRecord = {
isStream: true,
type: streamRecordType[match[3]],
content: parseCString()
};
outOfBandRecord.push(streamRecord);
}
output = output.replace(newlineRegex, "");
}
if (match = resultRecordRegex.exec(output)) {
output = output.substr(match[0].length);
if (match[1] && token === undefined) {
token = parseInt(match[1]);
}
resultRecords = {
resultClass: match[2],
results: []
};
let result;
while (result = parseCommaResult())
resultRecords.results.push(result);
output = output.replace(newlineRegex, "");
}
return new MINode(token, <any> outOfBandRecord || [], resultRecords);
}

207
src/frontend/extension.ts Normal file
View File

@ -0,0 +1,207 @@
import * as vscode from "vscode";
import * as net from "net";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider("debugmemory", new MemoryContentProvider()));
context.subscriptions.push(vscode.commands.registerCommand("code-debug.examineMemoryLocation", examineMemory));
context.subscriptions.push(vscode.commands.registerCommand("code-debug.getFileNameNoExt", () => {
if (!vscode.window.activeTextEditor || !vscode.window.activeTextEditor.document || !vscode.window.activeTextEditor.document.fileName) {
vscode.window.showErrorMessage("No editor with valid file name active");
return;
}
const fileName = vscode.window.activeTextEditor.document.fileName;
const ext = path.extname(fileName);
return fileName.substr(0, fileName.length - ext.length);
}));
context.subscriptions.push(vscode.commands.registerCommand("code-debug.startRecord", handleRecord));
context.subscriptions.push(vscode.commands.registerCommand("code-debug.stopRecord", handleRecord));
context.subscriptions.push(vscode.commands.registerCommand("code-debug.getFileBasenameNoExt", () => {
if (!vscode.window.activeTextEditor || !vscode.window.activeTextEditor.document || !vscode.window.activeTextEditor.document.fileName) {
vscode.window.showErrorMessage("No editor with valid file name active");
return;
}
const fileName = path.basename(vscode.window.activeTextEditor.document.fileName);
const ext = path.extname(fileName);
return fileName.substr(0, fileName.length - ext.length);
}));
vscode.debug.onDidStartDebugSession(()=>{
vscode.commands.executeCommand('setContext', 'code-debug.enableStartRecord', true);
});
}
const memoryLocationRegex = /^0x[0-9a-f]+$/;
function getMemoryRange(range: string) {
if (!range)
return undefined;
range = range.replace(/\s+/g, "").toLowerCase();
let index;
if ((index = range.indexOf("+")) != -1) {
const from = range.substr(0, index);
let length = range.substr(index + 1);
if (!memoryLocationRegex.exec(from))
return undefined;
if (memoryLocationRegex.exec(length))
length = parseInt(length.substr(2), 16).toString();
return "from=" + encodeURIComponent(from) + "&length=" + encodeURIComponent(length);
} else if ((index = range.indexOf("-")) != -1) {
const from = range.substr(0, index);
const to = range.substr(index + 1);
if (!memoryLocationRegex.exec(from))
return undefined;
if (!memoryLocationRegex.exec(to))
return undefined;
return "from=" + encodeURIComponent(from) + "&to=" + encodeURIComponent(to);
} else if (memoryLocationRegex.exec(range))
return "at=" + encodeURIComponent(range);
else return undefined;
}
function execCmd(func:any){
const socketlists = path.join(os.tmpdir(), "code-debug-sockets");
if (!fs.existsSync(socketlists)) {
if (process.platform == "win32")
return vscode.window.showErrorMessage("This command is not available on windows");
else
return vscode.window.showErrorMessage("No debugging sessions available");
}
fs.readdir(socketlists, (err, files) => {
if (err) {
if (process.platform == "win32")
return vscode.window.showErrorMessage("This command is not available on windows");
else
return vscode.window.showErrorMessage("No debugging sessions available");
}
if (files.length == 1)
func(files[0]);
else if (files.length > 0)
vscode.window.showQuickPick(files, { placeHolder: "Running debugging instance" }).then(file => func(file));
else if (process.platform == "win32")
return vscode.window.showErrorMessage("This command is not available on windows");
else
vscode.window.showErrorMessage("No debugging sessions available");
});
}
function examineMemory() {
const pickedFile = (file) => {
vscode.window.showInputBox({ placeHolder: "Memory Location or Range", validateInput: range => getMemoryRange(range) === undefined ? "Range must either be in format 0xF00-0xF01, 0xF100+32 or 0xABC154" : "" }).then(range => {
vscode.window.showTextDocument(vscode.Uri.parse("debugmemory://" + file + "?" + getMemoryRange(range)));
});
};
execCmd(pickedFile);
}
function handleRecord() {
const record = (file) => {
const conn = net.connect(path.join(os.tmpdir(), "code-debug-sockets", file));
conn.write("record");
conn.once("data", data => {
if (data){
let ret = JSON.parse(data.toString());
if (ret && ret.result == '1'){
console.log('successful to satrt reverse debugging.');
}
else {
vscode.window.showErrorMessage(`Failed to ${ret.oldValue == '1' ? 'stop' : 'start'} reverse debugging`);
}
vscode.commands.executeCommand('setContext', 'code-debug.enableStartRecord', ret.oldValue == '1' ? true : false);
}
conn.destroy();
});
};
execCmd(record);
}
class MemoryContentProvider implements vscode.TextDocumentContentProvider {
provideTextDocumentContent(uri: vscode.Uri, token: vscode.CancellationToken): Thenable<string> {
return new Promise((resolve, reject) => {
const conn = net.connect(path.join(os.tmpdir(), "code-debug-sockets", uri.authority.toLowerCase()));
let from, to;
let highlightAt = -1;
const splits = uri.query.split("&");
if (splits[0].split("=")[0] == "at") {
const loc = parseInt(splits[0].split("=")[1].substr(2), 16);
highlightAt = 64;
from = Math.max(loc - 64, 0);
to = Math.max(loc + 768, 0);
} else if (splits[0].split("=")[0] == "from") {
from = parseInt(splits[0].split("=")[1].substr(2), 16);
if (splits[1].split("=")[0] == "to") {
to = parseInt(splits[1].split("=")[1].substr(2), 16);
} else if (splits[1].split("=")[0] == "length") {
to = from + parseInt(splits[1].split("=")[1]);
} else return reject("Invalid Range");
} else return reject("Invalid Range");
if (to < from)
return reject("Negative Range");
conn.write("examineMemory " + JSON.stringify([from, to - from + 1]));
conn.once("data", data => {
let formattedCode = " 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\n";
var index: number = from;
const hexString = data.toString();
let x = 0;
let asciiLine = "";
let byteNo = 0;
for (let i = 0; i < hexString.length; i += 2) {
if (x == 0) {
var addr = index.toString(16);
while (addr.length < 16) addr = '0' + addr;
formattedCode += addr + " ";
}
index++;
const digit = hexString.substr(i, 2);
const digitNum = parseInt(digit, 16);
if (digitNum >= 32 && digitNum <= 126)
asciiLine += String.fromCharCode(digitNum);
else
asciiLine += ".";
if (highlightAt == byteNo) {
formattedCode = formattedCode.slice(0, -1) + "[" + digit + "]";
} else {
formattedCode += digit + " ";
}
if (x == 7)
formattedCode += " ";
if (++x >= 16) {
formattedCode += " " + asciiLine + "\n";
x = 0;
asciiLine = "";
}
byteNo++;
}
if (x > 0) {
for (let i = 0; i <= 16 - x; i++) {
formattedCode += " ";
}
if (x >= 8)
formattedCode = formattedCode.slice(0, -2);
else
formattedCode = formattedCode.slice(0, -1);
formattedCode += asciiLine;
}
resolve(center("Memory Range from 0x" + from.toString(16) + " to 0x" + to.toString(16), 84) + "\n\n" + formattedCode);
conn.destroy();
});
});
}
}
function center(str: string, width: number): string {
var left = true;
while (str.length < width) {
if (left) str = ' ' + str;
else str = str + ' ';
left = !left;
}
return str;
}

173
src/gdb.ts Normal file
View File

@ -0,0 +1,173 @@
import { MI2DebugSession, RunCommand } from './mibase';
import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter';
import { DebugProtocol } from 'vscode-debugprotocol';
import { MI2, escape } from "./backend/mi2/mi2";
import { SSHArguments, ValuesFormattingMode } from './backend/backend';
export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
cwd: string;
target: string;
gdbpath: string;
env: any;
debugger_args: string[];
pathSubstitutions: { [index: string]: string };
arguments: string;
terminal: string;
autorun: string[];
stopAtEntry: boolean | string;
ssh: SSHArguments;
valuesFormatting: ValuesFormattingMode;
printCalls: boolean;
showDevDebugOutput: boolean;
}
export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments {
cwd: string;
target: string;
gdbpath: string;
env: any;
debugger_args: string[];
pathSubstitutions: { [index: string]: string };
executable: string;
remote: boolean;
autorun: string[];
stopAtConnect: boolean;
stopAtEntry: boolean | string;
ssh: SSHArguments;
valuesFormatting: ValuesFormattingMode;
printCalls: boolean;
showDevDebugOutput: boolean;
}
class GDBDebugSession extends MI2DebugSession {
protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
response.body.supportsGotoTargetsRequest = true;
response.body.supportsHitConditionalBreakpoints = true;
response.body.supportsConfigurationDoneRequest = true;
response.body.supportsConditionalBreakpoints = true;
response.body.supportsFunctionBreakpoints = true;
response.body.supportsEvaluateForHovers = true;
response.body.supportsSetVariable = true;
response.body.supportsStepBack = true;
this.sendResponse(response);
}
protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void {
this.miDebugger = new MI2(args.gdbpath || "gdb", ["-q", "--interpreter=mi2"], args.debugger_args, args.env);
this.setPathSubstitutions(args.pathSubstitutions);
this.initDebugger();
this.quit = false;
this.attached = false;
this.initialRunCommand = RunCommand.RUN;
this.isSSH = false;
this.started = false;
this.crashed = false;
this.setValuesFormattingMode(args.valuesFormatting);
this.miDebugger.printCalls = !!args.printCalls;
this.miDebugger.debugOutput = !!args.showDevDebugOutput;
this.stopAtEntry = args.stopAtEntry;
if (args.ssh !== undefined) {
if (args.ssh.forwardX11 === undefined)
args.ssh.forwardX11 = true;
if (args.ssh.port === undefined)
args.ssh.port = 22;
if (args.ssh.x11port === undefined)
args.ssh.x11port = 6000;
if (args.ssh.x11host === undefined)
args.ssh.x11host = "localhost";
if (args.ssh.remotex11screen === undefined)
args.ssh.remotex11screen = 0;
this.isSSH = true;
this.setSourceFileMap(args.ssh.sourceFileMap, args.ssh.cwd, args.cwd);
this.miDebugger.ssh(args.ssh, args.ssh.cwd, args.target, args.arguments, args.terminal, false).then(() => {
if (args.autorun)
args.autorun.forEach(command => {
this.miDebugger.sendUserInput(command);
});
this.sendResponse(response);
}, err => {
this.sendErrorResponse(response, 102, `Failed to SSH: ${err.toString()}`);
});
} else {
this.miDebugger.load(args.cwd, args.target, args.arguments, args.terminal).then(() => {
if (args.autorun)
args.autorun.forEach(command => {
this.miDebugger.sendUserInput(command);
});
this.sendResponse(response);
}, err => {
this.sendErrorResponse(response, 103, `Failed to load MI Debugger: ${err.toString()}`);
});
}
}
protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void {
this.miDebugger = new MI2(args.gdbpath || "gdb", ["-q", "--interpreter=mi2"], args.debugger_args, args.env);
this.setPathSubstitutions(args.pathSubstitutions);
this.initDebugger();
this.quit = false;
this.attached = !args.remote;
this.initialRunCommand = !!args.stopAtConnect ? RunCommand.NONE : RunCommand.CONTINUE;
this.isSSH = false;
this.setValuesFormattingMode(args.valuesFormatting);
this.miDebugger.printCalls = !!args.printCalls;
this.miDebugger.debugOutput = !!args.showDevDebugOutput;
this.stopAtEntry = args.stopAtEntry;
if (args.ssh !== undefined) {
if (args.ssh.forwardX11 === undefined)
args.ssh.forwardX11 = true;
if (args.ssh.port === undefined)
args.ssh.port = 22;
if (args.ssh.x11port === undefined)
args.ssh.x11port = 6000;
if (args.ssh.x11host === undefined)
args.ssh.x11host = "localhost";
if (args.ssh.remotex11screen === undefined)
args.ssh.remotex11screen = 0;
this.isSSH = true;
this.setSourceFileMap(args.ssh.sourceFileMap, args.ssh.cwd, args.cwd);
this.miDebugger.ssh(args.ssh, args.ssh.cwd, args.target, "", undefined, true).then(() => {
if (args.autorun)
args.autorun.forEach(command => {
this.miDebugger.sendUserInput(command);
});
this.sendResponse(response);
}, err => {
this.sendErrorResponse(response, 102, `Failed to SSH: ${err.toString()}`);
});
} else {
if (args.remote) {
this.miDebugger.connect(args.cwd, args.executable, args.target).then(() => {
if (args.autorun)
args.autorun.forEach(command => {
this.miDebugger.sendUserInput(command);
});
this.sendResponse(response);
}, err => {
this.sendErrorResponse(response, 102, `Failed to attach: ${err.toString()}`);
});
} else {
this.miDebugger.attach(args.cwd, args.executable, args.target).then(() => {
if (args.autorun)
args.autorun.forEach(command => {
this.miDebugger.sendUserInput(command);
});
this.sendResponse(response);
}, err => {
this.sendErrorResponse(response, 101, `Failed to attach: ${err.toString()}`);
});
}
}
}
// Add extra commands for source file path substitution in GDB-specific syntax
protected setPathSubstitutions(substitutions: { [index: string]: string }): void {
if (substitutions) {
Object.keys(substitutions).forEach(source => {
this.miDebugger.extraCommands.push("gdb-set substitute-path \"" + escape(source) + "\" \"" + escape(substitutions[source]) + "\"");
})
}
}
}
DebugSession.run(GDBDebugSession);

126
src/lldb.ts Normal file
View File

@ -0,0 +1,126 @@
import { MI2DebugSession, RunCommand } from './mibase';
import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter';
import { DebugProtocol } from 'vscode-debugprotocol';
import { MI2_LLDB } from "./backend/mi2/mi2lldb";
import { SSHArguments, ValuesFormattingMode } from './backend/backend';
export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
cwd: string;
target: string;
lldbmipath: string;
env: any;
debugger_args: string[];
pathSubstitutions: { [index: string]: string };
arguments: string;
autorun: string[];
stopAtEntry: boolean | string;
ssh: SSHArguments;
valuesFormatting: ValuesFormattingMode;
printCalls: boolean;
showDevDebugOutput: boolean;
}
export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments {
cwd: string;
target: string;
lldbmipath: string;
env: any;
debugger_args: string[];
pathSubstitutions: { [index: string]: string };
executable: string;
autorun: string[];
stopAtConnect: boolean;
stopAtEntry: boolean | string;
valuesFormatting: ValuesFormattingMode;
printCalls: boolean;
showDevDebugOutput: boolean;
}
class LLDBDebugSession extends MI2DebugSession {
protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
response.body.supportsGotoTargetsRequest = true;
response.body.supportsHitConditionalBreakpoints = true;
response.body.supportsConfigurationDoneRequest = true;
response.body.supportsConditionalBreakpoints = true;
response.body.supportsFunctionBreakpoints = true;
response.body.supportsEvaluateForHovers = true;
this.sendResponse(response);
}
protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void {
this.miDebugger = new MI2_LLDB(args.lldbmipath || "lldb-mi", [], args.debugger_args, args.env);
this.setPathSubstitutions(args.pathSubstitutions);
this.initDebugger();
this.quit = false;
this.attached = false;
this.initialRunCommand = RunCommand.RUN;
this.isSSH = false;
this.started = false;
this.crashed = false;
this.setValuesFormattingMode(args.valuesFormatting);
this.miDebugger.printCalls = !!args.printCalls;
this.miDebugger.debugOutput = !!args.showDevDebugOutput;
this.stopAtEntry = args.stopAtEntry;
if (args.ssh !== undefined) {
if (args.ssh.forwardX11 === undefined)
args.ssh.forwardX11 = true;
if (args.ssh.port === undefined)
args.ssh.port = 22;
if (args.ssh.x11port === undefined)
args.ssh.x11port = 6000;
if (args.ssh.x11host === undefined)
args.ssh.x11host = "localhost";
if (args.ssh.remotex11screen === undefined)
args.ssh.remotex11screen = 0;
this.isSSH = true;
this.setSourceFileMap(args.ssh.sourceFileMap, args.ssh.cwd, args.cwd);
this.miDebugger.ssh(args.ssh, args.ssh.cwd, args.target, args.arguments, undefined, false).then(() => {
if (args.autorun)
args.autorun.forEach(command => {
this.miDebugger.sendUserInput(command);
});
this.sendResponse(response);
});
} else {
this.miDebugger.load(args.cwd, args.target, args.arguments, undefined).then(() => {
if (args.autorun)
args.autorun.forEach(command => {
this.miDebugger.sendUserInput(command);
});
this.sendResponse(response);
});
}
}
protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void {
this.miDebugger = new MI2_LLDB(args.lldbmipath || "lldb-mi", [], args.debugger_args, args.env);
this.setPathSubstitutions(args.pathSubstitutions);
this.initDebugger();
this.quit = false;
this.attached = true;
this.initialRunCommand = !!args.stopAtConnect ? RunCommand.NONE : RunCommand.CONTINUE;
this.isSSH = false;
this.setValuesFormattingMode(args.valuesFormatting);
this.miDebugger.printCalls = !!args.printCalls;
this.miDebugger.debugOutput = !!args.showDevDebugOutput;
this.stopAtEntry = args.stopAtEntry;
this.miDebugger.attach(args.cwd, args.executable, args.target).then(() => {
if (args.autorun)
args.autorun.forEach(command => {
this.miDebugger.sendUserInput(command);
});
this.sendResponse(response);
});
}
// Add extra commands for source file path substitution in LLDB-specific syntax
protected setPathSubstitutions(substitutions: { [index: string]: string }): void {
if (substitutions) {
Object.keys(substitutions).forEach(source => {
this.miDebugger.extraCommands.push("settings append target.source-map " + source + " " + substitutions[source]);
})
}
}
}
DebugSession.run(LLDBDebugSession);

93
src/mago.ts Normal file
View File

@ -0,0 +1,93 @@
import { MI2DebugSession, RunCommand } from './mibase';
import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, OutputEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter';
import { DebugProtocol } from 'vscode-debugprotocol';
import { MI2_Mago } from "./backend/mi2/mi2mago";
import { SSHArguments, ValuesFormattingMode } from './backend/backend';
export interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments {
cwd: string;
target: string;
magomipath: string;
env: any;
debugger_args: string[];
arguments: string;
autorun: string[];
valuesFormatting: ValuesFormattingMode;
printCalls: boolean;
showDevDebugOutput: boolean;
}
export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments {
cwd: string;
target: string;
magomipath: string;
env: any;
debugger_args: string[];
executable: string;
autorun: string[];
stopAtConnect: boolean;
valuesFormatting: ValuesFormattingMode;
printCalls: boolean;
showDevDebugOutput: boolean;
}
class MagoDebugSession extends MI2DebugSession {
public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) {
super(debuggerLinesStartAt1, isServer);
}
protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
response.body.supportsHitConditionalBreakpoints = true;
response.body.supportsConfigurationDoneRequest = true;
response.body.supportsConditionalBreakpoints = true;
response.body.supportsFunctionBreakpoints = true;
response.body.supportsEvaluateForHovers = true;
this.sendResponse(response);
}
getThreadID() {
return 0;
}
protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void {
this.miDebugger = new MI2_Mago(args.magomipath || "mago-mi", ["-q"], args.debugger_args, args.env);
this.initDebugger();
this.quit = false;
this.attached = false;
this.initialRunCommand = RunCommand.RUN;
this.isSSH = false;
this.started = false;
this.crashed = false;
this.setValuesFormattingMode(args.valuesFormatting);
this.miDebugger.printCalls = !!args.printCalls;
this.miDebugger.debugOutput = !!args.showDevDebugOutput;
this.miDebugger.load(args.cwd, args.target, args.arguments, undefined).then(() => {
if (args.autorun)
args.autorun.forEach(command => {
this.miDebugger.sendUserInput(command);
});
this.sendResponse(response);
});
}
protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void {
this.miDebugger = new MI2_Mago(args.magomipath || "mago-mi", [], args.debugger_args, args.env);
this.initDebugger();
this.quit = false;
this.attached = true;
this.initialRunCommand = !!args.stopAtConnect ? RunCommand.NONE : RunCommand.CONTINUE;
this.isSSH = false;
this.setValuesFormattingMode(args.valuesFormatting);
this.miDebugger.printCalls = !!args.printCalls;
this.miDebugger.debugOutput = !!args.showDevDebugOutput;
this.miDebugger.attach(args.cwd, args.executable, args.target).then(() => {
if (args.autorun)
args.autorun.forEach(command => {
this.miDebugger.sendUserInput(command);
});
this.sendResponse(response);
});
}
}
DebugSession.run(MagoDebugSession);

798
src/mibase.ts Normal file
View File

@ -0,0 +1,798 @@
import * as DebugAdapter from 'vscode-debugadapter';
import { DebugSession, InitializedEvent, TerminatedEvent, StoppedEvent, ThreadEvent, OutputEvent, ContinuedEvent, Thread, StackFrame, Scope, Source, Handles } from 'vscode-debugadapter';
import { DebugProtocol } from 'vscode-debugprotocol';
import { Breakpoint, IBackend, Variable, VariableObject, ValuesFormattingMode, MIError } from './backend/backend';
import { MINode } from './backend/mi_parse';
import { expandValue, isExpandable } from './backend/gdb_expansion';
import { MI2 } from './backend/mi2/mi2';
import { posix } from "path";
import * as systemPath from "path";
import * as net from "net";
import * as os from "os";
import * as fs from "fs";
class ExtendedVariable {
constructor(public name, public options) {
}
}
export enum RunCommand { CONTINUE, RUN, NONE }
const STACK_HANDLES_START = 1000;
const VAR_HANDLES_START = 512 * 256 + 1000;
export class MI2DebugSession extends DebugSession {
protected variableHandles = new Handles<string | VariableObject | ExtendedVariable>(VAR_HANDLES_START);
protected variableHandlesReverse: { [id: string]: number } = {};
protected useVarObjects: boolean;
protected quit: boolean;
protected attached: boolean;
protected initialRunCommand: RunCommand;
protected stopAtEntry: boolean | string;
protected isSSH: boolean;
protected sourceFileMap: Map<string,string>;
protected started: boolean;
protected crashed: boolean;
protected miDebugger: MI2;
protected commandServer: net.Server;
protected serverPath: string;
public constructor(debuggerLinesStartAt1: boolean, isServer: boolean = false) {
super(debuggerLinesStartAt1, isServer);
}
protected async initDebugger() {
this.miDebugger.on("launcherror", this.launchError.bind(this));
this.miDebugger.on("quit", this.quitEvent.bind(this));
this.miDebugger.on("exited-normally", this.quitEvent.bind(this));
this.miDebugger.on("stopped", this.stopEvent.bind(this));
this.miDebugger.on("msg", this.handleMsg.bind(this));
this.miDebugger.on("breakpoint", this.handleBreakpoint.bind(this));
this.miDebugger.on("watchpoint", this.handleBreak.bind(this)); // consider to parse old/new, too (otherwise it is in the console only)
this.miDebugger.on("step-end", this.handleBreak.bind(this));
// this.miDebugger.on("step-out-end", this.handleBreak.bind(this)); // was combined into step-end
this.miDebugger.on("step-other", this.handleBreak.bind(this));
this.miDebugger.on("signal-stop", this.handlePause.bind(this));
this.miDebugger.on("thread-created", this.threadCreatedEvent.bind(this));
this.miDebugger.on("thread-exited", this.threadExitedEvent.bind(this));
this.miDebugger.once("debug-ready", (() => this.sendEvent(new InitializedEvent())));
try {
const socketlists = systemPath.join(os.tmpdir(), "code-debug-sockets");
if (fs.existsSync(socketlists)) {
await cleanInvalidSocketPath(socketlists);
}
this.commandServer = net.createServer(c => {
c.on("data", data => {
const rawCmd = data.toString();
const spaceIndex = rawCmd.indexOf(" ");
let func = rawCmd;
let args = [];
if (spaceIndex != -1) {
func = rawCmd.substr(0, spaceIndex);
args = JSON.parse(rawCmd.substr(spaceIndex + 1));
}
Promise.resolve(this.miDebugger[func].apply(this.miDebugger, args)).then(data => {
c.write(data instanceof Object ? JSON.stringify(data).toString() : data.toString());
});
});
});
this.commandServer.on("error", err => {
if (process.platform != "win32")
this.handleMsg("stderr", "Code-Debug WARNING: Utility Command Server: Error in command socket " + err.toString() + "\nCode-Debug WARNING: The examine memory location command won't work");
});
if (!fs.existsSync(systemPath.join(os.tmpdir(), "code-debug-sockets")))
fs.mkdirSync(systemPath.join(os.tmpdir(), "code-debug-sockets"));
this.commandServer.listen(this.serverPath = systemPath.join(os.tmpdir(), "code-debug-sockets", ("Debug-Instance-" + Math.floor(Math.random() * 36 * 36 * 36 * 36).toString(36)).toLowerCase()));
} catch (e) {
if (process.platform != "win32")
this.handleMsg("stderr", "Code-Debug WARNING: Utility Command Server: Failed to start " + e.toString() + "\nCode-Debug WARNING: The examine memory location command won't work");
}
}
protected setValuesFormattingMode(mode: ValuesFormattingMode) {
switch (mode) {
case "disabled":
this.useVarObjects = true;
this.miDebugger.prettyPrint = false;
break;
case "prettyPrinters":
this.useVarObjects = true;
this.miDebugger.prettyPrint = true;
break;
case "parseText":
default:
this.useVarObjects = false;
this.miDebugger.prettyPrint = false;
}
}
protected handleMsg(type: string, msg: string) {
if (type == "target")
type = "stdout";
if (type == "log")
type = "stderr";
this.sendEvent(new OutputEvent(msg, type));
}
protected handleBreakpoint(info: MINode) {
const event = new StoppedEvent("breakpoint", parseInt(info.record("thread-id")));
(event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") == "all";
this.sendEvent(event);
}
protected handleBreak(info?: MINode) {
const event = new StoppedEvent("step", info ? parseInt(info.record("thread-id")) : 1);
(event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info ? info.record("stopped-threads") == "all" : true;
this.sendEvent(event);
}
protected handlePause(info: MINode) {
const event = new StoppedEvent("user request", parseInt(info.record("thread-id")));
(event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") == "all";
this.sendEvent(event);
}
protected stopEvent(info: MINode) {
if (!this.started)
this.crashed = true;
if (!this.quit) {
const event = new StoppedEvent("exception", parseInt(info.record("thread-id")));
(event as DebugProtocol.StoppedEvent).body.allThreadsStopped = info.record("stopped-threads") == "all";
this.sendEvent(event);
}
}
protected threadCreatedEvent(info: MINode) {
this.sendEvent(new ThreadEvent("started", info.record("id")));
}
protected threadExitedEvent(info: MINode) {
this.sendEvent(new ThreadEvent("exited", info.record("id")));
}
protected quitEvent() {
this.quit = true;
this.sendEvent(new TerminatedEvent());
if (this.serverPath)
fs.unlink(this.serverPath, (err) => {
console.error("Failed to unlink debug server");
});
}
protected launchError(err: any) {
this.handleMsg("stderr", "Could not start debugger process, does the program exist in filesystem?\n");
this.handleMsg("stderr", err.toString() + "\n");
this.quitEvent();
}
protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments): void {
if (this.attached)
this.miDebugger.detach();
else
this.miDebugger.stop();
this.commandServer.close();
this.commandServer = undefined;
this.sendResponse(response);
}
protected async setVariableRequest(response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetVariableArguments): Promise<void> {
try {
if (this.useVarObjects) {
let name = args.name;
if (args.variablesReference >= VAR_HANDLES_START) {
const parent = this.variableHandles.get(args.variablesReference) as VariableObject;
name = `${parent.name}.${name}`;
}
const res = await this.miDebugger.varAssign(name, args.value);
response.body = {
value: res.result("value")
};
} else {
await this.miDebugger.changeVariable(args.name, args.value);
response.body = {
value: args.value
};
}
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 11, `Could not continue: ${err}`);
}
}
protected setFunctionBreakPointsRequest(response: DebugProtocol.SetFunctionBreakpointsResponse, args: DebugProtocol.SetFunctionBreakpointsArguments): void {
const all = [];
args.breakpoints.forEach(brk => {
all.push(this.miDebugger.addBreakPoint({ raw: brk.name, condition: brk.condition, countCondition: brk.hitCondition }));
});
Promise.all(all).then(brkpoints => {
const finalBrks = [];
brkpoints.forEach(brkp => {
if (brkp[0])
finalBrks.push({ line: brkp[1].line });
});
response.body = {
breakpoints: finalBrks
};
this.sendResponse(response);
}, msg => {
this.sendErrorResponse(response, 10, msg.toString());
});
}
protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void {
this.miDebugger.clearBreakPoints(args.source.path).then(() => {
let path = args.source.path;
if (this.isSSH) {
// convert local path to ssh path
if (path.indexOf("\\") != -1)
path = path.replace(/\\/g, "/").toLowerCase();
// ideCWD is the local path, gdbCWD is the ssh path, both had the replacing \ -> / done up-front
for (let [ideCWD, gdbCWD] of this.sourceFileMap) {
if (path.startsWith(ideCWD)) {
path = posix.relative(ideCWD, path);
path = posix.join(gdbCWD, path); // we combined a guaranteed path with relative one (works with GDB both on GNU/Linux and Win32)
break;
}
}
}
const all = args.breakpoints.map(brk => {
return this.miDebugger.addBreakPoint({ file: path, line: brk.line, condition: brk.condition, countCondition: brk.hitCondition });
});
Promise.all(all).then(brkpoints => {
const finalBrks = [];
brkpoints.forEach(brkp => {
// TODO: Currently all breakpoints returned are marked as verified,
// which leads to verified breakpoints on a broken lldb.
if (brkp[0])
finalBrks.push(new DebugAdapter.Breakpoint(true, brkp[1].line));
});
response.body = {
breakpoints: finalBrks
};
this.sendResponse(response);
}, msg => {
this.sendErrorResponse(response, 9, msg.toString());
});
}, msg => {
this.sendErrorResponse(response, 9, msg.toString());
});
}
protected threadsRequest(response: DebugProtocol.ThreadsResponse): void {
if (!this.miDebugger) {
this.sendResponse(response);
return;
}
this.miDebugger.getThreads().then(threads => {
response.body = {
threads: []
};
for (const thread of threads) {
let threadName = thread.name || thread.targetId || "<unnamed>";
response.body.threads.push(new Thread(thread.id, thread.id + ":" + threadName));
}
this.sendResponse(response);
}).catch(error => {
this.sendErrorResponse(response, 17, `Could not get threads: ${error}`);
});
}
// Supports 65535 threads.
protected threadAndLevelToFrameId(threadId: number, level: number) {
return level << 16 | threadId;
}
protected frameIdToThreadAndLevel(frameId: number) {
return [frameId & 0xffff, frameId >> 16];
}
protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void {
this.miDebugger.getStack(args.startFrame, args.levels, args.threadId).then(stack => {
const ret: StackFrame[] = [];
stack.forEach(element => {
let source = undefined;
let path = element.file;
if (path) {
if (this.isSSH) {
// convert ssh path to local path
if (path.indexOf("\\") != -1)
path = path.replace(/\\/g, "/").toLowerCase();
// ideCWD is the local path, gdbCWD is the ssh path, both had the replacing \ -> / done up-front
for (let [ideCWD, gdbCWD] of this.sourceFileMap) {
if (path.startsWith(gdbCWD)) {
path = posix.relative(gdbCWD, path); // only operates on "/" paths
path = systemPath.resolve(ideCWD, path); // will do the conversion to "\" on Win32
break;
}
}
} else if (process.platform === "win32") {
if (path.startsWith("\\cygdrive\\") || path.startsWith("/cygdrive/")) {
path = path[10] + ":" + path.substr(11); // replaces /cygdrive/c/foo/bar.txt with c:/foo/bar.txt
}
}
source = new Source(element.fileName, path);
}
ret.push(new StackFrame(
this.threadAndLevelToFrameId(args.threadId, element.level),
element.function + "@" + element.address,
source,
element.line,
0));
});
response.body = {
stackFrames: ret
};
this.sendResponse(response);
}, err => {
this.sendErrorResponse(response, 12, `Failed to get Stack Trace: ${err.toString()}`);
});
}
protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void {
const promises: Thenable<any>[] = [];
let entryPoint: string | undefined = undefined;
let runToStart: boolean = false;
// Setup temporary breakpoint for the entry point if needed.
switch (this.initialRunCommand) {
case RunCommand.CONTINUE:
case RunCommand.NONE:
if (typeof this.stopAtEntry == 'boolean' && this.stopAtEntry)
entryPoint = "main"; // sensible default
else if (typeof this.stopAtEntry == 'string')
entryPoint = this.stopAtEntry;
break;
case RunCommand.RUN:
if (typeof this.stopAtEntry == 'boolean' && this.stopAtEntry) {
if (this.miDebugger.features.includes("exec-run-start-option"))
runToStart = true;
else
entryPoint = "main"; // sensible fallback
} else if (typeof this.stopAtEntry == 'string')
entryPoint = this.stopAtEntry;
break;
default:
throw new Error('Unhandled run command: ' + RunCommand[this.initialRunCommand]);
}
if (entryPoint)
promises.push(this.miDebugger.setEntryBreakPoint(entryPoint));
switch (this.initialRunCommand) {
case RunCommand.CONTINUE:
promises.push(this.miDebugger.continue().then(() => {
// Some debuggers will provide an out-of-band status that they are stopped
// when attaching (e.g., gdb), so the client assumes we are stopped and gets
// confused if we start running again on our own.
//
// If we don't send this event, the client may start requesting data (such as
// stack frames, local variables, etc.) since they believe the target is
// stopped. Furthermore the client may not be indicating the proper status
// to the user (may indicate stopped when the target is actually running).
this.sendEvent(new ContinuedEvent(1, true));
}));
break;
case RunCommand.RUN:
promises.push(this.miDebugger.start(runToStart).then(() => {
this.started = true;
if (this.crashed)
this.handlePause(undefined);
}));
break;
case RunCommand.NONE:
// Not all debuggers seem to provide an out-of-band status that they are stopped
// when attaching (e.g., lldb), so the client assumes we are running and gets
// confused when we don't actually run or continue. Therefore, we'll force a
// stopped event to be sent to the client (just in case) to synchronize the state.
const event: DebugProtocol.StoppedEvent = new StoppedEvent("pause", 1);
event.body.description = "paused on attach";
event.body.allThreadsStopped = true;
this.sendEvent(event);
break;
default:
throw new Error('Unhandled run command: ' + RunCommand[this.initialRunCommand]);
}
Promise.all(promises).then(() => {
this.sendResponse(response);
}).catch(err => {
this.sendErrorResponse(response, 18, `Could not run/continue: ${err.toString()}`);
});
}
protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void {
const scopes = new Array<Scope>();
scopes.push(new Scope("Local", STACK_HANDLES_START + (parseInt(args.frameId as any) || 0), false));
response.body = {
scopes: scopes
};
this.sendResponse(response);
}
protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): Promise<void> {
const variables: DebugProtocol.Variable[] = [];
let id: number | string | VariableObject | ExtendedVariable;
if (args.variablesReference < VAR_HANDLES_START) {
id = args.variablesReference - STACK_HANDLES_START;
} else {
id = this.variableHandles.get(args.variablesReference);
}
const createVariable = (arg, options?) => {
if (options)
return this.variableHandles.create(new ExtendedVariable(arg, options));
else
return this.variableHandles.create(arg);
};
const findOrCreateVariable = (varObj: VariableObject): number => {
let id: number;
if (this.variableHandlesReverse.hasOwnProperty(varObj.name)) {
id = this.variableHandlesReverse[varObj.name];
} else {
id = createVariable(varObj);
this.variableHandlesReverse[varObj.name] = id;
}
return varObj.isCompound() ? id : 0;
};
if (typeof id == "number") {
let stack: Variable[];
try {
const [threadId, level] = this.frameIdToThreadAndLevel(id);
stack = await this.miDebugger.getStackVariables(threadId, level);
for (const variable of stack) {
if (this.useVarObjects) {
try {
const varObjName = `var_${id}_${variable.name}`;
let varObj: VariableObject;
try {
const changes = await this.miDebugger.varUpdate(varObjName);
const changelist = changes.result("changelist");
changelist.forEach((change) => {
const name = MINode.valueOf(change, "name");
const vId = this.variableHandlesReverse[name];
const v = this.variableHandles.get(vId) as any;
v.applyChanges(change);
});
const varId = this.variableHandlesReverse[varObjName];
varObj = this.variableHandles.get(varId) as any;
} catch (err) {
if (err instanceof MIError && err.message == "Variable object not found") {
varObj = await this.miDebugger.varCreate(variable.name, varObjName);
const varId = findOrCreateVariable(varObj);
varObj.exp = variable.name;
varObj.id = varId;
} else {
throw err;
}
}
variables.push(varObj.toProtocolVariable());
} catch (err) {
variables.push({
name: variable.name,
value: `<${err}>`,
variablesReference: 0
});
}
} else {
if (variable.valueStr !== undefined) {
let expanded = expandValue(createVariable, `{${variable.name}=${variable.valueStr})`, "", variable.raw);
if (expanded) {
if (typeof expanded[0] == "string")
expanded = [
{
name: "<value>",
value: prettyStringArray(expanded),
variablesReference: 0
}
];
variables.push(expanded[0]);
}
} else
variables.push({
name: variable.name,
type: variable.type,
value: "<unknown>",
variablesReference: createVariable(variable.name)
});
}
}
response.body = {
variables: variables
};
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`);
}
} else if (typeof id == "string") {
// Variable members
let variable;
try {
// TODO: this evals on an (effectively) unknown thread for multithreaded programs.
variable = await this.miDebugger.evalExpression(JSON.stringify(id), 0, 0);
try {
let expanded = expandValue(createVariable, variable.result("value"), id, variable);
if (!expanded) {
this.sendErrorResponse(response, 2, `Could not expand variable`);
} else {
if (typeof expanded[0] == "string")
expanded = [
{
name: "<value>",
value: prettyStringArray(expanded),
variablesReference: 0
}
];
response.body = {
variables: expanded
};
this.sendResponse(response);
}
} catch (e) {
this.sendErrorResponse(response, 2, `Could not expand variable: ${e}`);
}
} catch (err) {
this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`);
}
} else if (typeof id == "object") {
if (id instanceof VariableObject) {
// Variable members
let children: VariableObject[];
try {
children = await this.miDebugger.varListChildren(id.name);
const vars = children.map(child => {
const varId = findOrCreateVariable(child);
child.id = varId;
return child.toProtocolVariable();
});
response.body = {
variables: vars
};
this.sendResponse(response);
} catch (err) {
this.sendErrorResponse(response, 1, `Could not expand variable: ${err}`);
}
} else if (id instanceof ExtendedVariable) {
const varReq = id;
if (varReq.options.arg) {
const strArr = [];
let argsPart = true;
let arrIndex = 0;
const submit = () => {
response.body = {
variables: strArr
};
this.sendResponse(response);
};
const addOne = async () => {
// TODO: this evals on an (effectively) unknown thread for multithreaded programs.
const variable = await this.miDebugger.evalExpression(JSON.stringify(`${varReq.name}+${arrIndex})`), 0, 0);
try {
const expanded = expandValue(createVariable, variable.result("value"), varReq.name, variable);
if (!expanded) {
this.sendErrorResponse(response, 15, `Could not expand variable`);
} else {
if (typeof expanded == "string") {
if (expanded == "<nullptr>") {
if (argsPart)
argsPart = false;
else
return submit();
} else if (expanded[0] != '"') {
strArr.push({
name: "[err]",
value: expanded,
variablesReference: 0
});
return submit();
}
strArr.push({
name: `[${(arrIndex++)}]`,
value: expanded,
variablesReference: 0
});
addOne();
} else {
strArr.push({
name: "[err]",
value: expanded,
variablesReference: 0
});
submit();
}
}
} catch (e) {
this.sendErrorResponse(response, 14, `Could not expand variable: ${e}`);
}
};
addOne();
} else
this.sendErrorResponse(response, 13, `Unimplemented variable request options: ${JSON.stringify(varReq.options)}`);
} else {
response.body = {
variables: id
};
this.sendResponse(response);
}
} else {
response.body = {
variables: variables
};
this.sendResponse(response);
}
}
protected pauseRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void {
this.miDebugger.interrupt().then(done => {
this.sendResponse(response);
}, msg => {
this.sendErrorResponse(response, 3, `Could not pause: ${msg}`);
});
}
protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments): void {
this.miDebugger.continue(true).then(done => {
this.sendResponse(response);
}, msg => {
this.sendErrorResponse(response, 2, `Could not continue: ${msg}`);
});
}
protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void {
this.miDebugger.continue().then(done => {
this.sendResponse(response);
}, msg => {
this.sendErrorResponse(response, 2, `Could not continue: ${msg}`);
});
}
protected stepBackRequest(response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void {
this.miDebugger.step(true).then(done => {
this.sendResponse(response);
}, msg => {
this.sendErrorResponse(response, 4, `Could not step back: ${msg} - Try running 'target record-full' before stepping back`);
});
}
protected stepInRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void {
this.miDebugger.step().then(done => {
this.sendResponse(response);
}, msg => {
this.sendErrorResponse(response, 4, `Could not step in: ${msg}`);
});
}
protected stepOutRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void {
this.miDebugger.stepOut().then(done => {
this.sendResponse(response);
}, msg => {
this.sendErrorResponse(response, 5, `Could not step out: ${msg}`);
});
}
protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void {
this.miDebugger.next().then(done => {
this.sendResponse(response);
}, msg => {
this.sendErrorResponse(response, 6, `Could not step over: ${msg}`);
});
}
protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void {
const [threadId, level] = this.frameIdToThreadAndLevel(args.frameId);
if (args.context == "watch" || args.context == "hover") {
this.miDebugger.evalExpression(args.expression, threadId, level).then((res) => {
response.body = {
variablesReference: 0,
result: res.result("value")
};
this.sendResponse(response);
}, msg => {
this.sendErrorResponse(response, 7, msg.toString());
});
} else {
this.miDebugger.sendUserInput(args.expression, threadId, level).then(output => {
if (typeof output == "undefined")
response.body = {
result: "",
variablesReference: 0
};
else
response.body = {
result: JSON.stringify(output),
variablesReference: 0
};
this.sendResponse(response);
}, msg => {
this.sendErrorResponse(response, 8, msg.toString());
});
}
}
protected gotoTargetsRequest(response: DebugProtocol.GotoTargetsResponse, args: DebugProtocol.GotoTargetsArguments): void {
this.miDebugger.goto(args.source.path, args.line).then(done => {
response.body = {
targets: [{
id: 1,
label: args.source.name,
column: args.column,
line : args.line
}]
};
this.sendResponse(response);
}, msg => {
this.sendErrorResponse(response, 16, `Could not jump: ${msg}`);
});
}
protected gotoRequest(response: DebugProtocol.GotoResponse, args: DebugProtocol.GotoArguments): void {
this.sendResponse(response);
}
private addSourceFileMapEntry(gdbCWD :string, ideCWD : string): void {
// if it looks like a Win32 path convert to "/"-style for comparisions and to all-lower-case
if (ideCWD.indexOf("\\") != -1)
ideCWD = ideCWD.replace(/\\/g, "/").toLowerCase();
if (!ideCWD.endsWith("/"))
ideCWD = ideCWD + "/"
// ensure that we only replace complete paths
if (gdbCWD.indexOf("\\") != -1)
gdbCWD = gdbCWD.replace(/\\/g, "/").toLowerCase();
if (!gdbCWD.endsWith("/"))
gdbCWD = gdbCWD + "/"
this.sourceFileMap.set(ideCWD, gdbCWD);
}
protected setSourceFileMap(configMap: { [index: string]: string }, fallbackGDB :string, fallbackIDE : string): void {
this.sourceFileMap = new Map<string, string>();
if (configMap === undefined) {
this.addSourceFileMapEntry(fallbackGDB, fallbackIDE);
} else {
for (let [gdbPath, localPath] of Object.entries(configMap)) {
this.addSourceFileMapEntry(gdbPath, localPath);
}
}
}
}
function prettyStringArray(strings) {
if (typeof strings == "object") {
if (strings.length !== undefined)
return strings.join(", ");
else
return JSON.stringify(strings);
} else return strings;
}
async function cleanInvalidSocketPath(socketlists:string){
return new Promise((resolve, reject) => {
fs.readdir(socketlists, (err, files) => {
if (!err) {
if (files.length == 0) resolve('');
files.forEach((file)=>{
try {
const conn = net.connect(systemPath.join(socketlists, file));
conn.setTimeout(200);
conn.on('error',()=>{
fs.unlink(systemPath.join(socketlists, file), (err) => {
if (err)
console.error("Failed to unlink invalid debug server");
resolve('');
});
});
}
catch{
fs.unlink(systemPath.join(socketlists, file), (err) => {
if (err)
console.error("Failed to unlink invalid debug server");
resolve('');
});
}
})
}
resolve('');
});
});
}

24
src/test/runTest.ts Normal file
View File

@ -0,0 +1,24 @@
import * as path from 'path';
import { runTests } from 'vscode-test';
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// The path to the extension test runner script
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite/index');
// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error(err);
console.error('Failed to run tests');
process.exit(1);
}
}
main();

View File

@ -0,0 +1,306 @@
import * as assert from 'assert';
import { expandValue, isExpandable } from '../../backend/gdb_expansion';
suite("GDB Value Expansion", () => {
const variableCreate = (variable) => { return { expanded: variable }; };
test("Various values", () => {
assert.strictEqual(isExpandable(`false`), 0);
assert.equal(expandValue(variableCreate, `false`), "false");
assert.strictEqual(isExpandable(`5`), 0);
assert.equal(expandValue(variableCreate, `5`), "5");
assert.strictEqual(isExpandable(`"hello world!"`), 0);
assert.equal(expandValue(variableCreate, `"hello world!"`), `"hello world!"`);
assert.strictEqual(isExpandable(`0x7fffffffe956 "foobar"`), 0);
assert.equal(expandValue(variableCreate, `0x7fffffffe956 "foobar"`), `"foobar"`);
assert.strictEqual(isExpandable(`0x0`), 0);
assert.equal(expandValue(variableCreate, `0x0`), "<nullptr>");
assert.strictEqual(isExpandable(`0x000000`), 0);
assert.equal(expandValue(variableCreate, `0x000000`), "<nullptr>");
assert.strictEqual(isExpandable(`{...}`), 2);
assert.equal(expandValue(variableCreate, `{...}`), "<...>");
assert.strictEqual(isExpandable(`0x00abc`), 2);
assert.equal(expandValue(variableCreate, `0x007ffff7ecb480`), "*0x007ffff7ecb480");
assert.strictEqual(isExpandable(`{a = b, c = d}`), 1);
assert.deepEqual(expandValue(variableCreate, `{a = b, c = d}`), [
{
name: "a",
value: "b",
variablesReference: 0
}, {
name: "c",
value: "d",
variablesReference: 0
}]);
assert.strictEqual(isExpandable(`{[0] = 0x400730 "foo", [1] = 0x400735 "bar"}`), 1);
assert.deepEqual(expandValue(variableCreate, `{[0] = 0x400730 "foo", [1] = 0x400735 "bar"}`), [
{
name: "[0]",
value: "\"foo\"",
variablesReference: 0
}, {
name: "[1]",
value: "\"bar\"",
variablesReference: 0
}]);
assert.strictEqual(isExpandable(`{{a = b}}`), 1);
assert.deepEqual(expandValue(variableCreate, `{{a = b}}`), [
{
name: "[0]",
value: "Object",
variablesReference: {
expanded: [
{
name: "a",
value: "b",
variablesReference: 0
}
]
}
}
]);
assert.deepEqual(expandValue(variableCreate, `{1, 2, 3, 4}`), [
{
name: "[0]",
value: "1",
variablesReference: 0
}, {
name: "[1]",
value: "2",
variablesReference: 0
}, {
name: "[2]",
value: "3",
variablesReference: 0
}, {
name: "[3]",
value: "4",
variablesReference: 0
}]);
});
test("Error values", () => {
assert.strictEqual(isExpandable(`<No data fields>`), 0);
assert.equal(expandValue(variableCreate, `<No data fields>`), "<No data fields>");
});
test("Nested values", () => {
assert.strictEqual(isExpandable(`{a = {b = e}, c = d}`), 1);
assert.deepEqual(expandValue(variableCreate, `{a = {b = e}, c = d}`), [
{
name: "a",
value: "Object",
variablesReference: {
expanded: [
{
name: "b",
value: "e",
variablesReference: 0
}
]
}
}, {
name: "c",
value: "d",
variablesReference: 0
}]);
});
test("Simple node", () => {
assert.strictEqual(isExpandable(`{a = false, b = 5, c = 0x0, d = "foobar"}`), 1);
const variables = expandValue(variableCreate, `{a = false, b = 5, c = 0x0, d = "foobar"}`);
assert.equal(variables.length, 4);
assert.equal(variables[0].name, "a");
assert.equal(variables[0].value, "false");
assert.equal(variables[1].name, "b");
assert.equal(variables[1].value, "5");
assert.equal(variables[2].name, "c");
assert.equal(variables[2].value, "<nullptr>");
assert.equal(variables[3].name, "d");
assert.equal(variables[3].value, `"foobar"`);
});
test("Complex node", () => {
const node = `{quit = false, _views = {{view = 0x7ffff7ece1e8, renderer = 0x7ffff7eccc50, world = 0x7ffff7ece480}}, deltaTimer = {_flagStarted = false, _timeStart = {length = 0}, _timeMeasured = {length = 0}}, _start = {callbacks = 0x0}, _stop = {callbacks = 0x0}}`;
assert.strictEqual(isExpandable(node), 1);
const variables = expandValue(variableCreate, node);
assert.deepEqual(variables, [
{
name: "quit",
value: "false",
variablesReference: 0
},
{
name: "_views",
value: "Object",
variablesReference: {
expanded: [
{
name: "[0]",
value: "Object",
variablesReference: {
expanded: [
{
name: "view",
value: "Object@*0x7ffff7ece1e8",
variablesReference: { expanded: "*_views[0].view" }
},
{
name: "renderer",
value: "Object@*0x7ffff7eccc50",
variablesReference: { expanded: "*_views[0].renderer" }
},
{
name: "world",
value: "Object@*0x7ffff7ece480",
variablesReference: { expanded: "*_views[0].world" }
}
]
}
}
]
}
},
{
name: "deltaTimer",
value: "Object",
variablesReference: {
expanded: [
{
name: "_flagStarted",
value: "false",
variablesReference: 0
},
{
name: "_timeStart",
value: "Object",
variablesReference: {
expanded: [
{
name: "length",
value: "0",
variablesReference: 0
}
]
}
},
{
name: "_timeMeasured",
value: "Object",
variablesReference: {
expanded: [
{
name: "length",
value: "0",
variablesReference: 0
}
]
}
}
]
}
},
{
name: "_start",
value: "Object",
variablesReference: {
expanded: [
{
name: "callbacks",
value: "<nullptr>",
variablesReference: 0
}
]
}
},
{
name: "_stop",
value: "Object",
variablesReference: {
expanded: [
{
name: "callbacks",
value: "<nullptr>",
variablesReference: 0
}
]
}
}
]);
});
test("Simple node with errors", () => {
const node = `{_enableMipMaps = false, _minFilter = <incomplete type>, _magFilter = <incomplete type>, _wrapX = <incomplete type>, _wrapY = <incomplete type>, _inMode = 6408, _mode = 6408, _id = 1, _width = 1024, _height = 1024}`;
assert.strictEqual(isExpandable(node), 1);
const variables = expandValue(variableCreate, node);
assert.deepEqual(variables, [
{
name: "_enableMipMaps",
value: "false",
variablesReference: 0
},
{
name: "_minFilter",
value: "<incomplete type>",
variablesReference: 0
},
{
name: "_magFilter",
value: "<incomplete type>",
variablesReference: 0
},
{
name: "_wrapX",
value: "<incomplete type>",
variablesReference: 0
},
{
name: "_wrapY",
value: "<incomplete type>",
variablesReference: 0
},
{
name: "_inMode",
value: "6408",
variablesReference: 0
},
{
name: "_mode",
value: "6408",
variablesReference: 0
},
{
name: "_id",
value: "1",
variablesReference: 0
},
{
name: "_width",
value: "1024",
variablesReference: 0
},
{
name: "_height",
value: "1024",
variablesReference: 0
}
]);
});
test("lldb strings", () => {
const node = `{ name = {...} }`;
assert.strictEqual(isExpandable(node), 1);
const variables = expandValue(variableCreate, node);
assert.deepEqual(variables, [
{
name: "name",
value: "...",
variablesReference: { expanded: "name" }
}
]);
});
test("float values", () => {
const node = `{ intval1 = 123, floatval1 = 123.456, intval2 = 3, floatval2 = 234.45 }`;
const variables = expandValue(variableCreate, node);
assert.deepEqual(variables, [
{ name: "intval1", value: "123", variablesReference: 0 },
{ name: "floatval1", value: "123.456", variablesReference: 0 },
{ name: "intval2", value: "3", variablesReference: 0 },
{ name: "floatval2", value: "234.45", variablesReference: 0 }
]);
});
});

37
src/test/suite/index.ts Normal file
View File

@ -0,0 +1,37 @@
import * as path from 'path';
import * as Mocha from 'mocha';
import * as glob from 'glob';
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
useColors: true
});
const testsRoot = path.resolve(__dirname, '..');
return new Promise((c, e) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
e(err);
}
});
});
}

View File

@ -0,0 +1,207 @@
import * as assert from 'assert';
import { parseMI, MINode } from '../../backend/mi_parse';
suite("MI Parse", () => {
test("Very simple out of band record", () => {
const parsed = parseMI(`*stopped`);
assert.ok(parsed);
assert.strictEqual(parsed.token, undefined);
assert.strictEqual(parsed.outOfBandRecord.length, 1);
assert.strictEqual(parsed.outOfBandRecord[0].isStream, false);
assert.strictEqual(parsed.outOfBandRecord[0].asyncClass, "stopped");
assert.strictEqual(parsed.outOfBandRecord[0].output.length, 0);
assert.strictEqual(parsed.resultRecords, undefined);
});
test("Simple out of band record", () => {
const parsed = parseMI(`4=thread-exited,id="3",group-id="i1"`);
assert.ok(parsed);
assert.equal(parsed.token, 4);
assert.equal(parsed.outOfBandRecord.length, 1);
assert.equal(parsed.outOfBandRecord[0].isStream, false);
assert.equal(parsed.outOfBandRecord[0].asyncClass, "thread-exited");
assert.equal(parsed.outOfBandRecord[0].output.length, 2);
assert.deepEqual(parsed.outOfBandRecord[0].output[0], ["id", "3"]);
assert.deepEqual(parsed.outOfBandRecord[0].output[1], ["group-id", "i1"]);
assert.equal(parsed.resultRecords, undefined);
});
test("Console stream output with new line", () => {
const parsed = parseMI(`~"[Thread 0x7fffe993a700 (LWP 11002) exited]\\n"`);
assert.ok(parsed);
assert.equal(parsed.token, undefined);
assert.equal(parsed.outOfBandRecord.length, 1);
assert.equal(parsed.outOfBandRecord[0].isStream, true);
assert.equal(parsed.outOfBandRecord[0].content, "[Thread 0x7fffe993a700 (LWP 11002) exited]\n");
assert.equal(parsed.resultRecords, undefined);
});
test("Unicode", () => {
let parsed = parseMI(`~"[Depuraci\\303\\263n de hilo usando libthread_db enabled]\\n"`);
assert.ok(parsed);
assert.equal(parsed.token, undefined);
assert.equal(parsed.outOfBandRecord.length, 1);
assert.equal(parsed.outOfBandRecord[0].isStream, true);
assert.equal(parsed.outOfBandRecord[0].content, "[Depuración de hilo usando libthread_db enabled]\n");
assert.equal(parsed.resultRecords, undefined);
parsed = parseMI(`~"4\\t std::cout << \\"\\345\\245\\275\\345\\245\\275\\345\\255\\246\\344\\271\\240\\357\\274\\214\\345\\244\\251\\345\\244\\251\\345\\220\\221\\344\\270\\212\\" << std::endl;\\n"`);
assert.ok(parsed);
assert.equal(parsed.token, undefined);
assert.equal(parsed.outOfBandRecord.length, 1);
assert.equal(parsed.outOfBandRecord[0].isStream, true);
assert.equal(parsed.outOfBandRecord[0].content, `4\t std::cout << "好好学习,天天向上" << std::endl;\n`);
assert.equal(parsed.resultRecords, undefined);
});
test("Empty line", () => {
const parsed = parseMI(``);
assert.ok(parsed);
assert.equal(parsed.token, undefined);
assert.equal(parsed.outOfBandRecord.length, 0);
assert.equal(parsed.resultRecords, undefined);
});
test("'(gdb)' line", () => {
const parsed = parseMI(`(gdb)`);
assert.ok(parsed);
assert.equal(parsed.token, undefined);
assert.equal(parsed.outOfBandRecord.length, 0);
assert.equal(parsed.resultRecords, undefined);
});
test("Simple result record", () => {
const parsed = parseMI(`1^running`);
assert.ok(parsed);
assert.equal(parsed.token, 1);
assert.equal(parsed.outOfBandRecord.length, 0);
assert.notEqual(parsed.resultRecords, undefined);
assert.equal(parsed.resultRecords.resultClass, "running");
assert.equal(parsed.resultRecords.results.length, 0);
});
test("Advanced out of band record (Breakpoint hit)", () => {
const parsed = parseMI(`*stopped,reason="breakpoint-hit",disp="keep",bkptno="1",frame={addr="0x00000000004e807f",func="D main",args=[{name="args",value="..."}],file="source/app.d",fullname="/path/to/source/app.d",line="157"},thread-id="1",stopped-threads="all",core="0"`);
assert.ok(parsed);
assert.equal(parsed.token, undefined);
assert.equal(parsed.outOfBandRecord.length, 1);
assert.equal(parsed.outOfBandRecord[0].isStream, false);
assert.equal(parsed.outOfBandRecord[0].asyncClass, "stopped");
assert.equal(parsed.outOfBandRecord[0].output.length, 7);
assert.deepEqual(parsed.outOfBandRecord[0].output[0], ["reason", "breakpoint-hit"]);
assert.deepEqual(parsed.outOfBandRecord[0].output[1], ["disp", "keep"]);
assert.deepEqual(parsed.outOfBandRecord[0].output[2], ["bkptno", "1"]);
const frame = [
["addr", "0x00000000004e807f"],
["func", "D main"],
["args", [[["name", "args"], ["value", "..."]]]],
["file", "source/app.d"],
["fullname", "/path/to/source/app.d"],
["line", "157"]
];
assert.deepEqual(parsed.outOfBandRecord[0].output[3], ["frame", frame]);
assert.deepEqual(parsed.outOfBandRecord[0].output[4], ["thread-id", "1"]);
assert.deepEqual(parsed.outOfBandRecord[0].output[5], ["stopped-threads", "all"]);
assert.deepEqual(parsed.outOfBandRecord[0].output[6], ["core", "0"]);
assert.equal(parsed.resultRecords, undefined);
});
test("Advanced result record", () => {
const parsed = parseMI(`2^done,asm_insns=[src_and_asm_line={line="134",file="source/app.d",fullname="/path/to/source/app.d",line_asm_insn=[{address="0x00000000004e7da4",func-name="_Dmain",offset="0",inst="push %rbp"},{address="0x00000000004e7da5",func-name="_Dmain",offset="1",inst="mov %rsp,%rbp"}]}]`);
assert.ok(parsed);
assert.equal(parsed.token, 2);
assert.equal(parsed.outOfBandRecord.length, 0);
assert.notEqual(parsed.resultRecords, undefined);
assert.equal(parsed.resultRecords.resultClass, "done");
assert.equal(parsed.resultRecords.results.length, 1);
const asmInsns = [
"asm_insns",
[
[
"src_and_asm_line",
[
["line", "134"],
["file", "source/app.d"],
["fullname", "/path/to/source/app.d"],
[
"line_asm_insn",
[
[
["address", "0x00000000004e7da4"],
["func-name", "_Dmain"],
["offset", "0"],
["inst", "push %rbp"]
],
[
["address", "0x00000000004e7da5"],
["func-name", "_Dmain"],
["offset", "1"],
["inst", "mov %rsp,%rbp"]
]
]
]
]
]
]
];
assert.deepEqual(parsed.resultRecords.results[0], asmInsns);
assert.equal(parsed.result("asm_insns.src_and_asm_line.line_asm_insn[1].address"), "0x00000000004e7da5");
});
test("valueof children", () => {
const obj = [
[
"frame",
[
["level", "0"],
["addr", "0x0000000000435f70"],
["func", "D main"],
["file", "source/app.d"],
["fullname", "/path/to/source/app.d"],
["line", "5"]
]
],
[
"frame",
[
["level", "1"],
["addr", "0x00000000004372d3"],
["func", "rt.dmain2._d_run_main()"]
]
],
[
"frame",
[
["level", "2"],
["addr", "0x0000000000437229"],
["func", "rt.dmain2._d_run_main()"]
]
]
];
assert.equal(MINode.valueOf(obj[0], "@frame.level"), "0");
assert.equal(MINode.valueOf(obj[0], "@frame.addr"), "0x0000000000435f70");
assert.equal(MINode.valueOf(obj[0], "@frame.func"), "D main");
assert.equal(MINode.valueOf(obj[0], "@frame.file"), "source/app.d");
assert.equal(MINode.valueOf(obj[0], "@frame.fullname"), "/path/to/source/app.d");
assert.equal(MINode.valueOf(obj[0], "@frame.line"), "5");
assert.equal(MINode.valueOf(obj[1], "@frame.level"), "1");
assert.equal(MINode.valueOf(obj[1], "@frame.addr"), "0x00000000004372d3");
assert.equal(MINode.valueOf(obj[1], "@frame.func"), "rt.dmain2._d_run_main()");
assert.equal(MINode.valueOf(obj[1], "@frame.file"), undefined);
assert.equal(MINode.valueOf(obj[1], "@frame.fullname"), undefined);
assert.equal(MINode.valueOf(obj[1], "@frame.line"), undefined);
});
test("empty string values", () => {
const parsed = parseMI(`15^done,register-names=["r0","pc","","xpsr","","control"]`);
const result = parsed.result('register-names');
assert.deepEqual(result, ["r0", "pc", "", "xpsr", "", "control"]);
});
test("empty string value first and last", () => {
const parsed = parseMI(`15^done,register-names=["","r0","pc","","xpsr","","control",""]`);
const result = parsed.result('register-names');
assert.deepEqual(result, ["","r0","pc","","xpsr","","control", ""]);
});
test("empty array values", () => {
const parsed = parseMI(`15^done,foo={x=[],y="y"}`);
assert.deepEqual(parsed.result('foo.x'), []);
assert.equal(parsed.result('foo.y'), "y");
});
test("empty object values", () => {
// GDB may send {} as empty array
const parsed = parseMI(`15^done,foo={x={},y="y"}`);
assert.deepEqual(parsed.result('foo.x'), []);
assert.equal(parsed.result('foo.y'), "y");
});
});

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "out",
"lib": [
"es6"
],
"sourceMap": true,
"rootDir": ".",
"plugins": [
{
"name": "tslint-language-service"
}
]
},
"exclude": [
"node_modules",
".vscode-test"
]
}

38
tslint.json Normal file
View File

@ -0,0 +1,38 @@
{
"defaultSeverity": "error",
"extends": "tslint:recommended",
"rules": {
"no-null-keyword": true,
"indent": [true, "tabs"],
/* Rules in tslint:recommended that we don't follow yet. */
"array-type": false,
"arrow-parens": false,
"arrow-return-shorthand": false,
"ban-types": false,
"class-name": false,
"comment-format": false,
"curly": false,
"interface-name": false,
"max-classes-per-file": false,
"max-line-length": false,
"member-access": false,
"member-ordering": false,
"no-angle-bracket-type-assertion": false,
"no-bitwise": false,
"no-conditional-assignment": false,
"no-consecutive-blank-lines": false,
"no-empty": false,
"no-shadowed-variable": false,
"no-unnecessary-initializer": false,
"object-literal-shorthand": false,
"object-literal-sort-keys": false,
"one-variable-per-declaration": false,
"only-arrow-functions": false,
"ordered-imports": false,
"quotemark": false,
"radix": false,
"space-before-function-paren": false,
"trailing-comma": false,
"triple-equals": false
}
}