├── src ├── integration-tests │ ├── test-programs │ │ ├── empty.c │ │ ├── disassemble.c │ │ ├── evaluate.cpp │ │ ├── empty space.c │ │ ├── segv.c │ │ ├── count.c │ │ ├── .gitignore │ │ ├── count_other.c │ │ ├── count space.c │ │ ├── functions_other.c │ │ ├── functions.c │ │ ├── mem.c │ │ ├── vars.c │ │ ├── vars_cpp.cpp │ │ └── Makefile │ ├── stop.spec.ts │ ├── GDBBackend.spec.ts │ ├── launchRemote.spec.ts │ ├── attachRemote.spec.ts │ ├── README.md │ ├── evaluate.spec.ts │ ├── logpoints.spec.ts │ ├── launch.spec.ts │ ├── diassemble.spec.ts │ ├── mem.spec.ts │ ├── vars_cpp.spec.ts │ ├── functionBreakpoints.spec.ts │ ├── utils.ts │ ├── debugClient.ts │ ├── var.spec.ts │ └── breakpoints.spec.ts ├── index.ts ├── mi │ ├── index.ts │ ├── target.ts │ ├── base.ts │ ├── thread.ts │ ├── exec.ts │ ├── stack.ts │ ├── data.ts │ ├── breakpoint.ts │ └── var.ts ├── debugAdapter.ts ├── debugTargetAdapter.ts ├── stoppedEvent.ts ├── native │ ├── scoped_fd.h │ ├── pty.ts │ ├── pty.cc │ └── pty.spec.ts ├── varManager.ts ├── GDBBackend.ts ├── MIParser.ts └── GDBTargetDebugSession.ts ├── tsfmt.json ├── .gitignore ├── .vscode ├── settings.json ├── extensions.json ├── tasks.json └── launch.json ├── .npmignore ├── .editorconfig ├── install.js ├── tsconfig.json ├── tslint.json ├── README.md ├── binding.gyp ├── CONTRIBUTING.md ├── package.json ├── NOTICE └── LICENSE /src/integration-tests/test-programs/empty.c: -------------------------------------------------------------------------------- 1 | int main() 2 | { 3 | return 0; 4 | } 5 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/disassemble.c: -------------------------------------------------------------------------------- 1 | int main() { 2 | return 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/evaluate.cpp: -------------------------------------------------------------------------------- 1 | int main() { 2 | return 0; 3 | } 4 | -------------------------------------------------------------------------------- /tsfmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "insertSpaceAfterFunctionKeywordForAnonymousFunctions": false 3 | } 4 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/empty space.c: -------------------------------------------------------------------------------- 1 | int main() 2 | { 3 | return 0; 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | compile_commands.json 2 | node_modules 3 | /.vscode/ipch/ 4 | /build/ 5 | /dist/ 6 | /test-reports/ 7 | *.tgz 8 | yarn-error.log 9 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/segv.c: -------------------------------------------------------------------------------- 1 | int foo(int *addr) { 2 | return *addr; 3 | } 4 | 5 | int main() { 6 | foo(0); 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, 3 | "eslint.packageManager": "yarn" 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .editorconfig 3 | .vscode 4 | *.tgz 5 | test-reports 6 | tsconfig.json 7 | tsfmt.json 8 | tslint.json 9 | dist/integration-tests 10 | *.map 11 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/count.c: -------------------------------------------------------------------------------- 1 | int main() { 2 | int count = 0; 3 | while (count < 1000) { 4 | count ++; 5 | } 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/.gitignore: -------------------------------------------------------------------------------- 1 | count 2 | functions 3 | disassemble 4 | empty 5 | empty space 6 | evaluate 7 | mem 8 | vars 9 | segv 10 | *.o 11 | *.exe 12 | .vscode 13 | /vars_cpp 14 | /log 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | end_of_line = lf 6 | indent_style = space 7 | 8 | [*.{js,ts,md,c,cpp,h}] 9 | indent_size = 4 10 | 11 | [*.{json,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/count_other.c: -------------------------------------------------------------------------------- 1 | static int staticfunc1(void) { 2 | return 2; 3 | } 4 | static int staticfunc2(void) { 5 | return 2; 6 | } 7 | 8 | int other(void) { 9 | staticfunc1(); 10 | staticfunc2(); 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/count space.c: -------------------------------------------------------------------------------- 1 | static int staticfunc1(void) { 2 | return 2; 3 | } 4 | static int staticfunc2(void) { 5 | return 2; 6 | } 7 | 8 | int other_space(void) { 9 | staticfunc1(); 10 | staticfunc2(); 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/functions_other.c: -------------------------------------------------------------------------------- 1 | static int staticfunc1(void) { 2 | return 2; 3 | } 4 | static int staticfunc2(void) { 5 | return 2; 6 | } 7 | 8 | int other(void) { 9 | staticfunc1(); 10 | staticfunc2(); 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const { spawnSync } = require('child_process'); 3 | 4 | if (os.platform() === 'linux') { 5 | const {status} = spawnSync('npm', ['run', 'nativebuild'], { 6 | stdio: 'inherit' 7 | }); 8 | process.exitCode = status; 9 | } 10 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/functions.c: -------------------------------------------------------------------------------- 1 | extern int other(void); 2 | static int staticfunc1(void) { 3 | return 2; 4 | } 5 | static int staticfunc2(void) { 6 | return 2; 7 | } 8 | 9 | int sub(void) { 10 | return 0; 11 | } 12 | 13 | int main(void) { 14 | staticfunc1(); 15 | staticfunc2(); 16 | sub(); 17 | other(); 18 | return 0; 19 | } 20 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/mem.c: -------------------------------------------------------------------------------- 1 | int main() { 2 | static const unsigned char array[] = { 3 | 0xf1, 0xef, 0xd4, 0xfd, 4 | 0x72, 0x48, 0x45, 0x0c, 5 | 0x2d, 0x13, 0x74, 0xd6, 6 | 0xf6, 0x12, 0xdc, 0xe1, 7 | 0x89, 0x03, 0x9e, 0x42, 8 | }; 9 | 10 | static const unsigned char *parray = array; 11 | 12 | return 0; 13 | } 14 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/vars.c: -------------------------------------------------------------------------------- 1 | 2 | struct bar 3 | { 4 | int a; 5 | int b; 6 | }; 7 | 8 | struct foo 9 | { 10 | int x; 11 | int y; 12 | struct bar z; 13 | }; 14 | 15 | int main() 16 | { 17 | int a = 1; 18 | int b = 2; 19 | int c = a + b; // STOP HERE 20 | struct foo r = {1, 2, {3, 4}}; 21 | int d = r.x + r.y; 22 | int e = r.z.a + r.z.b; 23 | int f[] = {1, 2, 3}; 24 | int g = f[0] + f[1] + f[2]; 25 | return 0; 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "EditorConfig.EditorConfig" 8 | ], 9 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 10 | "unwantedRecommendations": [ 11 | 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | export * from './mi'; 12 | export * from './GDBBackend'; 13 | export * from './GDBDebugSession'; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "es2015", 6 | "lib": [ 7 | "es2015" 8 | ], 9 | "outDir": "dist", 10 | "sourceMap": true, 11 | "declaration": true, 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": true, 16 | "allowSyntheticDefaultImports": true, 17 | "experimentalDecorators": true, 18 | "plugins": [ 19 | { 20 | "name": "tslint-language-service" 21 | } 22 | ] 23 | }, 24 | "include": [ 25 | "src/**/*.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "rules": { 7 | "align": [ 8 | true, 9 | "statements", 10 | "members", 11 | "elements" 12 | ], 13 | "interface-name": false, 14 | "no-empty-interface": false, 15 | "object-literal-sort-keys": false, 16 | "ordered-imports": false, 17 | "quotemark": [ 18 | true, 19 | "single", 20 | "avoid-template", 21 | "avoid-escape" 22 | ], 23 | "space-before-function-paren": [ 24 | true, 25 | { 26 | "anonymous": "never" 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/mi/index.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 Ericsson and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | export * from './base'; 11 | export * from './breakpoint'; 12 | export * from './data'; 13 | export * from './exec'; 14 | export * from './stack'; 15 | export * from './target'; 16 | export * from './thread'; 17 | export * from './var'; 18 | -------------------------------------------------------------------------------- /src/debugAdapter.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import * as process from 'process'; 11 | import { logger } from 'vscode-debugadapter/lib/logger'; 12 | import { GDBDebugSession } from './GDBDebugSession'; 13 | 14 | process.on('uncaughtException', (err: any) => { 15 | logger.error(JSON.stringify(err)); 16 | }); 17 | 18 | GDBDebugSession.run(GDBDebugSession); 19 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Watch", 8 | "command": "yarn", 9 | "args": [ 10 | "watch" 11 | ], 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | }, 16 | "problemMatcher": [ 17 | "$tsc-watch" 18 | ], 19 | "isBackground": true 20 | }, 21 | { 22 | "label": "Tests", 23 | "command": "yarn", 24 | "args": [ 25 | "test:integration" 26 | ] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/debugTargetAdapter.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import * as process from 'process'; 11 | import { logger } from 'vscode-debugadapter/lib/logger'; 12 | import { GDBTargetDebugSession } from './GDBTargetDebugSession'; 13 | 14 | process.on('uncaughtException', (err: any) => { 15 | logger.error(JSON.stringify(err)); 16 | }); 17 | 18 | GDBTargetDebugSession.run(GDBTargetDebugSession); 19 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/vars_cpp.cpp: -------------------------------------------------------------------------------- 1 | //============================================================================ 2 | // Name : hello_cpp.cpp 3 | // Author : 4 | // Version : 5 | // Copyright : Your copyright notice 6 | // Description : Hello World in C++, Ansi-style 7 | //============================================================================ 8 | 9 | #include 10 | using namespace std; 11 | 12 | class Foo 13 | { 14 | public: 15 | int a; 16 | Foo(int, int, char); 17 | 18 | protected: 19 | int b; 20 | 21 | private: 22 | char c; 23 | }; 24 | 25 | Foo::Foo(int a, int b, char c) 26 | { 27 | this->a = a; 28 | this->b = b; 29 | this->c = c; 30 | } 31 | 32 | int main() 33 | { 34 | Foo *fooA = new Foo(1, 2, 'a'); 35 | Foo *fooB = new Foo(3, 4, 'b'); 36 | Foo *fooarr[] = {fooA, fooB}; 37 | cout << "!!!Hello World!!!" << endl; // STOP HERE 38 | cout << "!!!Hello World Again!!!" << endl; 39 | return 0; 40 | } 41 | -------------------------------------------------------------------------------- /src/mi/target.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import { GDBBackend } from '../GDBBackend'; 11 | import { MIResponse } from './base'; 12 | 13 | export function sendTargetAttachRequest(gdb: GDBBackend, params: { 14 | pid: string; 15 | }): Promise { 16 | return gdb.sendCommand(`-target-attach ${params.pid}`); 17 | } 18 | 19 | export function sendTargetSelectRequest(gdb: GDBBackend, params: { 20 | type: string; 21 | parameters: string[]; 22 | }): Promise { 23 | return gdb.sendCommand(`-target-select ${params.type} ${params.parameters.join(' ')}`); 24 | } 25 | -------------------------------------------------------------------------------- /src/integration-tests/test-programs/Makefile: -------------------------------------------------------------------------------- 1 | BINS = empty empty\ space evaluate vars vars_cpp mem segv count disassemble functions 2 | 3 | .PHONY: all 4 | all: $(BINS) 5 | 6 | CC = gcc 7 | CXX = g++ 8 | LINK = $(CC) -o $@ $^ 9 | LINK_CXX = $(CXX) -o $@ $^ 10 | 11 | functions: functions.o functions_other.o 12 | $(LINK) 13 | 14 | count: count.o count_other.o count\ space.o 15 | $(CC) -o "count" count.o count_other.o "count space.o" 16 | 17 | count\ space.o: count\ space.c 18 | $(CC) -c "count space.c" -g3 -O0 19 | 20 | empty: empty.o 21 | $(LINK) 22 | 23 | evaluate: evaluate.o 24 | $(LINK) 25 | 26 | mem: mem.o 27 | $(LINK) 28 | 29 | disassemble: disassemble.o 30 | $(LINK) 31 | 32 | vars: vars.o 33 | $(LINK) 34 | 35 | vars_cpp: vars_cpp.o 36 | $(LINK_CXX) 37 | 38 | segv: segv.o 39 | $(LINK) 40 | 41 | %.o: %.c 42 | $(CC) -c $< -g3 -O0 43 | 44 | %.o: %.cpp 45 | $(CXX) -c $< -g3 -O0 46 | 47 | empty\ space: empty\ space.o 48 | $(CC) -o "empty space" "empty space.o" 49 | 50 | empty\ space.o: empty\ space.c 51 | $(CC) -c "empty space.c" -g3 -O0 52 | 53 | .PHONY: clean 54 | clean: 55 | rm -f $(BINS) *.o 56 | -------------------------------------------------------------------------------- /src/stoppedEvent.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2019 Arm Ltd. and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import { Event } from 'vscode-debugadapter'; 11 | import { DebugProtocol } from 'vscode-debugprotocol'; 12 | 13 | export class StoppedEvent extends Event implements DebugProtocol.StoppedEvent { 14 | public body: { 15 | reason: string; 16 | threadId?: number; 17 | allThreadsStopped?: boolean; 18 | }; 19 | 20 | constructor(reason: string, threadId: number, allThreadsStopped: boolean = false) { 21 | super('stopped'); 22 | 23 | this.body = { 24 | reason, 25 | allThreadsStopped, 26 | }; 27 | 28 | if (typeof threadId === 'number') { 29 | this.body.threadId = threadId; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/native/scoped_fd.h: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 Ericsson and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | #include 11 | 12 | /** 13 | * Own a file descriptor, close it on scope exit. 14 | */ 15 | class scoped_fd { 16 | public: 17 | explicit scoped_fd(int fd) : m_fd(fd) {} 18 | 19 | scoped_fd(const scoped_fd &other) = delete; 20 | scoped_fd &operator=(const scoped_fd &other) = delete; 21 | 22 | bool operator==(int other) { return m_fd == other; } 23 | 24 | ~scoped_fd() { close(); } 25 | 26 | void close() { 27 | if (m_fd >= 0) { 28 | ::close(m_fd); 29 | m_fd = -1; 30 | } 31 | } 32 | 33 | int release() { 34 | int fd(m_fd); 35 | m_fd = -1; 36 | return fd; 37 | } 38 | 39 | int get() const { return m_fd; } 40 | 41 | private: 42 | int m_fd; 43 | }; 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CDT GDB Debug Adapter 2 | 3 | This is an implementation of the Debug Adapter Protocol for gdb. 4 | It is loosely based on the Eclipse CDT MI layer. 5 | We are at least learning from it. 6 | 7 | The source code can be found in the following repository: https://github.com/eclipse-cdt/cdt-gdb-adapter 8 | 9 | ## Building 10 | 11 | Build is pretty simple. 12 | 13 | ```sh 14 | yarn 15 | ``` 16 | 17 | The entry point for the adapter is `out/debugAdapter.js` for local debugging 18 | and `out/debugTargetAdapter.js` for target (remote) debugging.g 19 | 20 | ## Testing 21 | 22 | Testing of the adapter can be run with `yarn test`. 23 | 24 | ## Debugging 25 | 26 | To debug the adapter there are multiple options depending on how this module is integrated. For example, 27 | if being used as a VS Code extension, see https://github.com/eclipse-cdt/cdt-gdb-vscode#building. 28 | 29 | However, if you are writing tests and developing this module independently you can use the launch 30 | configurations in the launch.json with VS Code. For example, if you open a *.spec.ts file in VS Code 31 | you can use the "Mocha Current File & launch Server" configuration to automatically launch the debug 32 | server in one debugged process and the test in another. 33 | -------------------------------------------------------------------------------- /src/native/pty.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 Ericsson and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import { Socket } from 'net'; 11 | 12 | // tslint:disable-next-line:variable-name 13 | const tty_wrap = (process as any).binding('tty_wrap'); 14 | // tslint:disable-next-line:no-var-requires 15 | const pty = require('../../build/Release/pty.node'); 16 | interface PtyHandles { 17 | master_fd: number; 18 | slave_name: string; 19 | } 20 | 21 | export class Pty { 22 | 23 | public master: Socket; 24 | public readonly name: string; 25 | 26 | constructor() { 27 | const handles: PtyHandles = pty.create_pty(); 28 | const backup = tty_wrap.guessHandleType; 29 | tty_wrap.guessHandleType = () => 'PIPE'; 30 | this.master = new Socket({ fd: handles.master_fd }); 31 | tty_wrap.guessHandleType = backup; 32 | this.name = handles.slave_name; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/integration-tests/stop.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import { CdtDebugClient } from './debugClient'; 11 | import { standardBeforeEach, gdbPath, testProgramsDir, openGdbConsole } from './utils'; 12 | import { LaunchRequestArguments } from '../GDBDebugSession'; 13 | import { expect } from 'chai'; 14 | import * as path from 'path'; 15 | 16 | describe('stop', async () => { 17 | 18 | let dc: CdtDebugClient; 19 | 20 | beforeEach(async () => { 21 | dc = await standardBeforeEach(); 22 | }); 23 | 24 | afterEach(async () => { 25 | await dc.stop(); 26 | }); 27 | 28 | it('handles segv', async () => { 29 | await dc.launchRequest({ 30 | verbose: true, 31 | gdb: gdbPath, 32 | program: path.join(testProgramsDir, 'segv'), 33 | openGdbConsole, 34 | } as LaunchRequestArguments); 35 | await dc.configurationDoneRequest(); 36 | const stoppedEvent = await dc.waitForEvent('stopped'); 37 | expect(stoppedEvent.body.reason).to.eq('SIGSEGV'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/mi/base.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import { GDBBackend } from '../GDBBackend'; 11 | 12 | export interface MIResponse { 13 | _class: string; 14 | } 15 | 16 | export abstract class MIRequest { 17 | public abstract send(backend: GDBBackend): Promise; 18 | } 19 | 20 | // Shared types 21 | export interface MIBreakpointInfo { 22 | number: string; 23 | type: string; 24 | disp: string; 25 | enabled: string; 26 | addr?: string; 27 | func?: string; 28 | file?: string; // docs are wrong 29 | fullname?: string; 30 | line?: string; 31 | threadGroups: string[]; 32 | times: string; 33 | 'original-location'?: string; 34 | cond?: string; 35 | // TODO there are a lot more fields here 36 | } 37 | 38 | export interface MIFrameInfo { 39 | level: string; 40 | func?: string; 41 | addr?: string; 42 | file?: string; 43 | fullname?: string; 44 | line?: string; 45 | from?: string; 46 | } 47 | 48 | export interface MIVariableInfo { 49 | name: string; 50 | value?: string; 51 | type?: string; 52 | } 53 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': 'pty', 5 | 'sources': ['src/native/pty.cc'], 6 | 'conditions': [ 7 | ['OS=="win"', { 'defines': ['WINDOWS', 'NAPI_CPP_EXCEPTIONS'] }], 8 | ['OS=="mac"', { 'defines': ['MAC'] }], 9 | ['OS=="linux"', { 'defines': ['LINUX'] }], 10 | ], 11 | # https://github.com/nodejs/node/blob/master/doc/api/n-api.md#n-api-version-matrix 12 | 'defines': ['NAPI_VERSION=2'], 13 | }, 14 | ], 15 | 'target_defaults': { 16 | # https://github.com/nodejs/node-addon-api/blob/master/doc/setup.md#installation-and-usage 17 | # Setup N-API C++ wrappers: 18 | 'include_dirs': [" { 31 | let command = '-thread-info'; 32 | if (params.threadId) { 33 | command += ` ${params.threadId}`; 34 | } 35 | return gdb.sendCommand(command); 36 | } 37 | 38 | export interface MIThreadSelectResponse extends MIResponse { 39 | 'new-thread-id': string; 40 | frame: MIFrameInfo; 41 | } 42 | 43 | export function sendThreadSelectRequest(gdb: GDBBackend, params: { 44 | threadId: number; 45 | }): Promise { 46 | return gdb.sendCommand(`-thread-select ${params.threadId}`); 47 | } 48 | -------------------------------------------------------------------------------- /src/integration-tests/GDBBackend.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2019 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import { expect } from 'chai'; 12 | import { LaunchRequestArguments } from '..'; 13 | import { GDBBackend } from '..'; 14 | 15 | describe('GDB Backend Test Suite', function() { 16 | let gdb: GDBBackend; 17 | 18 | beforeEach(function() { 19 | gdb = new GDBBackend(); 20 | const args: LaunchRequestArguments = { 21 | program: 'foo', 22 | }; 23 | gdb.spawn(args); 24 | }); 25 | 26 | afterEach(function() { 27 | gdb.sendGDBExit(); 28 | }); 29 | // Move the timeout out of the way if the adapter is going to be debugged. 30 | if (process.env.INSPECT_DEBUG_ADAPTER) { 31 | this.timeout(9999999); 32 | } 33 | 34 | it('can read a value from -gdb-show', async function() { 35 | const response = await gdb.sendGDBShow('width'); 36 | expect(response.value).to.be.a('string'); 37 | expect(Number(response.value)).to.be.not.equal(NaN); 38 | expect(Number(response.value)).to.be.greaterThan(0); 39 | }); 40 | 41 | it('can set a value using -gdb-set', async function() { 42 | await gdb.sendGDBSet('width 88'); 43 | const response = await gdb.sendGDBShow('width'); 44 | expect(response.value).to.equal('88'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CDT GDB Debug Adapter 2 | 3 | Thanks for your interest in this project, part of the [Eclipse C/C++ Development Tools project](https://projects.eclipse.org/projects/tools.cdt) 4 | 5 | The source code can be found in the following repository: https://github.com/eclipse-cdt/cdt-gdb-adapter 6 | 7 | ## Project description 8 | 9 | This is an implementation of the Debug Adapter Protocol for gdb. 10 | It is loosely based on the Eclipse CDT MI layer. 11 | We are at least learning from it. 12 | 13 | ## Build Instructions 14 | 15 | See build instructions in the [Readme](README.md#building) and details on testing in 16 | the [test readme](src/integration-tests/README.md). Further information on building, 17 | including CI information, download locations, etc can be found in the 18 | [Wiki](https://github.com/eclipse-cdt/cdt-infra/wiki) 19 | 20 | ## Eclipse Contributor Agreement 21 | 22 | Before your contribution can be accepted by the project team contributors must 23 | electronically sign the Eclipse Contributor Agreement (ECA). 24 | 25 | * http://www.eclipse.org/legal/ECA.php 26 | 27 | Commits that are provided by non-committers must have a Signed-off-by field in 28 | the footer indicating that the author is aware of the terms by which the 29 | contribution has been provided to the project. The non-committer must 30 | additionally have an Eclipse Foundation account and must have a signed Eclipse 31 | Contributor Agreement (ECA) on file. 32 | 33 | For more information, please see the Eclipse Committer Handbook: 34 | https://www.eclipse.org/projects/handbook/#resources-commit 35 | 36 | ## Contact 37 | 38 | Contact the project developers via the project's "dev" list. 39 | 40 | * https://dev.eclipse.org/mailman/listinfo/cdt-dev 41 | -------------------------------------------------------------------------------- /src/mi/exec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import { GDBBackend } from '../GDBBackend'; 11 | import { MIResponse } from './base'; 12 | 13 | export function sendExecArguments(gdb: GDBBackend, params: { 14 | arguments: string; 15 | }): Promise { 16 | return gdb.sendCommand(`-exec-arguments ${params.arguments}`); 17 | } 18 | 19 | export function sendExecRun(gdb: GDBBackend) { 20 | return gdb.sendCommand('-exec-run'); 21 | } 22 | 23 | export function sendExecContinue(gdb: GDBBackend, threadId?: number) { 24 | let command = '-exec-continue'; 25 | if (threadId) { 26 | command += ` --thread ${threadId}`; 27 | } 28 | return gdb.sendCommand(command); 29 | } 30 | 31 | export function sendExecNext(gdb: GDBBackend, threadId?: number) { 32 | let command = '-exec-next'; 33 | if (threadId) { 34 | command += ` --thread ${threadId}`; 35 | } 36 | return gdb.sendCommand(command); 37 | } 38 | 39 | export function sendExecStep(gdb: GDBBackend, threadId?: number) { 40 | let command = '-exec-step'; 41 | if (threadId) { 42 | command += ` --thread ${threadId}`; 43 | } 44 | return gdb.sendCommand(command); 45 | } 46 | 47 | export function sendExecFinish(gdb: GDBBackend, threadId?: number) { 48 | let command = '-exec-finish'; 49 | if (threadId) { 50 | command += ` --thread ${threadId}`; 51 | } 52 | return gdb.sendCommand(command); 53 | } 54 | -------------------------------------------------------------------------------- /src/integration-tests/launchRemote.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2019 Kichwa Coders and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import * as path from 'path'; 12 | import { TargetLaunchRequestArguments, TargetLaunchArguments } from '../GDBTargetDebugSession'; 13 | import { CdtDebugClient } from './debugClient'; 14 | import { standardBeforeEach, testProgramsDir } from './utils'; 15 | import { gdbPath, openGdbConsole } from './utils'; 16 | 17 | // Allow non-arrow functions: https://mochajs.org/#arrow-functions 18 | // tslint:disable:only-arrow-functions 19 | describe('launch remote', function() { 20 | 21 | let dc: CdtDebugClient; 22 | const emptyProgram = path.join(testProgramsDir, 'empty'); 23 | const emptySrc = path.join(testProgramsDir, 'empty.c'); 24 | 25 | beforeEach(async function() { 26 | dc = await standardBeforeEach('debugTargetAdapter.js'); 27 | }); 28 | 29 | afterEach(async function() { 30 | await dc.stop(); 31 | }); 32 | 33 | // Move the timeout out of the way if the adapter is going to be debugged. 34 | if (process.env.INSPECT_DEBUG_ADAPTER) { 35 | this.timeout(9999999); 36 | } 37 | 38 | it('can launch remote and hit a breakpoint', async function() { 39 | await dc.hitBreakpoint({ 40 | verbose: true, 41 | gdb: gdbPath, 42 | program: emptyProgram, 43 | openGdbConsole, 44 | target : { 45 | type: 'remote', 46 | } as TargetLaunchArguments, 47 | } as TargetLaunchRequestArguments, { 48 | path: emptySrc, 49 | line: 3, 50 | }); 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /src/integration-tests/attachRemote.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2019 Kichwa Coders and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import * as cp from 'child_process'; 12 | import * as path from 'path'; 13 | import { TargetAttachRequestArguments, TargetAttachArguments } from '../GDBTargetDebugSession'; 14 | import { CdtDebugClient } from './debugClient'; 15 | import { standardBeforeEach, testProgramsDir, gdbServerPath } from './utils'; 16 | import { gdbPath, openGdbConsole } from './utils'; 17 | 18 | // Allow non-arrow functions: https://mochajs.org/#arrow-functions 19 | // tslint:disable:only-arrow-functions 20 | describe('attach remote', function() { 21 | 22 | let dc: CdtDebugClient; 23 | let gdbserver: cp.ChildProcess; 24 | let port: number; 25 | const emptyProgram = path.join(testProgramsDir, 'empty'); 26 | const emptySrc = path.join(testProgramsDir, 'empty.c'); 27 | 28 | beforeEach(async function() { 29 | dc = await standardBeforeEach('debugTargetAdapter.js'); 30 | gdbserver = cp.spawn(gdbServerPath, [':0', emptyProgram], { cwd: testProgramsDir }); 31 | port = await new Promise((resolve, reject) => { 32 | gdbserver.stderr.on('data', (data) => { 33 | const line = String(data); 34 | const LISTENING_ON_PORT = 'Listening on port '; 35 | const index = line.indexOf(LISTENING_ON_PORT); 36 | if (index >= 0) { 37 | const portStr = line.substr(index + LISTENING_ON_PORT.length, 6).trim(); 38 | resolve(parseInt(portStr, 10)); 39 | } 40 | }); 41 | }); 42 | }); 43 | 44 | afterEach(async function() { 45 | await gdbserver.kill(); 46 | await dc.stop(); 47 | }); 48 | 49 | // Move the timeout out of the way if the adapter is going to be debugged. 50 | if (process.env.INSPECT_DEBUG_ADAPTER) { 51 | this.timeout(9999999); 52 | } 53 | 54 | it('can attach remote and hit a breakpoint', async function() { 55 | await dc.hitBreakpoint({ 56 | verbose: true, 57 | gdb: gdbPath, 58 | program: emptyProgram, 59 | openGdbConsole, 60 | target: { 61 | type: 'remote', 62 | parameters: [`localhost:${port}`], 63 | } as TargetAttachArguments, 64 | } as TargetAttachRequestArguments, { 65 | path: emptySrc, 66 | line: 3, 67 | }); 68 | }); 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /src/integration-tests/README.md: -------------------------------------------------------------------------------- 1 | # Integration tests 2 | 3 | This directory contains integration tests of the debug adapter. Theses tests 4 | spawn a debug adapter process and, using a "fake" client, drive a debug 5 | session. It uses the `gdb` in your `PATH`. 6 | 7 | ## Running the tests 8 | 9 | 1. Build the test programs: run `make` in the `test-programs` directory 10 | 2. Build the package as usual: run `yarn` in the top-level directory 11 | 3. Run the tests: run `yarn test:integration` in the top-level directory 12 | 13 | ## Running the tests using Docker 14 | 15 | The tests can be run on a docker container. This is useful to run the testsuite 16 | in the same environment as it is run on the CI machine. 17 | 18 | To do this, simply prefix the desired command (such as `yarn`) with this 19 | command to run it in docker. 20 | 21 | `docker run --rm -it -v $(git rev-parse --show-toplevel):/work -w /work/$(git rev-parse --show-prefix) --cap-add=SYS_PTRACE --security-opt seccomp=unconfined quay.io/eclipse-cdt/cdt-infra-eclipse-full:latest` 22 | 23 | For example, to build and test: 24 | 25 | ``` 26 | docker run --rm -it -v $(git rev-parse --show-toplevel):/work -w /work/$(git rev-parse --show-prefix) --cap-add=SYS_PTRACE --security-opt seccomp=unconfined quay.io/eclipse-cdt/cdt-infra-eclipse-full:latest yarn 27 | docker run --rm -it -v $(git rev-parse --show-toplevel):/work -w /work/$(git rev-parse --show-prefix) --cap-add=SYS_PTRACE --security-opt seccomp=unconfined quay.io/eclipse-cdt/cdt-infra-eclipse-full:latest yarn test 28 | ``` 29 | 30 | ## "Error: No source file name /work/..." 31 | 32 | This error can occur if the compiled test programs have had their path changed, such as when running in a container vs running on the host. 33 | 34 | If you have already built the plug-in on your host system, you may need to clean before running yarn again in the container to ensure that the all compiled paths are correct. A full clean can be done with git, for example `git clean -dnx` (change the `n` to `f` to actually do the clean). 35 | 36 | ## Debugging tips 37 | 38 | To debug, use the included launch configurations in the launch.json file for this vscode project. 39 | 40 | 'Mocha All' will run all compiled spec.js files in the {workspace}/dist/integration-tests/ directory. 41 | 'Mocha Current File' will run the currently open spec.js file in the editor window. You can find the corresponding spec.js file for a given spec.ts file in the directory listed above. 42 | 43 | Breakpoints can be set in the corresponding spec.ts file and will work as expected, though stepping 44 | may take you into compiled or framework .js files. 45 | It may be possible with some additional configuration to run the spec.ts files directly. 46 | 47 | When debugging a test case, you will likely want to run just this one. You can 48 | do so by changing the test declaration: 49 | 50 | it('should do something', ...) 51 | 52 | to 53 | 54 | it.only('should do something', ...) 55 | -------------------------------------------------------------------------------- /src/mi/stack.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import { GDBBackend } from '../GDBBackend'; 11 | import { MIFrameInfo, MIResponse, MIVariableInfo } from './base'; 12 | 13 | export interface MIStackInfoDepthResponse extends MIResponse { 14 | depth: string; 15 | } 16 | 17 | export interface MIStackListVariablesResponse extends MIResponse { 18 | variables: MIVariableInfo[]; 19 | } 20 | 21 | export function sendStackInfoDepth(gdb: GDBBackend, params: { 22 | maxDepth: number; 23 | threadId?: number; 24 | }): Promise { 25 | let command = '-stack-info-depth'; 26 | if (params.threadId) { 27 | command += ` --thread ${params.threadId}`; 28 | } 29 | if (params.maxDepth) { 30 | command += ` ${params.maxDepth}`; 31 | } 32 | return gdb.sendCommand(command); 33 | } 34 | 35 | export function sendStackListFramesRequest(gdb: GDBBackend, params: { 36 | noFrameFilters?: boolean; 37 | lowFrame?: number; 38 | highFrame?: number; 39 | threadId?: number; 40 | }): Promise<{ 41 | stack: MIFrameInfo[]; 42 | }> { 43 | let command = '-stack-list-frames'; 44 | if (params.threadId) { 45 | command += ` --thread ${params.threadId}`; 46 | } 47 | if (params.noFrameFilters) { 48 | command += ' -no-frame-filters'; 49 | } 50 | if (params.lowFrame !== undefined) { 51 | command += ` ${params.lowFrame}`; 52 | } 53 | if (params.highFrame !== undefined) { 54 | command += ` ${params.highFrame}`; 55 | } 56 | return gdb.sendCommand(command); 57 | } 58 | 59 | export function sendStackSelectFrame(gdb: GDBBackend, params: { 60 | framenum: number; 61 | }): Promise { 62 | return gdb.sendCommand(`-stack-select-frame ${params.framenum}`); 63 | } 64 | 65 | export function sendStackListVariables(gdb: GDBBackend, params: { 66 | thread?: number; 67 | frame?: number; 68 | printValues: 'no-values' | 'all-values' | 'simple-values'; 69 | noFrameFilters?: boolean; 70 | skipUnavailable?: boolean; 71 | }): Promise { 72 | let command = '-stack-list-variables'; 73 | if (params.noFrameFilters) { 74 | command += ' --no-frame-filters'; 75 | } 76 | if (params.skipUnavailable) { 77 | command += ' --skip-unavailable'; 78 | } 79 | if (params.thread) { 80 | command += ` --thread ${params.thread}`; 81 | } 82 | if (params.frame) { 83 | command += ` --frame ${params.frame}`; 84 | } 85 | command += ` --${params.printValues}`; 86 | 87 | return gdb.sendCommand(command); 88 | } 89 | -------------------------------------------------------------------------------- /src/integration-tests/evaluate.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2019 Ericsson and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import { expect } from 'chai'; 12 | import * as path from 'path'; 13 | import { CdtDebugClient } from './debugClient'; 14 | import { 15 | expectRejection, gdbPath, getScopes, openGdbConsole, Scope, standardBeforeEach, testProgramsDir, 16 | } from './utils'; 17 | 18 | // Allow non-arrow functions: https://mochajs.org/#arrow-functions 19 | // tslint:disable:only-arrow-functions 20 | 21 | describe('evaluate request', function() { 22 | let dc: CdtDebugClient; 23 | let scope: Scope; 24 | 25 | const evaluateProgram = path.join(testProgramsDir, 'evaluate'); 26 | const evaluateSrc = path.join(testProgramsDir, 'evaluate.cpp'); 27 | 28 | beforeEach(async function() { 29 | // Move the timeout out of the way if the adapter is going to be debugged. 30 | if (process.env.INSPECT_DEBUG_ADAPTER) { 31 | this.timeout(9999999); 32 | } 33 | dc = await standardBeforeEach(); 34 | await dc.hitBreakpoint({ 35 | verbose: true, 36 | gdb: gdbPath, 37 | program: evaluateProgram, 38 | openGdbConsole, 39 | }, { 40 | path: evaluateSrc, 41 | line: 2, 42 | }); 43 | scope = await getScopes(dc); 44 | }); 45 | 46 | afterEach(async function() { 47 | await dc.stop(); 48 | }); 49 | 50 | // Move the timeout out of the way if the adapter is going to be debugged. 51 | if (process.env.INSPECT_DEBUG_ADAPTER) { 52 | this.timeout(9999999); 53 | } 54 | 55 | it('should evaluate a simple literal expression', async function() { 56 | const res = await dc.evaluateRequest({ 57 | context: 'repl', 58 | expression: '2 + 2', 59 | frameId: scope.frame.id, 60 | }); 61 | 62 | expect(res.body.result).eq('4'); 63 | }); 64 | 65 | it('should reject evaluation of expression without a frame', async function() { 66 | const err = await expectRejection(dc.evaluateRequest({ 67 | context: 'repl', 68 | expression: '2 + 2', 69 | })); 70 | 71 | expect(err.message).eq('Evaluation of expression without frameId is not supported.'); 72 | }); 73 | 74 | it('should reject evaluation of invalid expression', async function() { 75 | const err = await expectRejection(dc.evaluateRequest({ 76 | context: 'repl', 77 | expression: '2 +', 78 | frameId: scope.frame.id, 79 | })); 80 | 81 | expect(err.message).eq('-var-create: unable to create variable object'); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/integration-tests/logpoints.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2019 Arm and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import { join } from 'path'; 12 | import { expect } from 'chai'; 13 | import { CdtDebugClient } from './debugClient'; 14 | import { LaunchRequestArguments } from '../GDBDebugSession'; 15 | import { 16 | standardBeforeEach, 17 | gdbPath, 18 | testProgramsDir, 19 | openGdbConsole, 20 | } from './utils'; 21 | 22 | describe('logpoints', async () => { 23 | let dc: CdtDebugClient; 24 | 25 | beforeEach(async () => { 26 | dc = await standardBeforeEach(); 27 | 28 | await dc.launchRequest({ 29 | verbose: true, 30 | gdb: gdbPath, 31 | program: join(testProgramsDir, 'count'), 32 | openGdbConsole, 33 | } as LaunchRequestArguments); 34 | }); 35 | 36 | afterEach(async () => { 37 | await dc.stop(); 38 | }); 39 | 40 | it('hits a logpoint', async () => { 41 | const logMessage = 'log message'; 42 | 43 | await dc.setBreakpointsRequest({ 44 | source: { 45 | name: 'count.c', 46 | path: join(testProgramsDir, 'count.c'), 47 | }, 48 | breakpoints: [ 49 | { 50 | column: 1, 51 | line: 4, 52 | logMessage, 53 | }, 54 | ], 55 | }); 56 | await dc.configurationDoneRequest(); 57 | const logEvent = await dc.waitForOutputEvent('console'); 58 | expect(logEvent.body.output).to.eq(logMessage); 59 | }); 60 | 61 | it('supports changing log messages', async () => { 62 | const logMessage = 'log message'; 63 | 64 | await dc.setBreakpointsRequest({ 65 | source: { 66 | name: 'count.c', 67 | path: join(testProgramsDir, 'count.c'), 68 | }, 69 | breakpoints: [ 70 | { 71 | column: 1, 72 | line: 4, 73 | logMessage: 'something uninteresting', 74 | }, 75 | ], 76 | }); 77 | await dc.setBreakpointsRequest({ 78 | source: { 79 | name: 'count.c', 80 | path: join(testProgramsDir, 'count.c'), 81 | }, 82 | breakpoints: [ 83 | { 84 | column: 1, 85 | line: 4, 86 | logMessage, 87 | }, 88 | ], 89 | }); 90 | await dc.configurationDoneRequest(); 91 | const logEvent = await dc.waitForOutputEvent('console'); 92 | expect(logEvent.body.output).to.eq(logMessage); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Server", 8 | "cwd": "${workspaceFolder}", 9 | "program": "${workspaceFolder}/dist/debugAdapter.js", 10 | "args": [ 11 | "--server=4711" 12 | ], 13 | "outFiles": [ 14 | "${workspaceFolder}/dist/**/*.js" 15 | ] 16 | }, 17 | { 18 | "type": "node", 19 | "request": "launch", 20 | "name": "Server (Target adapter)", 21 | "cwd": "${workspaceFolder}", 22 | "program": "${workspaceFolder}/dist/debugTargetAdapter.js", 23 | "args": [ 24 | "--server=4711" 25 | ], 26 | "outFiles": [ 27 | "${workspaceFolder}/dist/**/*.js" 28 | ] 29 | }, 30 | { 31 | "type": "node", 32 | "request": "launch", 33 | "name": "Mocha All", 34 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 35 | "args": [ 36 | "--timeout", 37 | "999999", 38 | "--colors", 39 | "${workspaceFolder}/dist/integration-tests/*.spec.js" 40 | ], 41 | "console": "integratedTerminal", 42 | "internalConsoleOptions": "neverOpen" 43 | }, 44 | { 45 | "type": "node", 46 | "request": "launch", 47 | "name": "Mocha Current File", 48 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 49 | "args": [ 50 | "--timeout", 51 | "999999", 52 | "--colors", 53 | "-r", 54 | "ts-node/register", 55 | "${file}" 56 | ], 57 | "console": "integratedTerminal", 58 | "internalConsoleOptions": "neverOpen" 59 | }, 60 | { 61 | "type": "node", 62 | "request": "launch", 63 | "name": "Mocha Current File (Attach to Server)", 64 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 65 | "args": [ 66 | "--debugserverport", 67 | "4711", 68 | "--timeout", 69 | "999999", 70 | "--colors", 71 | "-r", 72 | "ts-node/register", 73 | "${file}" 74 | ], 75 | "console": "integratedTerminal", 76 | "internalConsoleOptions": "neverOpen" 77 | } 78 | ], 79 | "compounds": [ 80 | { 81 | "name": "Mocha Current File & launch Server", 82 | "configurations": [ 83 | "Server", 84 | "Mocha Current File (Attach to Server)" 85 | ] 86 | }, 87 | { 88 | "name": "Mocha Current File & launch Target Server", 89 | "configurations": [ 90 | "Server (Target adapter)", 91 | "Mocha Current File (Attach to Server)" 92 | ] 93 | } 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /src/mi/data.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 Ericsson and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import { GDBBackend } from '../GDBBackend'; 12 | import { MIResponse } from './base'; 13 | 14 | interface MIDataReadMemoryBytesResponse { 15 | memory: Array<{ 16 | begin: string; 17 | end: string; 18 | offset: string; 19 | contents: string; 20 | }>; 21 | } 22 | interface MIDataDisassembleAsmInsn { 23 | address: string; 24 | // func-name in MI 25 | 'func-name': string; 26 | offset: string; 27 | opcodes: string; 28 | inst: string; 29 | } 30 | 31 | interface MIDataDisassembleSrcAndAsmLine { 32 | line: string; 33 | file: string; 34 | fullname: string; 35 | line_asm_insn: MIDataDisassembleAsmInsn[]; 36 | } 37 | interface MIDataDisassembleResponse { 38 | asm_insns: MIDataDisassembleSrcAndAsmLine[]; 39 | } 40 | 41 | export interface MIGDBDataEvaluateExpressionResponse extends MIResponse { 42 | value?: string; 43 | } 44 | 45 | export function sendDataReadMemoryBytes(gdb: GDBBackend, address: string, size: number, offset: number = 0) 46 | : Promise { 47 | return gdb.sendCommand(`-data-read-memory-bytes -o ${offset} "${address}" ${size}`); 48 | } 49 | 50 | export function sendDataEvaluateExpression(gdb: GDBBackend, expr: string) 51 | : Promise { 52 | return gdb.sendCommand(`-data-evaluate-expression "${expr}"`); 53 | } 54 | 55 | // https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Data-Manipulation.html#The-_002ddata_002ddisassemble-Command 56 | export async function sendDataDisassemble(gdb: GDBBackend, startAddress: string, endAddress: string) 57 | : Promise { 58 | // -- 5 == mixed source and disassembly with raw opcodes 59 | // TODO needs to be -- 3 for GDB < 7.11 -- are we supporting such old versions? 60 | const result: MIDataDisassembleResponse = 61 | await gdb.sendCommand(`-data-disassemble -s "${startAddress}" -e "${endAddress}" -- 5`); 62 | 63 | // cleanup the result data 64 | if (result.asm_insns.length > 0) { 65 | if (!result.asm_insns[0].hasOwnProperty('line_asm_insn')) { 66 | // In this case there is no source info available for any instruction, 67 | // so GDB treats as if we had done -- 2 instead of -- 5 68 | // This bit of code remaps the data to look like it should 69 | const e: MIDataDisassembleSrcAndAsmLine = { 70 | line_asm_insn: result.asm_insns as unknown as MIDataDisassembleAsmInsn[], 71 | } as MIDataDisassembleSrcAndAsmLine; 72 | result.asm_insns = [e]; 73 | } 74 | for (const asmInsn of result.asm_insns) { 75 | if (!asmInsn.hasOwnProperty('line_asm_insn')) { 76 | asmInsn.line_asm_insn = []; 77 | } 78 | } 79 | } 80 | return Promise.resolve(result); 81 | } 82 | -------------------------------------------------------------------------------- /src/integration-tests/launch.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 Ericsson and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import { expect } from 'chai'; 12 | import * as path from 'path'; 13 | import { LaunchRequestArguments } from '../GDBDebugSession'; 14 | import { CdtDebugClient } from './debugClient'; 15 | import { standardBeforeEach, testProgramsDir } from './utils'; 16 | import { gdbPath, openGdbConsole } from './utils'; 17 | 18 | // Allow non-arrow functions: https://mochajs.org/#arrow-functions 19 | // tslint:disable:only-arrow-functions 20 | 21 | describe('launch', function() { 22 | 23 | let dc: CdtDebugClient; 24 | const emptyProgram = path.join(testProgramsDir, 'empty'); 25 | const emptySpaceProgram = path.join(testProgramsDir, 'empty space'); 26 | const emptySrc = path.join(testProgramsDir, 'empty.c'); 27 | const emptySpaceSrc = path.join(testProgramsDir, 'empty space.c'); 28 | 29 | beforeEach(async function() { 30 | dc = await standardBeforeEach(); 31 | }); 32 | 33 | afterEach(async function() { 34 | await dc.stop(); 35 | }); 36 | 37 | // Move the timeout out of the way if the adapter is going to be debugged. 38 | if (process.env.INSPECT_DEBUG_ADAPTER) { 39 | this.timeout(9999999); 40 | } 41 | 42 | it('can launch and hit a breakpoint', async function() { 43 | await dc.hitBreakpoint({ 44 | verbose: true, 45 | gdb: gdbPath, 46 | program: emptyProgram, 47 | openGdbConsole, 48 | } as LaunchRequestArguments, { 49 | path: emptySrc, 50 | line: 3, 51 | }); 52 | }); 53 | 54 | it('reports an error when specifying a non-existent binary', async function() { 55 | const errorMessage = await new Promise((resolve, reject) => { 56 | dc.launchRequest({ 57 | verbose: true, 58 | gdb: gdbPath, 59 | program: '/does/not/exist', 60 | openGdbConsole, 61 | } as LaunchRequestArguments) 62 | .then(reject) 63 | .catch(resolve); 64 | }); 65 | 66 | // When launching a remote test gdbserver generates the error which is not exactly the same 67 | // as GDB's error 68 | expect(errorMessage.message).to.satisfy((msg: string) => msg.includes('/does/not/exist') 69 | && (msg.includes('The system cannot find the path specified') 70 | || msg.includes('No such file or directory') 71 | || msg.includes('not found'))); 72 | }); 73 | 74 | it('works with a space in file names', async function() { 75 | await dc.hitBreakpoint({ 76 | verbose: true, 77 | gdb: gdbPath, 78 | program: emptySpaceProgram, 79 | openGdbConsole, 80 | } as LaunchRequestArguments, { 81 | path: emptySpaceSrc, 82 | line: 3, 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdt-gdb-adapter", 3 | "version": "0.0.15-next", 4 | "description": "gdb adapter implementing the debug adapter protocol", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "install": "node install.js", 9 | "nativebuild": "node-gyp rebuild", 10 | "prepublish": "yarn build", 11 | "build": "tsc", 12 | "watch": "tsc -w", 13 | "clean": "git clean -dfx", 14 | "docker:build": "docker run --rm -it -v $(git rev-parse --show-toplevel):/work -w /work/$(git rev-parse --show-prefix) --cap-add=SYS_PTRACE --security-opt seccomp=unconfined quay.io/eclipse-cdt/cdt-infra-eclipse-full:latest yarn", 15 | "docker:test": "docker run --rm -it -v $(git rev-parse --show-toplevel):/work -w /work/$(git rev-parse --show-prefix) --cap-add=SYS_PTRACE --security-opt seccomp=unconfined quay.io/eclipse-cdt/cdt-infra-eclipse-full:latest yarn test", 16 | "test": "yarn test:integration && yarn test:pty && yarn test:integration-run-in-terminal && yarn test:integration-remote-target && yarn test:integration-remote-target-run-in-terminal", 17 | "test:integration": "cross-env JUNIT_REPORT_PATH=test-reports/integration.xml JUNIT_REPORT_STACK=1 JUNIT_REPORT_PACKAGES=1 mocha --reporter mocha-jenkins-reporter dist/integration-tests/*.spec.js", 18 | "test:integration-run-in-terminal": "cross-env JUNIT_REPORT_PATH=test-reports/integration-run-in-terminal.xml JUNIT_REPORT_STACK=1 JUNIT_REPORT_PACKAGES=1 mocha --run-in-terminal --reporter mocha-jenkins-reporter dist/integration-tests/*.spec.js", 19 | "test:integration-remote-target": "cross-env JUNIT_REPORT_PATH=test-reports/integration-remote-target.xml JUNIT_REPORT_STACK=1 JUNIT_REPORT_PACKAGES=1 mocha --test-remote --reporter mocha-jenkins-reporter dist/integration-tests/*.spec.js", 20 | "test:integration-remote-target-run-in-terminal": "cross-env JUNIT_REPORT_PATH=test-reports/integration-remote-target-run-in-terminal.xml JUNIT_REPORT_STACK=1 JUNIT_REPORT_PACKAGES=1 mocha --test-remote --run-in-terminal --reporter mocha-jenkins-reporter dist/integration-tests/*.spec.js", 21 | "test:pty": "cross-env JUNIT_REPORT_PATH=test-reports/native.xml JUNIT_REPORT_STACK=1 JUNIT_REPORT_PACKAGES=1 mocha --reporter mocha-jenkins-reporter dist/native/*.spec.js" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/eclipse-cdt/cdt-gdb-adapter.git" 26 | }, 27 | "author": "Eclipse CDT", 28 | "contributors": [ 29 | "Rob Moran " 30 | ], 31 | "license": "EPL-2.0", 32 | "bugs": { 33 | "url": "https://github.com/eclipse-cdt/cdt-gdb-adapter/issues" 34 | }, 35 | "homepage": "https://github.com/eclipse-cdt/cdt-gdb-adapter#readme", 36 | "dependencies": { 37 | "node-addon-api": "^1.6.2", 38 | "vscode-debugadapter": "^1.37.1", 39 | "vscode-debugprotocol": "^1.37.0" 40 | }, 41 | "devDependencies": { 42 | "@types/chai": "^4.1.7", 43 | "@types/mocha": "^5.2.5", 44 | "@types/node": "^10.12.2", 45 | "chai": "^4.2.0", 46 | "cross-env": "^5.2.0", 47 | "mocha": "^5.2.0", 48 | "mocha-jenkins-reporter": "^0.4.1", 49 | "node-gyp": "^3.8.0", 50 | "ts-loader": "^5.3.0", 51 | "ts-node": "^8.3.0", 52 | "tslint": "^5.11.0", 53 | "tslint-language-service": "^0.9.9", 54 | "typescript": "^3.1.5", 55 | "vscode-debugadapter-testsupport": "^1.37.1", 56 | "webpack": "^4.23.1", 57 | "webpack-cli": "^3.1.2" 58 | }, 59 | "files": [ 60 | "NOTICE", 61 | "LICENSE", 62 | "README.md", 63 | "CONTRIBUTING.md", 64 | "dist/**/*.js", 65 | "dist/**/*.js.map", 66 | "dist/**/*.d.ts", 67 | "src/**/*.cc", 68 | "src/**/*.h", 69 | "src/*.ts", 70 | "src/mi/*.ts", 71 | "src/native/*.ts", 72 | "install.js", 73 | "binding.gyp" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /src/native/pty.cc: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 Ericsson and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | #include "napi.h" 11 | 12 | #ifndef WINDOWS 13 | #include "scoped_fd.h" 14 | #include 15 | #include 16 | #include 17 | #include 18 | #endif 19 | 20 | static void _throw_exc(const Napi::Env env, const char *message) { 21 | throw Napi::Error::New(env, message); 22 | } 23 | 24 | #ifdef LINUX 25 | /** 26 | * Takes an error code and throws a pretty JS error such as: 27 | * "function_name: errormsg". 28 | */ 29 | static void _throw_exc_format(const Napi::Env env, int error, 30 | const char *function_name) { 31 | const int ERRMSG_MAX_SIZE = 128; 32 | char errmsg_buffer[ERRMSG_MAX_SIZE]; 33 | char message[ERRMSG_MAX_SIZE]; 34 | #ifdef _GNU_SOURCE 35 | char *errmsg = strerror_r(error, errmsg_buffer, ERRMSG_MAX_SIZE); 36 | snprintf(message, ERRMSG_MAX_SIZE, "%s: %s", function_name, errmsg); 37 | #else 38 | int rc = strerror_r(error, errmsg_buffer, ERRMSG_MAX_SIZE); 39 | if (rc) { 40 | snprintf(message, ERRMSG_MAX_SIZE, "%s", function_name); 41 | } else { 42 | snprintf(message, ERRMSG_MAX_SIZE, "%s: %s", function_name, errmsg_buffer); 43 | } 44 | #endif 45 | 46 | _throw_exc(env, message); 47 | } 48 | #endif 49 | 50 | static Napi::Value create_pty(const Napi::CallbackInfo &info) { 51 | Napi::Env env = info.Env(); 52 | #ifndef LINUX 53 | // Windows does not supports TTYs. 54 | _throw_exc(env, ".create_pty() is not supported on this platform"); 55 | #else 56 | // master_fd will be closed on scope exit if an error is thrown. 57 | scoped_fd master_fd(open("/dev/ptmx", O_RDWR)); 58 | if (master_fd == -1) { 59 | _throw_exc(env, "open(\"/dev/ptmx\", O_RDWR) failed"); 60 | } 61 | const int SLAVE_NAME_MAX_SIZE = 128; 62 | char slave_name[SLAVE_NAME_MAX_SIZE]; 63 | termios configuration; 64 | int error; 65 | 66 | error = tcgetattr(master_fd.get(), &configuration); 67 | if (error) 68 | _throw_exc_format(env, error, "tcgetattr"); 69 | 70 | // By default, the master tty will be in echo mode, which means that we will 71 | // get what we write back when we read from it. The stream is also line 72 | // buffered by default. Making it raw prevents all this. 73 | // see: man cfmakeraw 74 | cfmakeraw(&configuration); 75 | 76 | error = tcsetattr(master_fd.get(), 0, &configuration); 77 | if (error) 78 | _throw_exc_format(env, error, "tcsetattr"); 79 | 80 | // see: man ptmx 81 | error = ptsname_r(master_fd.get(), slave_name, SLAVE_NAME_MAX_SIZE); 82 | if (error) 83 | _throw_exc_format(env, error, "ptsname_r"); 84 | error = grantpt(master_fd.get()); 85 | if (error) 86 | _throw_exc_format(env, error, "grantpt"); 87 | error = unlockpt(master_fd.get()); 88 | if (error) 89 | _throw_exc_format(env, error, "unlockpt"); 90 | 91 | // We release master_fd for the scoped_fd wrapper to not actually close it, 92 | // as we want to send it to the running JS scripts. 93 | Napi::Object terminal = Napi::Object::New(env); 94 | terminal.Set("master_fd", master_fd.release()); 95 | terminal.Set("slave_name", slave_name); 96 | return terminal; 97 | #endif 98 | } 99 | 100 | static Napi::Object initialize(Napi::Env env, Napi::Object exports) { 101 | exports.Set("create_pty", Napi::Function::New(env, create_pty)); 102 | return exports; 103 | } 104 | 105 | NODE_API_MODULE(NODE_GYP_MODULE_NAME, initialize); 106 | -------------------------------------------------------------------------------- /src/mi/breakpoint.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import { GDBBackend } from '../GDBBackend'; 11 | import { MIBreakpointInfo, MIResponse } from './base'; 12 | 13 | /** 14 | * The generic MI Parser (see MIParser.handleAsyncData) cannot differentiate 15 | * properly between an array or single result from -break-insert. Therefore 16 | * we get two possible response types. The cleanupBreakpointResponse 17 | * normalizes the response. 18 | */ 19 | interface MIBreakInsertResponseInternal extends MIResponse { 20 | bkpt: MIBreakpointInfo[] | MIBreakpointInfo; 21 | } 22 | export interface MIBreakInsertResponse extends MIResponse { 23 | bkpt: MIBreakpointInfo; 24 | /** 25 | * In cases where GDB inserts multiple breakpoints, the "children" 26 | * breakpoints will be stored in multiple field. 27 | */ 28 | multiple?: MIBreakpointInfo[]; 29 | } 30 | 31 | export interface MIBreakDeleteRequest { 32 | 33 | } 34 | 35 | export interface MIBreakDeleteResponse extends MIResponse { 36 | } 37 | 38 | export interface MIBreakListResponse extends MIResponse { 39 | BreakpointTable: { 40 | nr_rows: string, 41 | nr_cols: string, 42 | hrd: Array<{ 43 | width: string, 44 | alignment: string, 45 | col_name: string, 46 | colhdr: string, 47 | }>; 48 | body: MIBreakpointInfo[] 49 | }; 50 | } 51 | 52 | function cleanupBreakpointResponse(raw: MIBreakInsertResponseInternal): MIBreakInsertResponse { 53 | if (Array.isArray(raw.bkpt)) { 54 | const bkpt = raw.bkpt[0]; 55 | const multiple = raw.bkpt.slice(1); 56 | return { 57 | _class: raw._class, 58 | bkpt, 59 | multiple, 60 | }; 61 | } 62 | return { 63 | _class: raw._class, 64 | bkpt: raw.bkpt, 65 | }; 66 | } 67 | 68 | export async function sendBreakInsert(gdb: GDBBackend, request: { 69 | temporary?: boolean; 70 | hardware?: boolean; 71 | pending?: boolean; 72 | disabled?: boolean; 73 | tracepoint?: boolean; 74 | condition?: string; 75 | ignoreCount?: number; 76 | threadId?: string; 77 | source: string; 78 | line: number; 79 | }): Promise { 80 | // Todo: lots of options 81 | const temp = request.temporary ? '-t ' : ''; 82 | const ignore = request.ignoreCount ? `-i ${request.ignoreCount} ` : ''; 83 | const source = `--source ${gdb.standardEscape(request.source)}`; 84 | const line = `--line ${request.line}`; 85 | const command = `-break-insert ${temp}${ignore}${source} ${line}`; 86 | const result = await gdb.sendCommand(command); 87 | const clean = cleanupBreakpointResponse(result); 88 | if (request.condition) { 89 | await gdb.sendCommand(`-break-condition ${clean.bkpt.number} ${request.condition}`); 90 | } 91 | 92 | return clean; 93 | } 94 | 95 | export function sendBreakDelete(gdb: GDBBackend, request: { 96 | breakpoints: string[]; 97 | }): Promise { 98 | return gdb.sendCommand(`-break-delete ${request.breakpoints.join(' ')}`); 99 | } 100 | 101 | export function sendBreakList(gdb: GDBBackend): Promise { 102 | return gdb.sendCommand('-break-list'); 103 | } 104 | 105 | export async function sendBreakFunctionInsert(gdb: GDBBackend, fn: string): Promise { 106 | const command = `-break-insert --function ${fn}`; 107 | const result = await gdb.sendCommand(command); 108 | const clean = cleanupBreakpointResponse(result); 109 | return clean; 110 | } 111 | -------------------------------------------------------------------------------- /src/native/pty.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2019 Ericsson and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import { expect } from 'chai'; 11 | import * as fs from 'fs'; 12 | import { Socket } from 'net'; 13 | import * as os from 'os'; 14 | import { Duplex, Readable, Writable } from 'stream'; 15 | import { Pty } from '../native/pty'; 16 | 17 | // Allow non-arrow functions: https://mochajs.org/#arrow-functions 18 | // tslint:disable:only-arrow-functions no-console no-bitwise 19 | 20 | if (os.platform() !== 'win32') { 21 | describe('pty creation', function() { 22 | 23 | let master: Socket; 24 | let slave: File; 25 | 26 | afterEach(function() { 27 | if (slave) { 28 | slave.destroy(); 29 | } 30 | if (master) { 31 | master.destroy(); 32 | } 33 | }); 34 | 35 | it('should be able to open a ptmx/pts pair', async function() { 36 | const pty = new Pty(); 37 | 38 | master = pty.master; 39 | slave = new File(fs.openSync(pty.name, 'r+')); 40 | 41 | function onError(error: Error) { 42 | console.error(error); 43 | throw error; 44 | } 45 | master.on('error', onError); 46 | slave.on('error', onError); 47 | 48 | let masterStream = ''; 49 | let slaveStream = ''; 50 | 51 | master.on('data', (data) => masterStream += data.toString('utf8')); 52 | slave.on('data', (data) => slaveStream += data.toString('utf8')); 53 | 54 | expect(masterStream).eq(''); 55 | expect(slaveStream).eq(''); 56 | 57 | await sendAndAwait('master2slave', master, slave); 58 | 59 | expect(masterStream).eq(''); 60 | expect(slaveStream).eq('master2slave'); 61 | 62 | await sendAndAwait('slave2master', slave, master); 63 | 64 | expect(masterStream).eq('slave2master'); 65 | expect(slaveStream).eq('master2slave'); 66 | }); 67 | 68 | }); 69 | 70 | /** 71 | * Assumes that we are the only one writing to 72 | * @param str 73 | * @param writeTo 74 | * @param readFrom 75 | */ 76 | function sendAndAwait(str: string, writeTo: Writable, readFrom: Readable): Promise { 77 | return new Promise((resolve) => { 78 | readFrom.once('data', () => resolve()); 79 | writeTo.write(str); 80 | }); 81 | } 82 | 83 | class File extends Duplex { 84 | 85 | public static MIN_BUFFER_SIZE = 1 << 10; 86 | public static DEFAULT_BUFFER_SIZE = 1 << 16; 87 | 88 | protected destroyed = false; 89 | protected buffer: Buffer; 90 | 91 | constructor( 92 | public fd: number, 93 | bufferSize: number = File.DEFAULT_BUFFER_SIZE, 94 | ) { 95 | super(); 96 | this.buffer = Buffer.alloc(Math.max(bufferSize, File.MIN_BUFFER_SIZE)); 97 | } 98 | 99 | public _write(str: string, encoding: string, callback: (error?: Error | null) => void): void { 100 | fs.write(this.fd, Buffer.from(str, encoding), callback); 101 | } 102 | 103 | public _read(size: number): void { 104 | fs.read(this.fd, this.buffer, 0, Math.min(this.buffer.length, size), null, 105 | (error, bytesRead, readBuffer) => { 106 | if (error) { 107 | if (this.destroyed) { return; } 108 | throw error; 109 | } 110 | this.push(readBuffer.slice(0, bytesRead)); 111 | }, 112 | ); 113 | } 114 | 115 | public _destroy(error: Error | null, callback?: (error: Error | null) => void): void { 116 | this.destroyed = true; 117 | if (error) { 118 | throw error; 119 | } 120 | if (callback) { 121 | fs.close(this.fd, callback); 122 | } 123 | } 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/mi/var.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import { GDBBackend } from '../GDBBackend'; 11 | import { MIResponse } from './base'; 12 | 13 | export interface MIVarCreateResponse extends MIResponse { 14 | name: string; 15 | numchild: string; 16 | value: string; 17 | type: string; 18 | 'thread-id'?: string; 19 | has_more?: string; 20 | dynamic?: string; 21 | displayhint?: string; 22 | } 23 | 24 | export interface MIVarListChildrenResponse { 25 | numchild: string; 26 | children: MIVarChild[]; 27 | } 28 | 29 | export interface MIVarChild { 30 | name: string; 31 | exp: string; 32 | numchild: string; 33 | type: string; 34 | value?: string; 35 | 'thread-id'?: string; 36 | frozen?: string; 37 | displayhint?: string; 38 | dynamic?: string; 39 | } 40 | 41 | export interface MIVarUpdateResponse { 42 | changelist: Array<{ 43 | name: string; 44 | value: string; 45 | in_scope: string; 46 | type_changed: string; 47 | has_more: string; 48 | }>; 49 | } 50 | 51 | export interface MIVarEvalResponse { 52 | value: string; 53 | } 54 | 55 | export interface MIVarAssignResponse { 56 | value: string; 57 | } 58 | 59 | export interface MIVarPathInfoResponse { 60 | path_expr: string; 61 | } 62 | 63 | function quote(expression: string) { 64 | return `"${expression}"` ; 65 | } 66 | 67 | export function sendVarCreate(gdb: GDBBackend, params: { 68 | name?: string; 69 | frameAddr?: string; 70 | frame?: 'current' | 'floating'; 71 | expression: string; 72 | }): Promise { 73 | let command = '-var-create'; 74 | command += ` ${params.name ? params.name : '-'}`; 75 | if (params.frameAddr) { 76 | command += ` ${params.frameAddr}`; 77 | } else if (params.frame) { 78 | switch (params.frame) { 79 | case 'current': 80 | command += ' *'; 81 | break; 82 | case 'floating': 83 | command += ' @'; 84 | break; 85 | } 86 | } 87 | command += ` ${quote(params.expression)}`; 88 | 89 | return gdb.sendCommand(command); 90 | } 91 | 92 | export function sendVarListChildren(gdb: GDBBackend, params: { 93 | printValues?: 'no-values' | 'all-values' | 'simple-values'; 94 | name: string; 95 | from?: number; 96 | to?: number; 97 | }): Promise { 98 | let command = '-var-list-children'; 99 | if (params.printValues) { 100 | command += ` --${params.printValues}`; 101 | } 102 | command += ` ${params.name}`; 103 | if (params.from && params.to) { 104 | command += ` ${params.from} ${params.to}`; 105 | } 106 | 107 | return gdb.sendCommand(command); 108 | } 109 | 110 | export function sendVarUpdate(gdb: GDBBackend, params: { 111 | threadId?: number; 112 | name?: string; 113 | }): Promise { 114 | let command = '-var-update'; 115 | if (params.threadId) { 116 | command += ` ${params.threadId}`; 117 | } 118 | if (params.name) { 119 | command += ` ${params.name}`; 120 | } else { 121 | command += ' *'; 122 | } 123 | return gdb.sendCommand(command); 124 | } 125 | 126 | export function sendVarDelete(gdb: GDBBackend, params: { 127 | varname: string, 128 | }): Promise { 129 | const command = `-var-delete ${params.varname}`; 130 | return gdb.sendCommand(command); 131 | } 132 | 133 | export function sendVarAssign(gdb: GDBBackend, params: { 134 | varname: string, 135 | expression: string, 136 | }): Promise { 137 | const command = `-var-assign ${params.varname} ${params.expression}`; 138 | return gdb.sendCommand(command); 139 | } 140 | 141 | export function sendVarEvaluateExpression(gdb: GDBBackend, params: { 142 | varname: string, 143 | }): Promise { 144 | const command = `-var-evaluate-expression ${params.varname}`; 145 | return gdb.sendCommand(command); 146 | } 147 | 148 | export function sendVarInfoPathExpression(gdb: GDBBackend, name: string): Promise { 149 | const command = `-var-info-path-expression ${name}`; 150 | return gdb.sendCommand(command); 151 | } 152 | -------------------------------------------------------------------------------- /src/varManager.ts: -------------------------------------------------------------------------------- 1 | import { GDBBackend } from './GDBBackend'; 2 | import { MIVarCreateResponse } from './mi/var'; 3 | import { sendVarCreate, sendVarDelete, sendVarUpdate } from './mi/var'; 4 | 5 | export interface VarObjType { 6 | varname: string; 7 | expression: string; 8 | numchild: string; 9 | children: VarObjType[]; 10 | value: string; 11 | type: string; 12 | isVar: boolean; 13 | isChild: boolean; 14 | } 15 | 16 | export class VarManager { 17 | protected readonly variableMap: Map = new Map(); 18 | 19 | constructor(protected gdb: GDBBackend) { 20 | this.gdb = gdb; 21 | } 22 | 23 | public getKey(frameId: number, threadId: number, depth: number): string { 24 | return `frame${frameId}_thread${threadId}_depth${depth}`; 25 | } 26 | 27 | public getVars(frameId: number, threadId: number, depth: number): VarObjType[] | undefined { 28 | return this.variableMap.get(this.getKey(frameId, threadId, depth)); 29 | } 30 | 31 | public getVar(frameId: number, threadId: number, depth: number, expression: string): VarObjType | undefined { 32 | const vars = this.getVars(frameId, threadId, depth); 33 | if (vars) { 34 | for (const varobj of vars) { 35 | if (varobj.expression === expression) { 36 | return varobj; 37 | } 38 | } 39 | } 40 | return; 41 | } 42 | 43 | public getVarByName(frameId: number, threadId: number, depth: number, varname: string) 44 | : VarObjType | undefined { 45 | const vars = this.getVars(frameId, threadId, depth); 46 | if (vars) { 47 | for (const varobj of vars) { 48 | if (varobj.varname === varname) { 49 | return varobj; 50 | } 51 | } 52 | } 53 | return; 54 | } 55 | 56 | public addVar(frameId: number, threadId: number, depth: number, expression: string, isVar: boolean, 57 | isChild: boolean, varCreateResponse: MIVarCreateResponse): VarObjType { 58 | let vars = this.variableMap.get(this.getKey(frameId, threadId, depth)); 59 | if (!vars) { 60 | vars = []; 61 | this.variableMap.set(this.getKey(frameId, threadId, depth), vars); 62 | } 63 | const varobj: VarObjType = { 64 | varname: varCreateResponse.name, expression, numchild: varCreateResponse.numchild, 65 | children: [], value: varCreateResponse.value, type: varCreateResponse.type, isVar, isChild, 66 | }; 67 | vars.push(varobj); 68 | return varobj; 69 | } 70 | 71 | public async removeVar(frameId: number, threadId: number, depth: number, varname: string) 72 | : Promise { 73 | let deleteme: VarObjType | undefined; 74 | const vars = this.variableMap.get(this.getKey(frameId, threadId, depth)); 75 | if (vars) { 76 | for (const varobj of vars) { 77 | if (varobj.varname === varname) { 78 | deleteme = varobj; 79 | break; 80 | } 81 | } 82 | if (deleteme) { 83 | await sendVarDelete(this.gdb, { varname: deleteme.varname }); 84 | vars.splice(vars.indexOf(deleteme), 1); 85 | for (const child of deleteme.children) { 86 | await this.removeVar(frameId, threadId, depth, child.varname); 87 | } 88 | } 89 | } 90 | } 91 | 92 | public async updateVar(frameId: number, threadId: number, depth: number, varobj: VarObjType) 93 | : Promise { 94 | let returnVar = varobj; 95 | const vup = await sendVarUpdate(this.gdb, { threadId, name: varobj.varname }); 96 | const update = vup.changelist[0]; 97 | if (update) { 98 | if (update.in_scope === 'true') { 99 | if (update.name === varobj.varname) { 100 | // don't update the parent value to a child's value 101 | varobj.value = update.value; 102 | } 103 | } else { 104 | this.removeVar(frameId, threadId, depth, varobj.varname); 105 | await sendVarDelete(this.gdb, { varname: varobj.varname }); 106 | const createResponse = await sendVarCreate(this.gdb, 107 | { frame: 'current', expression: varobj.expression }); 108 | returnVar = this.addVar(frameId, threadId, depth, varobj.expression, varobj.isVar, varobj.isChild, 109 | createResponse); 110 | } 111 | } 112 | return Promise.resolve(returnVar); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | # Notices for Eclipse C/C++ Development Tools 2 | 3 | This content is produced and maintained by the Eclipse C/C++ Development Tools 4 | project. 5 | 6 | * Project home: https://projects.eclipse.org/projects/tools.cdt 7 | 8 | ## Trademarks 9 | 10 | Eclipse C/C++ Development Tools, C/C++ Development Tools, Eclipse CDT, and CDT 11 | are trademarks of the Eclipse Foundation. 12 | 13 | ## Copyright 14 | 15 | All content is the property of the respective authors or their employers. For 16 | more information regarding authorship of content, please consult the listed 17 | source code repository logs. 18 | 19 | ## Declared Project Licenses 20 | 21 | This program and the accompanying materials are made available under the terms 22 | of the Eclipse Public License v. 2.0 which is available at 23 | http://www.eclipse.org/legal/epl-2.0. 24 | 25 | SPDX-License-Identifier: EPL-2.0 26 | 27 | ## Source Code 28 | 29 | The project maintains the following source code repositories: 30 | 31 | * https://git.eclipse.org/r/plugins/gitiles/cdt/org.eclipse.cdt 32 | * https://git.eclipse.org/r/plugins/gitiles/cdt/org.eclipse.cdt.edc 33 | * https://git.eclipse.org/r/plugins/gitiles/cdt/org.eclipse.cdt.master 34 | * https://git.eclipse.org/r/plugins/gitiles/cdt/org.eclipse.launchbar 35 | * https://git.eclipse.org/r/plugins/gitiles/cdt/org.eclipse.tools.templates 36 | * https://github.com/eclipse-cdt/cdt-gdb-adapter 37 | * https://github.com/eclipse-cdt/cdt-gdb-vscode 38 | * https://github.com/eclipse-cdt/cdt-vscode 39 | * https://github.com/eclipse-cdt/cdt-infra 40 | 41 | ## Third-party Content 42 | 43 | This project leverages the following third party content. 44 | 45 | acorn (2.6.4) 46 | 47 | * License: MIT License 48 | 49 | antlr (4.5.1) 50 | 51 | * License: New BSD license 52 | * Project: antlr.org 53 | * Source: 54 | http://search.maven.org/remotecontent?filepath=org/antlr/antlr4-runtime/4.5.1-1/antlr4-runtime-4.5.1-1-sources.jar 55 | 56 | Apache Commons Compress (1.6) 57 | 58 | * License: Apache License 2.0 59 | 60 | Apache Commons Lang (3.1.0) 61 | 62 | * License: Apache License, 2.0 63 | 64 | freemarker (2.3.22) 65 | 66 | * License: Apache License, 2.0 67 | * Project: http://freemarker.org/ 68 | * Source: 69 | http://search.maven.org/remotecontent?filepath=org/freemarker/freemarker/2.3.22/freemarker-2.3.22-sources.jar 70 | 71 | Google Gson (2.2.4) 72 | 73 | * License: Apache License 2.0 74 | 75 | Google Guava (15.0.0) 76 | 77 | * License: Apache License, 2.0 78 | 79 | hamcrest - all (1.1) 80 | 81 | * License: New BSD license 82 | 83 | Ispell English Word Lists (a zip file with word lists): (3.1.20) 84 | 85 | * License: iSpell Open Source License (based on Apache 1.1) 86 | 87 | Jabsorb (1.3.1) 88 | 89 | * License: Apache License, 2.0 + JSON License 90 | 91 | jcl104-over-slf4j (1.4.3) 92 | 93 | * License: Apache License, 2.0 94 | 95 | log4j (1.2.15) 96 | 97 | * License: Apache License, 2.0 98 | 99 | LPG parser generator (1.1) 100 | 101 | * License: Eclipse Public License 102 | 103 | meson.json SHA 550c704 (n/a) 104 | 105 | * License: Apache-2.0 106 | * Project: https://github.com/TingPing/language-meson 107 | * Source: 108 | https://github.com/TingPing/language-meson/blob/master/grammars/meson.json 109 | 110 | ninja.tmLanguage SHA 250e7af (n/a) 111 | 112 | * License: LicenseRef-Php_Tmbundle 113 | * Project: https://github.com/textmate/ninja.tmbundle 114 | * Source: 115 | https://github.com/textmate/ninja.tmbundle/blob/master/Syntaxes/Ninja.tmLanguage 116 | 117 | node-bindings (n/a) 118 | 119 | * License: MIT 120 | 121 | node-tar (n/a) 122 | 123 | * License: MIT AND ISC 124 | * Source: https://github.com/npm/node-tar 125 | 126 | react (16.8) 127 | 128 | * License: MIT 129 | 130 | request (n/a) 131 | 132 | * License: MIT AND BSD-3-Clause AND Apache-2.0 AND BSD-2-Clause AND 133 | Public-Domain AND ISC AND X11 AND Unlicense AND BSD-2-Clause-FreeBSD AND 134 | (AFL-2.1 OR BSD-3-Clause) AND (GPL-2.0 OR MIT) AND MPL-2.0 135 | * Source: https://github.com/request/request 136 | 137 | slf4j log4j12 (1.7.2) 138 | 139 | * License: MIT License 140 | 141 | slf4j-api (1.5.6) 142 | 143 | * License: MIT license, + MIT License with no endorsement clause 144 | 145 | slf4j-log4j12 (1.5.6) 146 | 147 | * License: MIT license, + MIT License with no endorsement clause 148 | 149 | tern (0.16.0) 150 | 151 | * License: MIT License 152 | 153 | TrueZip (6.6) 154 | 155 | * License: Apache License, 2.0 156 | 157 | vscode-debugadapter (n/a) 158 | 159 | * License: MIT 160 | 161 | vscode-languageclient (5.3.0) 162 | 163 | * License: MIT AND ISC 164 | 165 | winpty Version: 10/2013 *Subset* (n/a) 166 | 167 | * License: MIT License 168 | 169 | winpty Version: 12/2013 (n/a) 170 | 171 | * License: MIT License 172 | 173 | ## Cryptography 174 | 175 | Content may contain encryption software. The country in which you are currently 176 | may have restrictions on the import, possession, and use, and/or re-export to 177 | another country, of encryption software. BEFORE using any encryption software, 178 | please check the country's laws, regulations and policies concerning the import, 179 | possession, or use, and re-export of encryption software, to see if this is 180 | permitted. 181 | -------------------------------------------------------------------------------- /src/integration-tests/diassemble.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 Ericsson and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import { expect } from 'chai'; 12 | import * as path from 'path'; 13 | import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; 14 | import { LaunchRequestArguments } from '../GDBDebugSession'; 15 | import { CdtDebugClient } from './debugClient'; 16 | import { gdbPath, openGdbConsole, standardBeforeEach, testProgramsDir } from './utils'; 17 | 18 | // Allow non-arrow functions: https://mochajs.org/#arrow-functions 19 | // tslint:disable:only-arrow-functions 20 | describe('Disassembly Test Suite', function() { 21 | 22 | let dc: CdtDebugClient; 23 | let frame: DebugProtocol.StackFrame; 24 | const disProgram = path.join(testProgramsDir, 'disassemble'); 25 | const disSrc = path.join(testProgramsDir, 'disassemble.c'); 26 | 27 | beforeEach(async function() { 28 | dc = await standardBeforeEach(); 29 | 30 | await dc.hitBreakpoint({ 31 | gdb: gdbPath, 32 | program: disProgram, 33 | openGdbConsole, 34 | } as LaunchRequestArguments, { 35 | path: disSrc, 36 | line: 2, 37 | }); 38 | const threads = await dc.threadsRequest(); 39 | // On windows additional threads can exist to handle signals, therefore find 40 | // the real thread & frame running the user code. The other thread will 41 | // normally be running code from ntdll or similar. 42 | loop_threads: 43 | for (const thread of threads.body.threads) { 44 | const stack = await dc.stackTraceRequest({ threadId: thread.id }); 45 | if (stack.body.stackFrames.length >= 1) { 46 | for (const f of stack.body.stackFrames) { 47 | if (f.source && f.source.name === 'disassemble.c') { 48 | frame = f; 49 | break loop_threads; 50 | } 51 | } 52 | } 53 | } 54 | // Make sure we found the expected frame 55 | expect(frame).not.eq(undefined); 56 | }); 57 | 58 | afterEach(async function() { 59 | await dc.stop(); 60 | }); 61 | 62 | it('can disassemble', async function() { 63 | const disassemble = (await dc.send('disassemble', { 64 | memoryReference: 'main', 65 | instructionCount: 100, 66 | })) as DebugProtocol.DisassembleResponse; 67 | expect(disassemble).not.eq(undefined); 68 | expect(disassemble.body).not.eq(undefined); 69 | if (disassemble.body) { 70 | const instructions = disassemble.body.instructions; 71 | expect(instructions).to.have.lengthOf(100); 72 | // the contents of the instructions are platform dependent, so instead 73 | // make sure we have read fully 74 | for (const i of instructions) { 75 | expect(i.address).to.have.lengthOf.greaterThan(0); 76 | expect(i.instructionBytes).to.have.lengthOf.greaterThan(0); 77 | expect(i.instruction).to.have.lengthOf.greaterThan(0); 78 | } 79 | } 80 | }); 81 | 82 | it('can disassemble with no source references', async function() { 83 | // In this case we attempt to read from where there is no source, 84 | // GDB returns data in a different format in that case 85 | const disassemble = (await dc.send('disassemble', { 86 | memoryReference: 'main+1000', 87 | instructionCount: 100, 88 | })) as DebugProtocol.DisassembleResponse; 89 | expect(disassemble).not.eq(undefined); 90 | expect(disassemble.body).not.eq(undefined); 91 | if (disassemble.body) { 92 | const instructions = disassemble.body.instructions; 93 | expect(instructions).to.have.lengthOf(100); 94 | // the contents of the instructions are platform dependent, so instead 95 | // make sure we have read fully 96 | for (const i of instructions) { 97 | expect(i.address).to.have.lengthOf.greaterThan(0); 98 | expect(i.instructionBytes).to.have.lengthOf.greaterThan(0); 99 | expect(i.instruction).to.have.lengthOf.greaterThan(0); 100 | } 101 | } 102 | }); 103 | 104 | it('can handle disassemble at bad address', async function() { 105 | const disassemble = (await dc.send('disassemble', { 106 | memoryReference: '0x0', 107 | instructionCount: 10, 108 | })) as DebugProtocol.DisassembleResponse; 109 | expect(disassemble).not.eq(undefined); 110 | expect(disassemble.body).not.eq(undefined); 111 | if (disassemble.body) { 112 | const instructions = disassemble.body.instructions; 113 | expect(instructions).to.have.lengthOf(10); 114 | // the contens of the instructions are platform dependent, so instead 115 | // make sure we have read fully 116 | for (const i of instructions) { 117 | expect(i.address).to.have.lengthOf.greaterThan(0); 118 | expect(i.instruction).to.have.lengthOf.greaterThan(0); 119 | expect(i.instructionBytes).eq(undefined); 120 | } 121 | } 122 | }); 123 | 124 | }); 125 | -------------------------------------------------------------------------------- /src/integration-tests/mem.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 Ericsson and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import { expect } from 'chai'; 12 | import * as path from 'path'; 13 | import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; 14 | import { LaunchRequestArguments, MemoryResponse } from '../GDBDebugSession'; 15 | import { CdtDebugClient } from './debugClient'; 16 | import { expectRejection, gdbPath, openGdbConsole, standardBeforeEach, testProgramsDir } from './utils'; 17 | 18 | // Allow non-arrow functions: https://mochajs.org/#arrow-functions 19 | // tslint:disable:only-arrow-functions 20 | describe('Memory Test Suite', function() { 21 | 22 | let dc: CdtDebugClient; 23 | let frame: DebugProtocol.StackFrame; 24 | const memProgram = path.join(testProgramsDir, 'mem'); 25 | const memSrc = path.join(testProgramsDir, 'mem.c'); 26 | 27 | beforeEach(async function() { 28 | dc = await standardBeforeEach(); 29 | 30 | await dc.hitBreakpoint({ 31 | gdb: gdbPath, 32 | program: memProgram, 33 | openGdbConsole, 34 | } as LaunchRequestArguments, { 35 | path: memSrc, 36 | line: 12, 37 | }); 38 | const threads = await dc.threadsRequest(); 39 | // On windows additional threads can exist to handle signals, therefore find 40 | // the real thread & frame running the user code. The other thread will 41 | // normally be running code from ntdll or similar. 42 | loop_threads: 43 | for (const thread of threads.body.threads) { 44 | const stack = await dc.stackTraceRequest({ threadId: thread.id }); 45 | if (stack.body.stackFrames.length >= 1) { 46 | for (const f of stack.body.stackFrames) { 47 | if (f.source && f.source.name === 'mem.c') { 48 | frame = f; 49 | break loop_threads; 50 | } 51 | } 52 | } 53 | } 54 | // Make sure we found the expected frame 55 | expect(frame).not.eq(undefined); 56 | }); 57 | 58 | afterEach(async function() { 59 | await dc.stop(); 60 | }); 61 | 62 | /** 63 | * Verify that `resp` contains the bytes `expectedBytes` and the 64 | * `expectedAddress` start address. 65 | * 66 | * `expectedAddress` should be an hexadecimal string, with the leading 0x. 67 | */ 68 | function verifyMemoryReadResult(resp: MemoryResponse, expectedBytes: string, expectedAddress: number) { 69 | expect(resp.body.data).eq(expectedBytes); 70 | expect(resp.body.address).match(/^0x[0-9a-fA-F]+$/); 71 | 72 | const actualAddress = parseInt(resp.body.address, 16); 73 | expect(actualAddress).eq(expectedAddress); 74 | } 75 | 76 | // Test reading memory using cdt-gdb-adapter's extension request. 77 | it('can read memory', async function() { 78 | // Get the address of the array. 79 | const addrOfArrayResp = await dc.evaluateRequest({ expression: '&array', frameId: frame.id }); 80 | const addrOfArray = parseInt(addrOfArrayResp.body.result, 16); 81 | 82 | let mem = (await dc.send('cdt-gdb-adapter/Memory', { 83 | address: '0x' + addrOfArray.toString(16), 84 | length: 10, 85 | })) as MemoryResponse; 86 | 87 | verifyMemoryReadResult(mem, 'f1efd4fd7248450c2d13', addrOfArray); 88 | 89 | mem = (await dc.send('cdt-gdb-adapter/Memory', { 90 | address: '&array[3 + 2]', 91 | length: 10, 92 | })) as MemoryResponse; 93 | 94 | verifyMemoryReadResult(mem, '48450c2d1374d6f612dc', addrOfArray + 5); 95 | 96 | mem = (await dc.send('cdt-gdb-adapter/Memory', { 97 | address: 'parray', 98 | length: 10, 99 | })) as MemoryResponse; 100 | 101 | verifyMemoryReadResult(mem, 'f1efd4fd7248450c2d13', addrOfArray); 102 | }); 103 | 104 | it('handles unable to read memory', async function() { 105 | // This test will only work for targets for which address 0 is not readable, which is good enough for now. 106 | const err = await expectRejection(dc.send('cdt-gdb-adapter/Memory', { 107 | address: '0', 108 | length: 10, 109 | })); 110 | expect(err.message).contains('Unable to read memory'); 111 | }); 112 | 113 | it('can read memory with offset', async function() { 114 | const addrOfArrayResp = await dc.evaluateRequest({ expression: '&array', frameId: frame.id }); 115 | const addrOfArray = parseInt(addrOfArrayResp.body.result, 16); 116 | 117 | // Test positive offset 118 | let offset = 5; 119 | let mem = (await dc.send('cdt-gdb-adapter/Memory', { 120 | address: '&array', 121 | length: 5, 122 | offset, 123 | })) as MemoryResponse; 124 | 125 | verifyMemoryReadResult(mem, '48450c2d13', addrOfArray + offset); 126 | 127 | // Test negative offset 128 | offset = -5; 129 | mem = (await dc.send('cdt-gdb-adapter/Memory', { 130 | address: `array + ${-offset}`, 131 | length: 10, 132 | offset, 133 | })) as MemoryResponse; 134 | 135 | verifyMemoryReadResult(mem, 'f1efd4fd7248450c2d13', addrOfArray); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /src/integration-tests/vars_cpp.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import { expect } from 'chai'; 12 | import * as path from 'path'; 13 | import { LaunchRequestArguments } from '..'; 14 | import { CdtDebugClient } from './debugClient'; 15 | import { 16 | compareVariable, gdbPath, getScopes, openGdbConsole, resolveLineTagLocations, Scope, 17 | standardBeforeEach, testProgramsDir, verifyVariable, 18 | } from './utils'; 19 | 20 | // Allow non-arrow functions: https://mochajs.org/#arrow-functions 21 | // tslint:disable:only-arrow-functions 22 | 23 | describe('Variables CPP Test Suite', function() { 24 | 25 | let dc: CdtDebugClient; 26 | let scope: Scope; 27 | 28 | const varsCppProgram = path.join(testProgramsDir, 'vars_cpp'); 29 | const varsCppSrc = path.join(testProgramsDir, 'vars_cpp.cpp'); 30 | 31 | const lineTags = { 32 | 'STOP HERE': 0, 33 | }; 34 | 35 | before(function() { 36 | resolveLineTagLocations(varsCppSrc, lineTags); 37 | }); 38 | 39 | beforeEach(async function() { 40 | dc = await standardBeforeEach(); 41 | 42 | await dc.hitBreakpoint({ 43 | verbose: true, 44 | gdb: gdbPath, 45 | program: varsCppProgram, 46 | openGdbConsole, 47 | } as LaunchRequestArguments, { 48 | path: varsCppSrc, 49 | line: lineTags['STOP HERE'], 50 | }); 51 | scope = await getScopes(dc); 52 | expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); 53 | }); 54 | 55 | afterEach(async function() { 56 | await dc.stop(); 57 | }); 58 | 59 | // Move the timeout out of the way if the adapter is going to be debugged. 60 | if (process.env.INSPECT_DEBUG_ADAPTER) { 61 | this.timeout(9999999); 62 | } 63 | 64 | it('can read and set a cpp object variable', async function() { 65 | // check the initial conditions of the two variables 66 | const vr = scope.scopes.body.scopes[0].variablesReference; 67 | const vars = await dc.variablesRequest({ variablesReference: vr }); 68 | expect(vars.body.variables.length, 'There is a different number of variables than expected').to.equal(3); 69 | verifyVariable(vars.body.variables[0], 'fooA', 'Foo *', undefined, true); 70 | verifyVariable(vars.body.variables[1], 'fooB', 'Foo *', undefined, true); 71 | expect(vars.body.variables[0].value, 'Value of fooA matches fooB').to.not.equal(vars.body.variables[1].value); 72 | // check that the children names and values are the same, but values are different 73 | let childrenA = await dc.variablesRequest({ variablesReference: vars.body.variables[0].variablesReference }); 74 | let childrenB = await dc.variablesRequest({ variablesReference: vars.body.variables[1].variablesReference }); 75 | expect( 76 | childrenA.body.variables.length, 77 | 'There is a different number of child variables than expected', 78 | ).to.equal(childrenB.body.variables.length); 79 | compareVariable(childrenA.body.variables[0], childrenB.body.variables[0], true, true, false); 80 | compareVariable(childrenA.body.variables[1], childrenB.body.variables[1], true, true, false); 81 | compareVariable(childrenA.body.variables[2], childrenB.body.variables[2], true, true, false); 82 | // set fooA to be equal to fooB. 83 | await dc.setVariableRequest({ name: 'fooA', value: vars.body.variables[1].value, variablesReference: vr }); 84 | // check types and value after the set 85 | const vars2 = await dc.variablesRequest({ variablesReference: vr }); 86 | expect(vars2.body.variables.length, 'There is a different number of variables than expected').to.equal(3); 87 | compareVariable(vars2.body.variables[0], vars2.body.variables[1], false, true, true); 88 | // check the objects are identical 89 | childrenA = await dc.variablesRequest({ variablesReference: vars2.body.variables[0].variablesReference }); 90 | childrenB = await dc.variablesRequest({ variablesReference: vars2.body.variables[1].variablesReference }); 91 | compareVariable(childrenA.body.variables[0], childrenB.body.variables[0], true, true, true); 92 | compareVariable(childrenA.body.variables[1], childrenB.body.variables[1], true, true, true); 93 | compareVariable(childrenA.body.variables[2], childrenB.body.variables[2], true, true, true); 94 | }); 95 | 96 | it('can read and set nested variables from a cpp object', async function() { 97 | // check initial conditions of fooA and its child elements 98 | const vr = scope.scopes.body.scopes[0].variablesReference; 99 | const vars = await dc.variablesRequest({ variablesReference: vr }); 100 | expect(vars.body.variables.length, 'There is a different number of variables than expected').to.equal(3); 101 | verifyVariable(vars.body.variables[0], 'fooA', 'Foo *', undefined, true); 102 | expect( 103 | vars.body.variables[0].variablesReference, 104 | `${vars.body.variables[0].name} has no children`, 105 | ).to.not.equal(0); 106 | const childVR = vars.body.variables[0].variablesReference; 107 | let children = await dc.variablesRequest({ variablesReference: childVR }); 108 | expect( 109 | children.body.variables.length, 110 | 'There is a different number of child variables than expected', 111 | ).to.equal(3); 112 | verifyVariable(children.body.variables[0], 'a', 'int', '1'); 113 | verifyVariable(children.body.variables[1], 'c', 'char', '97 \'a\''); 114 | verifyVariable(children.body.variables[2], 'b', 'int', '2'); 115 | // set child value 116 | await dc.setVariableRequest({ 117 | name: children.body.variables[0].name, 118 | value: '55', 119 | variablesReference: vars.body.variables[0].variablesReference, 120 | }); 121 | // check the new values 122 | children = await dc.variablesRequest({ variablesReference: vars.body.variables[0].variablesReference }); 123 | expect( 124 | children.body.variables.length, 125 | 'There is a different number of child variables than expected', 126 | ).to.equal(3); 127 | verifyVariable(children.body.variables[0], 'a', 'int', '55'); 128 | // these two values should be unchanged. 129 | verifyVariable(children.body.variables[1], 'c', 'char', '97 \'a\''); 130 | verifyVariable(children.body.variables[2], 'b', 'int', '2'); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /src/GDBBackend.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import { execFile, spawn, ChildProcess } from 'child_process'; 11 | import * as events from 'events'; 12 | import { Writable } from 'stream'; 13 | import { logger } from 'vscode-debugadapter/lib/logger'; 14 | import { AttachRequestArguments, LaunchRequestArguments } from './GDBDebugSession'; 15 | import { MIResponse } from './mi'; 16 | import { MIParser } from './MIParser'; 17 | import { VarManager } from './varManager'; 18 | 19 | export interface MIExecNextRequest { 20 | reverse?: boolean; 21 | } 22 | 23 | export interface MIExecNextResponse extends MIResponse { 24 | } 25 | 26 | export interface MIGDBShowResponse extends MIResponse { 27 | value?: string; 28 | } 29 | 30 | export declare interface GDBBackend { 31 | on(event: 'consoleStreamOutput', listener: (output: string, category: string) => void): this; 32 | on(event: 'execAsync' | 'notifyAsync' | 'statusAsync', listener: (asyncClass: string, data: any) => void): this; 33 | 34 | emit(event: 'consoleStreamOutput', output: string, category: string): boolean; 35 | emit(event: 'execAsync' | 'notifyAsync' | 'statusAsync', asyncClass: string, data: any): boolean; 36 | } 37 | 38 | export class GDBBackend extends events.EventEmitter { 39 | protected parser = new MIParser(this); 40 | protected varMgr = new VarManager(this); 41 | protected out?: Writable; 42 | protected token = 0; 43 | protected proc?: ChildProcess; 44 | 45 | get varManager(): VarManager { 46 | return this.varMgr; 47 | } 48 | 49 | public spawn(requestArgs: LaunchRequestArguments | AttachRequestArguments) { 50 | const gdb = requestArgs.gdb ? requestArgs.gdb : 'gdb'; 51 | let args = ['--interpreter=mi2']; 52 | if (requestArgs.gdbArguments) { 53 | args = args.concat(requestArgs.gdbArguments); 54 | } 55 | this.proc = spawn(gdb, args); 56 | this.out = this.proc.stdin; 57 | return this.parser.parse(this.proc.stdout); 58 | } 59 | 60 | public async spawnInClientTerminal(requestArgs: LaunchRequestArguments | AttachRequestArguments, 61 | cb: (args: string[]) => Promise) { 62 | const gdb = requestArgs.gdb ? requestArgs.gdb : 'gdb'; 63 | // Use dynamic import to remove need for natively building this adapter 64 | // Useful when 'spawnInClientTerminal' isn't needed, but adapter is distributed on multiple OS's 65 | const { Pty } = await import('./native/pty'); 66 | const pty = new Pty(); 67 | let args = [gdb, '-ex', `new-ui mi2 ${pty.name}`]; 68 | if (requestArgs.gdbArguments) { 69 | args = args.concat(requestArgs.gdbArguments); 70 | } 71 | await cb(args); 72 | this.out = pty.master; 73 | return this.parser.parse(pty.master); 74 | } 75 | 76 | public pause() { 77 | if (this.proc) { 78 | this.proc.kill('SIGINT'); 79 | return true; 80 | } else { 81 | return false; 82 | } 83 | } 84 | 85 | public async supportsNewUi(gdbPath?: string): Promise { 86 | const gdb = gdbPath || 'gdb'; 87 | return new Promise((resolve, reject) => { 88 | execFile(gdb, ['-nx', '-batch', '-ex', 'new-ui'], (error, stdout, stderr) => { 89 | // - gdb > 8.2 outputs 'Usage: new-ui INTERPRETER TTY' 90 | // - gdb 7.12 to 8.2 outputs 'usage: new-ui ' 91 | // - gdb < 7.12 doesn't support the new-ui command, and outputs 92 | // 'Undefined command: "new-ui". Try "help".' 93 | resolve(/^usage: new-ui/im.test(stderr)); 94 | }); 95 | }); 96 | } 97 | 98 | public async sendCommands(commands?: string[]) { 99 | if (commands) { 100 | for (const command of commands) { 101 | await this.sendCommand(command); 102 | } 103 | } 104 | } 105 | 106 | public sendCommand(command: string): Promise { 107 | const token = this.nextToken(); 108 | logger.verbose(`GDB command: ${token} ${command}`); 109 | return new Promise((resolve, reject) => { 110 | if (this.out) { 111 | this.parser.queueCommand(token, (resultClass, resultData) => { 112 | switch (resultClass) { 113 | case 'done': 114 | case 'running': 115 | case 'connected': 116 | case 'exit': 117 | resolve(resultData); 118 | break; 119 | case 'error': 120 | reject(new Error(resultData.msg)); 121 | break; 122 | default: 123 | reject(new Error(`Unknown response ${resultClass}: ${JSON.stringify(resultData)}`)); 124 | } 125 | }); 126 | this.out.write(`${token}${command}\n`); 127 | } else { 128 | reject(new Error('gdb is not running.')); 129 | } 130 | }); 131 | } 132 | 133 | public sendEnablePrettyPrint() { 134 | return this.sendCommand('-enable-pretty-printing'); 135 | } 136 | 137 | // Rewrite the argument escaping whitespace, quotes and backslash 138 | public standardEscape(arg: string): string { 139 | let result = ''; 140 | for (const char of arg) { 141 | if (char === '\\' || char === '"') { 142 | result += '\\'; 143 | } 144 | result += char; 145 | } 146 | result = `"${result}"`; 147 | return result; 148 | } 149 | 150 | public sendFileExecAndSymbols(program: string) { 151 | return this.sendCommand(`-file-exec-and-symbols ${this.standardEscape(program)}`); 152 | } 153 | 154 | public sendFileSymbolFile(symbols: string) { 155 | return this.sendCommand(`-file-symbol-file ${this.standardEscape(symbols)}`); 156 | } 157 | 158 | public sendAddSymbolFile(symbols: string, offset: string) { 159 | return this.sendCommand(`add-symbol-file ${this.standardEscape(symbols)} ${offset}`); 160 | } 161 | 162 | public sendLoad(imageFileName: string, imageOffset: string | undefined) { 163 | return this.sendCommand(`load ${this.standardEscape(imageFileName)} ${imageOffset || ''}`); 164 | } 165 | 166 | public sendGDBSet(params: string) { 167 | return this.sendCommand(`-gdb-set ${params}`); 168 | } 169 | 170 | public sendGDBShow(params: string): Promise { 171 | return this.sendCommand(`-gdb-show ${params}`); 172 | } 173 | 174 | public sendGDBExit() { 175 | return this.sendCommand('-gdb-exit'); 176 | } 177 | 178 | protected nextToken() { 179 | return this.token++; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/integration-tests/functionBreakpoints.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2019 Arm and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import * as path from 'path'; 12 | import { expect } from 'chai'; 13 | import { join } from 'path'; 14 | import { CdtDebugClient } from './debugClient'; 15 | import { LaunchRequestArguments } from '../GDBDebugSession'; 16 | import { 17 | standardBeforeEach, 18 | gdbPath, 19 | testProgramsDir, 20 | openGdbConsole, 21 | getScopes, 22 | } from './utils'; 23 | 24 | describe('function breakpoints', async () => { 25 | let dc: CdtDebugClient; 26 | 27 | beforeEach(async () => { 28 | dc = await standardBeforeEach(); 29 | 30 | await dc.launchRequest({ 31 | verbose: true, 32 | gdb: gdbPath, 33 | program: join(testProgramsDir, 'functions'), 34 | openGdbConsole, 35 | } as LaunchRequestArguments); 36 | }); 37 | 38 | afterEach(async () => { 39 | await dc.stop(); 40 | }); 41 | 42 | it('hits the main function breakpoint', async () => { 43 | const bpResp = await dc.setFunctionBreakpointsRequest({ 44 | breakpoints: [ 45 | { 46 | name: 'main', 47 | }, 48 | ], 49 | }); 50 | expect(bpResp.body.breakpoints.length).eq(1); 51 | expect(bpResp.body.breakpoints[0].verified).eq(true); 52 | expect(bpResp.body.breakpoints[0].message).eq(undefined); 53 | await dc.configurationDoneRequest(); 54 | await dc.assertStoppedLocation('function breakpoint', { line: 14 }); 55 | }); 56 | 57 | it('hits the sub function breakpoint', async () => { 58 | await dc.setFunctionBreakpointsRequest({ 59 | breakpoints: [ 60 | { 61 | name: 'sub', 62 | }, 63 | ], 64 | }); 65 | await dc.configurationDoneRequest(); 66 | await dc.assertStoppedLocation('function breakpoint', { line: 10 }); 67 | }); 68 | 69 | it('handles responses (e.g. multiple static functions with same name)', async () => { 70 | await dc.setFunctionBreakpointsRequest({ 71 | breakpoints: [ 72 | { 73 | name: 'staticfunc1', 74 | }, 75 | ], 76 | }); 77 | await dc.configurationDoneRequest(); 78 | await dc.assertStoppedLocation('function breakpoint', { line: 3, path: /functions.c$/ }); 79 | const scope = await getScopes(dc); 80 | await dc.continue({ threadId: scope.thread.id }, 81 | 'function breakpoint', 82 | { line: 2, path: /functions_other.c$/ }); 83 | }); 84 | 85 | it('handles function changes', async () => { 86 | await dc.setFunctionBreakpointsRequest({ 87 | breakpoints: [ 88 | { 89 | name: 'staticfunc1', 90 | }, 91 | ], 92 | }); 93 | await dc.setFunctionBreakpointsRequest({ 94 | breakpoints: [ 95 | { 96 | name: 'staticfunc2', 97 | }, 98 | ], 99 | }); 100 | await dc.configurationDoneRequest(); 101 | await dc.assertStoppedLocation('function breakpoint', { line: 6, path: /functions.c$/ }); 102 | const scope = await getScopes(dc); 103 | await dc.continue({ threadId: scope.thread.id }, 104 | 'function breakpoint', 105 | { line: 5, path: /functions_other.c$/ }); 106 | }); 107 | 108 | it('handles mixed with line breakpoints', async () => { 109 | await dc.setFunctionBreakpointsRequest({ 110 | breakpoints: [ 111 | { 112 | name: 'staticfunc1', 113 | }, 114 | ], 115 | }); 116 | 117 | // This a regression test as this did not lead to an error back to 118 | // the user, but did mean that the adapter was trying to do: 119 | // -break-delete 1.1 1.2 120 | // which gets a warning back from GDB: 121 | // warning: bad breakpoint number at or near '0' 122 | let logOutput = ''; 123 | dc.on('output', (e) => { 124 | if (e.body.category === 'log') { 125 | logOutput += e.body.output; 126 | } 127 | }); 128 | await dc.setBreakpointsRequest({ 129 | source: { 130 | name: 'functions.c', 131 | path: path.join(testProgramsDir, 'functions.c'), 132 | }, 133 | breakpoints: [ 134 | { 135 | column: 1, 136 | line: 14, 137 | }, 138 | ], 139 | }); 140 | expect(logOutput).does.not.contain('warning'); 141 | await dc.configurationDoneRequest(); 142 | await dc.assertStoppedLocation('breakpoint', { line: 14, path: /functions.c$/ }); 143 | }); 144 | 145 | it('fails gracefully on unknown function', async () => { 146 | const bpResp = await dc.setFunctionBreakpointsRequest({ 147 | breakpoints: [ 148 | { 149 | name: 'mainBAD', 150 | }, 151 | ], 152 | }); 153 | expect(bpResp.body.breakpoints.length).eq(1); 154 | expect(bpResp.body.breakpoints[0].verified).eq(false); 155 | expect(bpResp.body.breakpoints[0].message).not.eq(undefined); 156 | }); 157 | 158 | it('maintains breakpoint order when modifying function breakpoints', async () => { 159 | const bpResp1 = await dc.setFunctionBreakpointsRequest({ 160 | breakpoints: [ 161 | { 162 | name: 'main', 163 | }, 164 | ], 165 | }); 166 | const bpResp2 = await dc.setFunctionBreakpointsRequest({ 167 | breakpoints: [ 168 | { 169 | name: 'sub', 170 | }, 171 | { 172 | name: 'main', 173 | }, 174 | ], 175 | }); 176 | // Unlike with line breakpoints, function breakpoints don't 177 | // really report back anything other than the ID that can 178 | // be used to check order is maintained 179 | expect(bpResp2.body.breakpoints[1].id).eq(bpResp1.body.breakpoints[0].id); 180 | expect(bpResp2.body.breakpoints[0].id).not.eq(bpResp1.body.breakpoints[0].id); 181 | }); 182 | 183 | it('deletes breakpoints in gdb when removed in IDE', async () => { 184 | await dc.setFunctionBreakpointsRequest({ 185 | breakpoints: [ 186 | { 187 | name: 'sub', 188 | }, 189 | { 190 | name: 'main', 191 | }, 192 | ], 193 | }); 194 | await dc.setFunctionBreakpointsRequest({ 195 | breakpoints: [ 196 | { 197 | name: 'sub', 198 | }, 199 | ], 200 | }); 201 | await dc.configurationDoneRequest(); 202 | await dc.assertStoppedLocation('function breakpoint', { line: 10 }); 203 | }); 204 | 205 | }); 206 | -------------------------------------------------------------------------------- /src/integration-tests/utils.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 Ericsson and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import { expect } from 'chai'; 12 | import * as cp from 'child_process'; 13 | import * as fs from 'fs'; 14 | import * as path from 'path'; 15 | import { DebugProtocol } from 'vscode-debugprotocol'; 16 | import { CdtDebugClient } from './debugClient'; 17 | 18 | export interface Scope { 19 | thread: DebugProtocol.Thread; 20 | frame: DebugProtocol.StackFrame; 21 | scopes: DebugProtocol.ScopesResponse; 22 | } 23 | 24 | export async function getScopes( 25 | dc: CdtDebugClient, 26 | threadIndex = 0, 27 | stackIndex = 0, 28 | ): Promise { 29 | // threads 30 | const threads = await dc.threadsRequest(); 31 | expect(threads.body.threads.length, 'There are fewer threads than expected.').to.be.at.least(threadIndex + 1); 32 | const thread = threads.body.threads[threadIndex]; 33 | const threadId = thread.id; 34 | // stack trace 35 | const stack = await dc.stackTraceRequest({ threadId }); 36 | expect(stack.body.stackFrames.length, 'There are fewer stack frames than expected.').to.be.at.least(stackIndex + 1); 37 | const frame = stack.body.stackFrames[stackIndex]; 38 | const frameId = frame.id; 39 | const scopes = await dc.scopesRequest({ frameId }); 40 | return Promise.resolve({ thread, frame, scopes }); 41 | } 42 | 43 | /** 44 | * Wrap `promise` in a new Promise that resolves if `promise` is rejected, and is rejected if `promise` is resolved. 45 | * 46 | * This is useful when we expect `promise` to be reject and want to test that it is indeed the case. 47 | */ 48 | export function expectRejection(promise: Promise): Promise { 49 | return new Promise((resolve, reject) => { 50 | promise.then(reject).catch(resolve); 51 | }); 52 | } 53 | 54 | /** 55 | * Test a given variable returned from a variablesRequest against an expected name, type, and/or value. 56 | */ 57 | export function verifyVariable( 58 | variable: DebugProtocol.Variable, 59 | expectedName: string, 60 | expectedType?: string, 61 | expectedValue?: string, 62 | hasChildren = false, 63 | ) { 64 | expect(variable.name, `The name of ${expectedName} is wrong`).to.equal(expectedName); 65 | if (expectedType) { 66 | expect(variable.type, `The type of ${expectedName} is wrong`).to.equal(expectedType); 67 | } 68 | if (expectedValue) { 69 | expect(variable.value, `The value of ${expectedName} is wrong`).to.equal(expectedValue); 70 | } 71 | if (hasChildren) { 72 | expect(variable.variablesReference, `${expectedName} has no children`).to.not.equal(0); 73 | } else { 74 | expect(variable.variablesReference, `${expectedName} has children`).to.equal(0); 75 | } 76 | } 77 | 78 | export function compareVariable( 79 | varA: DebugProtocol.Variable, 80 | varB: DebugProtocol.Variable, 81 | namesMatch: boolean, 82 | typesMatch: boolean, 83 | valuesMatch: boolean, 84 | ) { 85 | if (namesMatch) { 86 | expect(varA.name, `The name of ${varA.name} and ${varB.name} does not match`).to.equal(varB.name); 87 | } else { 88 | expect(varA.name, `The name of ${varA.name} and ${varB.name} matches`).to.not.equal(varB.name); 89 | } 90 | if (typesMatch) { 91 | expect(varA.type, `The type of ${varA.name} and ${varB.name} does not match`).to.equal(varB.type); 92 | } else { 93 | expect(varA.type, `The type of ${varA.name} and ${varB.name} match`).to.equal(varB.type); 94 | } 95 | if (valuesMatch) { 96 | expect(varA.value, `The value of ${varA.name} and ${varB.name} do not match`).to.equal(varB.value); 97 | } else { 98 | expect(varA.value, `The value of ${varA.name} and ${varB.name} matches`).to.not.equal(varB.value); 99 | } 100 | } 101 | 102 | export const testProgramsDir = path.join(__dirname, '..', '..', 'src', 'integration-tests', 'test-programs'); 103 | 104 | // Run make once per mocha execution by having root-level before 105 | before(function(done) { 106 | this.timeout(20000); 107 | cp.execSync('make', { cwd: testProgramsDir }); 108 | done(); 109 | }); 110 | 111 | function getAdapterAndArgs(adapter?: string): string { 112 | const chosenAdapter = adapter !== undefined ? adapter : defaultAdapter; 113 | let args: string = path.join(__dirname, '../../dist', chosenAdapter); 114 | if (process.env.INSPECT_DEBUG_ADAPTER) { 115 | args = '--inspect-brk ' + args; 116 | } 117 | return args; 118 | } 119 | 120 | export async function standardBeforeEach(adapter?: string): Promise { 121 | const dc: CdtDebugClient = new CdtDebugClient('node', getAdapterAndArgs(adapter), 'cppdbg', { 122 | shell: true, 123 | }); 124 | await dc.start(debugServerPort); 125 | await dc.initializeRequest(); 126 | 127 | return dc; 128 | } 129 | 130 | export const openGdbConsole: boolean = process.argv.indexOf('--run-in-terminal') !== -1; 131 | export const gdbPath: string | undefined = getGdbPathCli(); 132 | export const gdbServerPath: string = getGdbServerPathCli(); 133 | export const debugServerPort: number | undefined = getDebugServerPortCli(); 134 | export const defaultAdapter: string = getDefaultAdapterCli(); 135 | 136 | function getGdbPathCli(): string | undefined { 137 | const keyIndex = process.argv.indexOf('--gdb-path'); 138 | if (keyIndex === -1) { 139 | return undefined; 140 | } 141 | return process.argv[keyIndex + 1]; 142 | } 143 | 144 | function getGdbServerPathCli(): string { 145 | const keyIndex = process.argv.indexOf('--gdbserver-path'); 146 | if (keyIndex === -1) { 147 | return 'gdbserver'; 148 | } 149 | return process.argv[keyIndex + 1]; 150 | } 151 | 152 | function getDebugServerPortCli(): number | undefined { 153 | const keyIndex = process.argv.indexOf('--debugserverport'); 154 | if (keyIndex === -1) { 155 | return undefined; 156 | } 157 | return parseInt(process.argv[keyIndex + 1], 10); 158 | } 159 | 160 | function getDefaultAdapterCli(): string { 161 | const keyIndex = process.argv.indexOf('--test-remote'); 162 | if (keyIndex === -1) { 163 | return 'debugAdapter.js'; 164 | } 165 | return 'debugTargetAdapter.js'; 166 | } 167 | 168 | export interface LineTags { [key: string]: number; } 169 | 170 | /** 171 | * Find locations of tags in `sourceFile`. 172 | * 173 | * Instead of referring to source line numbers of test programs directly, 174 | * tests should place tags (usually some comments) in the source files. This 175 | * function finds the line number correspnding to each tag in `tags`. 176 | * 177 | * This function throws if a tag is found more than once or is not found. 178 | * 179 | * @param tags An object where keys are the tags to find, and values are 0. 180 | * This function will modify the object in place to full the values 181 | * with line number. 182 | */ 183 | export function resolveLineTagLocations(sourceFile: string, tags: LineTags): void { 184 | const lines = fs.readFileSync(sourceFile, { encoding: 'utf-8' }).split('\n'); 185 | 186 | for (let i = 0; i < lines.length; i++) { 187 | for (const tag of Object.keys(tags)) { 188 | if (lines[i].includes(tag)) { 189 | if (tags[tag] !== 0) { 190 | throw new Error(`Tag ${tag} has been found twice.`); 191 | } 192 | 193 | tags[tag] = i + 1; 194 | } 195 | } 196 | } 197 | 198 | for (const tag of Object.keys(tags)) { 199 | const line = tags[tag]; 200 | 201 | if (line === 0) { 202 | throw new Error(`Tag ${tag} was not found.`); 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/integration-tests/debugClient.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 Ericsson and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import * as cp from 'child_process'; 11 | import { DebugClient } from 'vscode-debugadapter-testsupport'; 12 | import { DebugProtocol } from 'vscode-debugprotocol'; 13 | 14 | // tslint:disable:no-string-literal 15 | 16 | export type ReverseRequestHandler = 17 | (args: A) => Promise; 18 | export interface ReverseRequestHandlers { 19 | [key: string]: ReverseRequestHandler | undefined; 20 | runInTerminal: ReverseRequestHandler< 21 | DebugProtocol.RunInTerminalRequestArguments, DebugProtocol.RunInTerminalResponse>; 22 | } 23 | 24 | /** 25 | * Extend the DebugClient to support Reverse Requests: 26 | * https://microsoft.github.io/debug-adapter-protocol/specification#Reverse_Requests_RunInTerminal 27 | */ 28 | export class CdtDebugClient extends DebugClient { 29 | 30 | /** 31 | * Reverse Request Handlers: 32 | */ 33 | protected reverseRequestHandlers: ReverseRequestHandlers = { 34 | runInTerminal: async (args) => { 35 | const process = await new Promise((resolve, reject) => { 36 | const child = cp.spawn(args.args[0], args.args.slice(1), { 37 | cwd: args.cwd, 38 | env: sanitizeEnv(args.env), 39 | }); 40 | if (typeof child.pid !== 'undefined') { 41 | return resolve(child); 42 | } 43 | child.once('error', (error) => { 44 | reject(error); 45 | }); 46 | }); 47 | return { 48 | processId: process.pid, 49 | }; 50 | }, 51 | }; 52 | 53 | /** 54 | * Notify the Debug Adapter by default that this client supports `runInTerminal`. 55 | */ 56 | public initializeRequest(args?: DebugProtocol.InitializeRequestArguments) { 57 | if (!args) { 58 | args = { 59 | supportsRunInTerminalRequest: true, 60 | adapterID: this['_debugType'], 61 | linesStartAt1: true, 62 | columnsStartAt1: true, 63 | pathFormat: 'path', 64 | }; 65 | } 66 | return super.initializeRequest(args); 67 | } 68 | 69 | /** 70 | * Send a continueRequest and wait for target to stop 71 | */ 72 | public async continue(args: DebugProtocol.ContinueArguments, reason: string, 73 | expected: { 74 | path?: string | RegExp, line?: number, column?: number, 75 | }): Promise { 76 | const waitForStopped = this.assertStoppedLocation(reason, expected); 77 | const continueResp = this.continueRequest(args); 78 | await Promise.all([waitForStopped, continueResp]); 79 | return waitForStopped; 80 | } 81 | 82 | /** 83 | * Send a nextRequest and wait for target to stop 84 | */ 85 | public async next(args: DebugProtocol.NextArguments, expected: { 86 | path?: string | RegExp, line?: number, column?: number, 87 | }): Promise { 88 | const waitForStopped = this.assertStoppedLocation('step', expected); 89 | const next = this.nextRequest(args); 90 | await Promise.all([waitForStopped, next]); 91 | return waitForStopped; 92 | } 93 | 94 | /** 95 | * Send a stepInRequest and wait for target to stop 96 | */ 97 | public async stepIn(args: DebugProtocol.StepInArguments, expected: { 98 | path?: string | RegExp, line?: number, column?: number, 99 | }): Promise { 100 | const waitForStopped = this.assertStoppedLocation('step', expected); 101 | const next = this.stepInRequest(args); 102 | await Promise.all([waitForStopped, next]); 103 | return waitForStopped; 104 | } 105 | 106 | /** 107 | * Send a stepOutRequest and wait for target to stop 108 | */ 109 | public async stepOut(args: DebugProtocol.StepOutArguments, expected: { 110 | path?: string | RegExp, line?: number, column?: number, 111 | }): Promise { 112 | const waitForStopped = this.assertStoppedLocation('step', expected); 113 | const next = this.stepOutRequest(args); 114 | await Promise.all([waitForStopped, next]); 115 | return waitForStopped; 116 | } 117 | 118 | /** 119 | * Send a stepBackRequest and wait for target to stop 120 | */ 121 | public async stepBack(args: DebugProtocol.StepBackArguments, expected: { 122 | path?: string | RegExp, line?: number, column?: number, 123 | }): Promise { 124 | const waitForStopped = this.assertStoppedLocation('step', expected); 125 | const next = this.stepBackRequest(args); 126 | await Promise.all([waitForStopped, next]); 127 | return waitForStopped; 128 | } 129 | 130 | /* 131 | * Returns a promise that will resolve if an output event with a specific category was received. 132 | * The promise will be rejected if a timeout occurs. 133 | */ 134 | public async waitForOutputEvent(category: string, timeout: number = this.defaultTimeout) 135 | : Promise { 136 | const isOutputEvent = (event: any): event is DebugProtocol.OutputEvent => { 137 | return !!(event as DebugProtocol.OutputEvent).body && !!(event as DebugProtocol.OutputEvent).body.output; 138 | }; 139 | 140 | const timer = setTimeout(() => { 141 | throw new Error(`no output event with category '${category}' received after ${timeout} ms`); 142 | }, timeout); 143 | 144 | while (true) { 145 | const event = await new Promise((resolve) => this.once('output', (e) => resolve(e))); 146 | 147 | if (!isOutputEvent(event)) { 148 | continue; 149 | } 150 | 151 | if (event.body.category === category) { 152 | clearTimeout(timer); 153 | return event; 154 | } 155 | } 156 | } 157 | 158 | /** 159 | * Send a response following a Debug Adapter Reverse Request. 160 | * @param request original request to respond to. 161 | * @param handler processes the request and returns the response body. 162 | */ 163 | private async doRespond(request: DebugProtocol.Request): Promise { 164 | const { command } = request; 165 | const handler: ReverseRequestHandler | undefined = this['reverseRequestHandlers'][command]; 166 | const response: Partial = { 167 | type: 'response', 168 | request_seq: request.seq, 169 | command, 170 | success: true, 171 | }; 172 | if (!handler) { 173 | response.success = false; 174 | response.message = `Unknown command: ${command}`; 175 | } else { 176 | try { 177 | response.body = await handler(request.arguments); 178 | } catch (error) { 179 | response.success = false; 180 | response.message = error instanceof Error ? error.message : error; 181 | } 182 | } 183 | const json = JSON.stringify(response); 184 | this['outputStream'].write(`Content-Length: ${Buffer.byteLength(json, 'utf-8')}\r\n\r\n${json}`); 185 | } 186 | 187 | } 188 | 189 | /** 190 | * DebugProtocol.dispatch is private, overriding manually. 191 | */ 192 | CdtDebugClient.prototype['dispatch'] = function dispatch(raw: any): void { 193 | const message: DebugProtocol.ProtocolMessage = JSON.parse(raw); 194 | if (isRequest(message)) { 195 | this['doRespond'](message); 196 | } else { 197 | DebugClient.prototype['dispatch'].apply(this, [raw]); 198 | } 199 | }; 200 | 201 | function isRequest(message: DebugProtocol.ProtocolMessage): message is DebugProtocol.Request { 202 | return message.type === 'request'; 203 | } 204 | 205 | function sanitizeEnv(env?: { [key: string]: any }): { [key: string]: string } { 206 | if (!env) { 207 | return {}; 208 | } 209 | const sanitized: { [key: string]: string } = {}; 210 | for (const key of Object.keys(env)) { 211 | if (typeof env[key] === 'string') { 212 | sanitized[key] = env[key]; 213 | } 214 | } 215 | return sanitized; 216 | } 217 | -------------------------------------------------------------------------------- /src/MIParser.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | import { Readable } from 'stream'; 11 | import { logger } from 'vscode-debugadapter/lib/logger'; 12 | import { GDBBackend } from './GDBBackend'; 13 | 14 | export class MIParser { 15 | protected line = ''; 16 | protected pos = 0; 17 | protected commandQueue: any = {}; 18 | protected waitReady?: (value?: void | PromiseLike) => void; 19 | 20 | constructor(protected gdb: GDBBackend) { 21 | } 22 | 23 | public parse(stream: Readable): Promise { 24 | return new Promise((resolve) => { 25 | this.waitReady = resolve; 26 | const lineRegex = /(.*)(\r?\n)/; 27 | let buff = ''; 28 | stream.on('data', (chunk) => { 29 | buff += chunk.toString(); 30 | let regexArray = lineRegex.exec(buff); 31 | while (regexArray) { 32 | this.line = regexArray[1]; 33 | this.pos = 0; 34 | this.handleLine(); 35 | buff = buff.substring(regexArray[1].length + regexArray[2].length); 36 | regexArray = lineRegex.exec(buff); 37 | } 38 | }); 39 | }); 40 | } 41 | 42 | public queueCommand(token: number, command: (resultClass: string, resultData: any) => void) { 43 | this.commandQueue[token] = command; 44 | } 45 | 46 | protected peek() { 47 | if (this.pos < this.line.length) { 48 | return this.line[this.pos]; 49 | } else { 50 | return null; 51 | } 52 | } 53 | 54 | protected next() { 55 | if (this.pos < this.line.length) { 56 | return this.line[this.pos++]; 57 | } else { 58 | return null; 59 | } 60 | } 61 | 62 | protected back() { 63 | this.pos--; 64 | } 65 | 66 | protected restOfLine() { 67 | return this.line.substr(this.pos); 68 | } 69 | 70 | protected handleToken(firstChar: string) { 71 | let token = firstChar; 72 | let c = this.next(); 73 | while (c && c >= '0' && c <= '9') { 74 | token += c; 75 | c = this.next(); 76 | } 77 | this.back(); 78 | return token; 79 | } 80 | 81 | protected handleCString() { 82 | let c = this.next(); 83 | if (!c || c !== '"') { 84 | return null; 85 | } 86 | 87 | let cstring = ''; 88 | for (c = this.next(); c; c = this.next()) { 89 | switch (c) { 90 | case '"': 91 | return cstring; 92 | case '\\': 93 | c = this.next(); 94 | if (c) { 95 | switch (c) { 96 | case 'n': 97 | cstring += '\n'; 98 | break; 99 | case 't': 100 | cstring += '\t'; 101 | break; 102 | case 'r': 103 | break; 104 | default: 105 | cstring += c; 106 | } 107 | } else { 108 | this.back(); 109 | } 110 | break; 111 | default: 112 | cstring += c; 113 | } 114 | } 115 | 116 | return cstring; 117 | } 118 | 119 | protected handleString() { 120 | let str = ''; 121 | for (let c = this.next(); c; c = this.next()) { 122 | if (c === '=' || c === ',') { 123 | this.back(); 124 | return str; 125 | } else { 126 | str += c; 127 | } 128 | } 129 | return str; 130 | } 131 | 132 | protected handleObject() { 133 | let c = this.next(); 134 | const result: any = {}; 135 | if (c === '{') { 136 | c = this.next(); 137 | while (c !== '}') { 138 | if (c !== ',') { 139 | this.back(); 140 | } 141 | const name = this.handleString(); 142 | if (this.next() === '=') { 143 | result[name] = this.handleValue(); 144 | } 145 | c = this.next(); 146 | } 147 | } 148 | 149 | if (c === '}') { 150 | return result; 151 | } else { 152 | return null; 153 | } 154 | } 155 | 156 | protected handleArray() { 157 | let c = this.next(); 158 | const result: any[] = []; 159 | if (c === '[') { 160 | c = this.next(); 161 | while (c !== ']') { 162 | if (c !== ',') { 163 | this.back(); 164 | } 165 | result.push(this.handleValue()); 166 | c = this.next(); 167 | } 168 | } 169 | 170 | if (c === ']') { 171 | return result; 172 | } else { 173 | return null; 174 | } 175 | } 176 | 177 | protected handleValue(): any { 178 | const c = this.next(); 179 | this.back(); 180 | switch (c) { 181 | case '"': 182 | return this.handleCString(); 183 | case '{': 184 | return this.handleObject(); 185 | case '[': 186 | return this.handleArray(); 187 | default: 188 | // A weird array element with a name, ignore the name and return the value 189 | this.handleString(); 190 | if (this.next() === '=') { 191 | return this.handleValue(); 192 | } 193 | } 194 | return null; 195 | } 196 | 197 | protected handleAsyncData() { 198 | const result: any = {}; 199 | 200 | let c = this.next(); 201 | let name = 'missing'; 202 | while (c === ',') { 203 | if (this.peek() !== '{') { 204 | name = this.handleString(); 205 | if (this.next() === '=') { 206 | result[name] = this.handleValue(); 207 | } 208 | } else { 209 | // In some cases, such as -break-insert with multiple results 210 | // GDB does not return an array, so we have to identify that 211 | // case and convert result to an array 212 | // An example is (many fields removed to make example readable): 213 | // 3-break-insert --function staticfunc1 214 | // tslint:disable-next-line: max-line-length 215 | // 3^done,bkpt={number="1",addr=""},{number="1.1",func="staticfunc1",file="functions.c"},{number="1.2",func="staticfunc1",file="functions_other.c"} 216 | if (!Array.isArray(result[name])) { 217 | result[name] = [result[name]]; 218 | } 219 | result[name].push(this.handleValue()); 220 | } 221 | c = this.next(); 222 | } 223 | 224 | return result; 225 | } 226 | 227 | protected handleConsoleStream() { 228 | const msg = this.handleCString(); 229 | if (msg) { 230 | this.gdb.emit('consoleStreamOutput', msg, 'stdout'); 231 | } 232 | } 233 | 234 | protected handleLogStream() { 235 | const msg = this.handleCString(); 236 | if (msg) { 237 | this.gdb.emit('consoleStreamOutput', msg, 'log'); 238 | } 239 | } 240 | 241 | protected handleLine() { 242 | let c = this.next(); 243 | if (!c) { 244 | return; 245 | } 246 | 247 | let token = ''; 248 | 249 | if (c >= '0' && c <= '9') { 250 | token = this.handleToken(c); 251 | c = this.next(); 252 | } 253 | 254 | switch (c) { 255 | case '^': 256 | const rest = this.restOfLine(); 257 | for (let i = 0; i < rest.length; i += 1000) { 258 | const msg = i === 0 ? 'result' : '-cont-'; 259 | logger.verbose(`GDB ${msg}: ${token} ${rest.substr(i, 1000)}`); 260 | } 261 | const command = this.commandQueue[token]; 262 | if (command) { 263 | const resultClass = this.handleString(); 264 | const resultData = this.handleAsyncData(); 265 | command(resultClass, resultData); 266 | delete this.commandQueue[token]; 267 | } else { 268 | logger.error('GDB response with no command: ' + token); 269 | } 270 | break; 271 | case '~': 272 | case '@': 273 | this.handleConsoleStream(); 274 | break; 275 | case '&': 276 | this.handleLogStream(); 277 | break; 278 | case '=': 279 | logger.verbose('GDB notify async: ' + this.restOfLine()); 280 | const notifyClass = this.handleString(); 281 | this.gdb.emit('notifyAsync', notifyClass, this.handleAsyncData()); 282 | break; 283 | case '*': 284 | logger.verbose('GDB exec async: ' + this.restOfLine()); 285 | const execClass = this.handleString(); 286 | this.gdb.emit('execAsync', execClass, this.handleAsyncData()); 287 | break; 288 | case '+': 289 | logger.verbose('GDB status async: ' + this.restOfLine()); 290 | const statusClass = this.handleString(); 291 | this.gdb.emit('statusAsync', statusClass, this.handleAsyncData()); 292 | break; 293 | case '(': 294 | if (this.waitReady) { 295 | this.waitReady(); 296 | this.waitReady = undefined; 297 | } 298 | break; 299 | default: 300 | // treat as console output. happens on Windows. 301 | this.back(); 302 | this.gdb.emit('consoleStreamOutput', this.restOfLine() + '\n', 'stdout'); 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/GDBTargetDebugSession.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2019 Kichwa Coders and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import { GDBDebugSession, RequestArguments } from './GDBDebugSession'; 12 | import { 13 | InitializedEvent, Logger, logger, OutputEvent, 14 | } from 'vscode-debugadapter'; 15 | import * as mi from './mi'; 16 | import { DebugProtocol } from 'vscode-debugprotocol'; 17 | import { spawn, ChildProcess } from 'child_process'; 18 | 19 | export interface TargetAttachArguments { 20 | // Target type default is "remote" 21 | type?: string; 22 | // Target parameters would be something like "localhost:12345", defaults 23 | // to [`${host}:${port}`] 24 | parameters?: string[]; 25 | // Target host to connect to, defaults to 'localhost', ignored if parameters is set 26 | host?: string; 27 | // Target port to connect to, ignored if parameters is set 28 | port?: string; 29 | // Target connect commands - if specified used in preference of type, parameters, host, target 30 | connectCommands?: string[]; 31 | } 32 | 33 | export interface TargetLaunchArguments extends TargetAttachArguments { 34 | // The executable for the target server to launch (e.g. gdbserver or JLinkGDBServerCLExe), 35 | // defaults to 'gdbserver --once :0 ${args.program}' (requires gdbserver >= 7.3) 36 | server?: string; 37 | serverParameters?: string[]; 38 | // Regular expression to extract port from by examinging stdout/err of server. 39 | // Once server is launched, port will be set to this if port is not set. 40 | // defaults to matching a string like 'Listening on port 41551' which is what gdbserver provides 41 | // Ignored if port or parameters is set 42 | serverPortRegExp?: string; 43 | // Delay after startup before continuing launch, in milliseconds. If serverPortRegExp is 44 | // provided, it is the delay after that regexp is seen. 45 | serverStartupDelay?: number; 46 | } 47 | 48 | export interface ImageAndSymbolArguments { 49 | // If specified, a symbol file to load at the given (optional) offset 50 | symbolFileName?: string; 51 | symbolOffset?: string; 52 | // If specified, an image file to load at the given (optional) offset 53 | imageFileName?: string; 54 | imageOffset?: string; 55 | } 56 | 57 | export interface TargetAttachRequestArguments extends RequestArguments { 58 | target?: TargetAttachArguments; 59 | imageAndSymbols?: ImageAndSymbolArguments; 60 | // Optional commands to issue between loading image and resuming target 61 | preRunCommands?: string[]; 62 | } 63 | 64 | export interface TargetLaunchRequestArguments extends TargetAttachRequestArguments { 65 | target?: TargetLaunchArguments; 66 | imageAndSymbols?: ImageAndSymbolArguments; 67 | // Optional commands to issue between loading image and resuming target 68 | preRunCommands?: string[]; 69 | } 70 | 71 | export class GDBTargetDebugSession extends GDBDebugSession { 72 | protected gdbserver?: ChildProcess; 73 | 74 | protected async launchRequest(response: DebugProtocol.LaunchResponse, 75 | args: TargetLaunchRequestArguments): Promise { 76 | try { 77 | this.setupCommonLoggerAndHandlers(args); 78 | await this.startGDBServer(args); 79 | await this.startGDBAndAttachToTarget(response, args); 80 | } catch (err) { 81 | this.sendErrorResponse(response, 1, err.message); 82 | } 83 | } 84 | 85 | protected async attachRequest(response: DebugProtocol.AttachResponse, 86 | args: TargetAttachRequestArguments): Promise { 87 | try { 88 | this.setupCommonLoggerAndHandlers(args); 89 | await this.startGDBAndAttachToTarget(response, args); 90 | } catch (err) { 91 | this.sendErrorResponse(response, 1, err.message); 92 | } 93 | } 94 | 95 | protected setupCommonLoggerAndHandlers(args: TargetLaunchRequestArguments) { 96 | logger.setup(args.verbose ? Logger.LogLevel.Verbose : Logger.LogLevel.Warn, args.logFile || false); 97 | 98 | this.gdb.on('consoleStreamOutput', (output, category) => { 99 | this.sendEvent(new OutputEvent(output, category)); 100 | }); 101 | 102 | this.gdb.on('execAsync', (resultClass, resultData) => this.handleGDBAsync(resultClass, resultData)); 103 | this.gdb.on('notifyAsync', (resultClass, resultData) => this.handleGDBNotify(resultClass, resultData)); 104 | } 105 | 106 | protected async startGDBServer(args: TargetLaunchRequestArguments): Promise { 107 | if (args.target === undefined) { 108 | args.target = {}; 109 | } 110 | const target = args.target; 111 | const serverExe = target.server !== undefined ? target.server : 'gdbserver'; 112 | const serverParams = target.serverParameters !== undefined 113 | ? target.serverParameters : ['--once', ':0', args.program]; 114 | 115 | // Wait until gdbserver is started and ready to receive connections. 116 | await new Promise((resolve, reject) => { 117 | this.gdbserver = spawn(serverExe, serverParams, { cwd: args.cwd }); 118 | let gdbserverStartupResolved = false; 119 | let accumulatedStderr = ''; 120 | let checkTargetPort = (data: any) => { 121 | // do nothing by default 122 | }; 123 | if (target.port && target.serverParameters) { 124 | setTimeout(() => { 125 | gdbserverStartupResolved = true; 126 | resolve(); 127 | }, target.serverStartupDelay !== undefined ? target.serverStartupDelay : 0); 128 | } else { 129 | checkTargetPort = (data: any) => { 130 | const regex = new RegExp(target.serverPortRegExp 131 | ? target.serverPortRegExp : 'Listening on port ([0-9]+)'); 132 | const m = regex.exec(data); 133 | if (m !== null) { 134 | target.port = m[1]; 135 | setTimeout(() => { 136 | gdbserverStartupResolved = true; 137 | resolve(); 138 | }, target.serverStartupDelay !== undefined ? target.serverStartupDelay : 0); 139 | } 140 | }; 141 | } 142 | 143 | this.gdbserver.stdout.on('data', (data) => { 144 | this.sendEvent(new OutputEvent(data.toString(), 'server')); 145 | checkTargetPort(data); 146 | }); 147 | 148 | this.gdbserver.stderr.on('data', (data) => { 149 | const err = data.toString(); 150 | accumulatedStderr += err; 151 | this.sendEvent(new OutputEvent(err, 'server')); 152 | checkTargetPort(data); 153 | }); 154 | 155 | this.gdbserver.on('exit', (code) => { 156 | const exitmsg = `${serverExe} has exited with code ${code}`; 157 | this.sendEvent(new OutputEvent(exitmsg, 'server')); 158 | if (!gdbserverStartupResolved) { 159 | gdbserverStartupResolved = true; 160 | reject(new Error(exitmsg + '\n' + accumulatedStderr)); 161 | } 162 | }); 163 | 164 | this.gdbserver.on('error', (err) => { 165 | const errmsg = `${serverExe} has hit error ${err}`; 166 | this.sendEvent(new OutputEvent(errmsg, 'server')); 167 | if (!gdbserverStartupResolved) { 168 | gdbserverStartupResolved = true; 169 | reject(new Error(errmsg + '\n' + accumulatedStderr)); 170 | } 171 | }); 172 | }); 173 | } 174 | 175 | protected async startGDBAndAttachToTarget(response: DebugProtocol.AttachResponse | DebugProtocol.LaunchResponse, 176 | args: TargetAttachRequestArguments): Promise { 177 | if (args.target === undefined) { 178 | args.target = {}; 179 | } 180 | const target = args.target; 181 | try { 182 | this.isAttach = true; 183 | await this.spawn(args); 184 | await this.gdb.sendFileExecAndSymbols(args.program); 185 | await this.gdb.sendEnablePrettyPrint(); 186 | if (args.imageAndSymbols) { 187 | if (args.imageAndSymbols.symbolFileName) { 188 | if (args.imageAndSymbols.symbolOffset) { 189 | await this.gdb.sendAddSymbolFile(args.imageAndSymbols.symbolFileName, 190 | args.imageAndSymbols.symbolOffset); 191 | } else { 192 | await this.gdb.sendFileSymbolFile(args.imageAndSymbols.symbolFileName); 193 | } 194 | } 195 | } 196 | 197 | if (target.connectCommands === undefined) { 198 | const targetType = target.type !== undefined ? target.type : 'remote'; 199 | let defaultTarget: string[]; 200 | if (target.port !== undefined) { 201 | defaultTarget = [target.host !== undefined 202 | ? `${target.host}:${target.port}` : `localhost:${target.port}`]; 203 | } else { 204 | defaultTarget = []; 205 | } 206 | const targetParameters = target.parameters !== undefined ? target.parameters : defaultTarget; 207 | await mi.sendTargetSelectRequest(this.gdb, { type: targetType, parameters: targetParameters }); 208 | this.sendEvent(new OutputEvent(`connected to ${targetType} target ${targetParameters.join(' ')}`)); 209 | } else { 210 | await this.gdb.sendCommands(target.connectCommands); 211 | this.sendEvent(new OutputEvent('connected to target using provided connectCommands')); 212 | } 213 | 214 | await this.gdb.sendCommands(args.initCommands); 215 | 216 | if (args.imageAndSymbols) { 217 | if (args.imageAndSymbols.imageFileName) { 218 | await this.gdb.sendLoad(args.imageAndSymbols.imageFileName, 219 | args.imageAndSymbols.imageOffset); 220 | } 221 | } 222 | await this.gdb.sendCommands(args.preRunCommands); 223 | this.sendEvent(new InitializedEvent()); 224 | this.sendResponse(response); 225 | } catch (err) { 226 | this.sendErrorResponse(response, 1, err.message); 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/integration-tests/var.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2018 QNX Software Systems and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import { expect } from 'chai'; 12 | import * as path from 'path'; 13 | import { LaunchRequestArguments } from '..'; 14 | import { CdtDebugClient } from './debugClient'; 15 | import { 16 | gdbPath, getScopes, openGdbConsole, resolveLineTagLocations, Scope, standardBeforeEach, 17 | testProgramsDir, verifyVariable, 18 | } from './utils'; 19 | 20 | // Allow non-arrow functions: https://mochajs.org/#arrow-functions 21 | // tslint:disable:only-arrow-functions 22 | describe('Variables Test Suite', function() { 23 | 24 | let dc: CdtDebugClient; 25 | let scope: Scope; 26 | const varsProgram = path.join(testProgramsDir, 'vars'); 27 | const varsSrc = path.join(testProgramsDir, 'vars.c'); 28 | const numVars = 8; // number of variables in the main() scope of vars.c 29 | 30 | const lineTags = { 31 | 'STOP HERE': 0, 32 | }; 33 | 34 | const hexValueRegex = /0x[\d]+/; 35 | 36 | before(function() { 37 | resolveLineTagLocations(varsSrc, lineTags); 38 | }); 39 | 40 | beforeEach(async function() { 41 | dc = await standardBeforeEach(); 42 | 43 | await dc.hitBreakpoint({ 44 | verbose: true, 45 | gdb: gdbPath, 46 | program: varsProgram, 47 | openGdbConsole, 48 | } as LaunchRequestArguments, { 49 | path: varsSrc, 50 | line: lineTags['STOP HERE'], 51 | }); 52 | scope = await getScopes(dc); 53 | expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); 54 | }); 55 | 56 | afterEach(async function() { 57 | await dc.stop(); 58 | }); 59 | 60 | // Move the timeout out of the way if the adapter is going to be debugged. 61 | if (process.env.INSPECT_DEBUG_ADAPTER) { 62 | this.timeout(9999999); 63 | } 64 | it('can read and set simple variables in a program', async function() { 65 | // read the variables 66 | let vr = scope.scopes.body.scopes[0].variablesReference; 67 | let vars = await dc.variablesRequest({ variablesReference: vr }); 68 | expect(vars.body.variables.length, 'There is a different number of variables than expected').to.equal(numVars); 69 | verifyVariable(vars.body.variables[0], 'a', 'int', '1'); 70 | verifyVariable(vars.body.variables[1], 'b', 'int', '2'); 71 | // set the variables to something different 72 | await dc.setVariableRequest({ name: 'a', value: '25', variablesReference: vr }); 73 | await dc.setVariableRequest({ name: 'b', value: '10', variablesReference: vr }); 74 | // assert that the variables have been updated to the new values 75 | vars = await dc.variablesRequest({ variablesReference: vr }); 76 | expect(vars.body.variables.length, 'There is a different number of variables than expected').to.equal(numVars); 77 | verifyVariable(vars.body.variables[0], 'a', 'int', '25'); 78 | verifyVariable(vars.body.variables[1], 'b', 'int', '10'); 79 | // step the program and see that the values were passed to the program and evaluated. 80 | await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 1 }); 81 | scope = await getScopes(dc); 82 | expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); 83 | vr = scope.scopes.body.scopes[0].variablesReference; 84 | vars = await dc.variablesRequest({ variablesReference: vr }); 85 | expect(vars.body.variables.length, 'There is a different number of variables than expected').to.equal(numVars); 86 | verifyVariable(vars.body.variables[2], 'c', 'int', '35'); 87 | }); 88 | 89 | it('can read and set struct variables in a program', async function() { 90 | // step past the initialization for the structure 91 | await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 1 }); 92 | await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 2 }); 93 | scope = await getScopes(dc); 94 | expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); 95 | // assert we can see the struct and its elements 96 | let vr = scope.scopes.body.scopes[0].variablesReference; 97 | let vars = await dc.variablesRequest({ variablesReference: vr }); 98 | expect(vars.body.variables.length, 'There is a different number of variables than expected').to.equal(numVars); 99 | verifyVariable(vars.body.variables[3], 'r', 'struct foo', '{...}', true); 100 | const childVR = vars.body.variables[3].variablesReference; 101 | let children = await dc.variablesRequest({ variablesReference: childVR }); 102 | expect( 103 | children.body.variables.length, 104 | 'There is a different number of child variables than expected', 105 | ).to.equal(3); 106 | verifyVariable(children.body.variables[0], 'x', 'int', '1'); 107 | verifyVariable(children.body.variables[1], 'y', 'int', '2'); 108 | verifyVariable(children.body.variables[2], 'z', 'struct bar', '{...}', true); 109 | // set the variables to something different 110 | await dc.setVariableRequest({ name: 'x', value: '25', variablesReference: childVR }); 111 | await dc.setVariableRequest({ name: 'y', value: '10', variablesReference: childVR }); 112 | // assert that the variables have been updated to the new values 113 | children = await dc.variablesRequest({ variablesReference: childVR }); 114 | expect( 115 | children.body.variables.length, 116 | 'There is a different number of child variables than expected', 117 | ).to.equal(3); 118 | verifyVariable(children.body.variables[0], 'x', 'int', '25'); 119 | verifyVariable(children.body.variables[1], 'y', 'int', '10'); 120 | // step the program and see that the values were passed to the program and evaluated. 121 | await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 3 }); 122 | scope = await getScopes(dc); 123 | expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); 124 | vr = scope.scopes.body.scopes[0].variablesReference; 125 | vars = await dc.variablesRequest({ variablesReference: vr }); 126 | expect(vars.body.variables.length, 'There is a different number of variables than expected').to.equal(numVars); 127 | verifyVariable(vars.body.variables[4], 'd', 'int', '35'); 128 | }); 129 | 130 | it('can read and set nested struct variables in a program', async function() { 131 | // step past the initialization for the structure 132 | await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 1 }); 133 | await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 2 }); 134 | scope = await getScopes(dc); 135 | expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); 136 | // assert we can see the 'foo' struct and its child 'bar' struct 137 | let vr = scope.scopes.body.scopes[0].variablesReference; 138 | let vars = await dc.variablesRequest({ variablesReference: vr }); 139 | expect(vars.body.variables.length, 'There is a different number of variables than expected').to.equal(numVars); 140 | verifyVariable(vars.body.variables[3], 'r', 'struct foo', '{...}', true); 141 | const childVR = vars.body.variables[3].variablesReference; 142 | const children = await dc.variablesRequest({ variablesReference: childVR }); 143 | expect( 144 | children.body.variables.length, 145 | 'There is a different number of child variables than expected', 146 | ).to.equal(3); 147 | verifyVariable(children.body.variables[2], 'z', 'struct bar', '{...}', true); 148 | // assert we can see the elements of z 149 | const subChildVR = children.body.variables[2].variablesReference; 150 | let subChildren = await dc.variablesRequest({ variablesReference: subChildVR }); 151 | expect( 152 | subChildren.body.variables.length, 153 | 'There is a different number of grandchild variables than expected', 154 | ).to.equal(2); 155 | verifyVariable(subChildren.body.variables[0], 'a', 'int', '3'); 156 | verifyVariable(subChildren.body.variables[1], 'b', 'int', '4'); 157 | // set the variables to something different 158 | await dc.setVariableRequest({ name: 'a', value: '25', variablesReference: subChildVR }); 159 | await dc.setVariableRequest({ name: 'b', value: '10', variablesReference: subChildVR }); 160 | // assert that the variables have been updated to the new values 161 | subChildren = await dc.variablesRequest({ variablesReference: subChildVR }); 162 | expect( 163 | subChildren.body.variables.length, 164 | 'There is a different number of grandchild variables than expected', 165 | ).to.equal(2); 166 | verifyVariable(subChildren.body.variables[0], 'a', 'int', '25'); 167 | verifyVariable(subChildren.body.variables[1], 'b', 'int', '10'); 168 | // step the program and see that the values were passed to the program and evaluated. 169 | await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 3 }); 170 | await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: lineTags['STOP HERE'] + 4 }); 171 | scope = await getScopes(dc); 172 | expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); 173 | vr = scope.scopes.body.scopes[0].variablesReference; 174 | vars = await dc.variablesRequest({ variablesReference: vr }); 175 | expect(vars.body.variables.length, 'There is a different number of variables than expected').to.equal(numVars); 176 | verifyVariable(vars.body.variables[5], 'e', 'int', '35'); 177 | }); 178 | 179 | it('can read and set array elements in a program', async function() { 180 | // skip ahead to array initialization 181 | const br = await dc.setBreakpointsRequest({ source: { path: varsSrc }, breakpoints: [{ line: 24 }] }); 182 | expect(br.success).to.equal(true); 183 | await dc.continue({ threadId: scope.thread.id }, 184 | 'breakpoint', 185 | { line: 24, path: varsSrc }); 186 | scope = await getScopes(dc); 187 | expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); 188 | // assert we can see the array and its elements 189 | let vr = scope.scopes.body.scopes[0].variablesReference; 190 | let vars = await dc.variablesRequest({ variablesReference: vr }); 191 | expect(vars.body.variables.length, 'There is a different number of variables than expected').to.equal(numVars); 192 | verifyVariable(vars.body.variables[6], 'f', 'int [3]', undefined, true); 193 | expect(hexValueRegex.test(vars.body.variables[6].value), 194 | 'The display value of the array is not a hexidecimal address').to.equal(true); 195 | const childVR = vars.body.variables[6].variablesReference; 196 | let children = await dc.variablesRequest({ variablesReference: childVR }); 197 | expect( 198 | children.body.variables.length, 199 | 'There is a different number of child variables than expected', 200 | ).to.equal(3); 201 | verifyVariable(children.body.variables[0], '[0]', 'int', '1'); 202 | verifyVariable(children.body.variables[1], '[1]', 'int', '2'); 203 | verifyVariable(children.body.variables[2], '[2]', 'int', '3'); 204 | // set the variables to something different 205 | await dc.setVariableRequest({ name: '[0]', value: '11', variablesReference: childVR }); 206 | await dc.setVariableRequest({ name: '[1]', value: '22', variablesReference: childVR }); 207 | await dc.setVariableRequest({ name: '[2]', value: '33', variablesReference: childVR }); 208 | // assert that the variables have been updated to the new values 209 | children = await dc.variablesRequest({ variablesReference: childVR }); 210 | expect( 211 | children.body.variables.length, 212 | 'There is a different number of child variables than expected', 213 | ).to.equal(3); 214 | verifyVariable(children.body.variables[0], '[0]', 'int', '11'); 215 | verifyVariable(children.body.variables[1], '[1]', 'int', '22'); 216 | verifyVariable(children.body.variables[2], '[2]', 'int', '33'); 217 | // step the program and see that the values were passed to the program and evaluated. 218 | await dc.next({ threadId: scope.thread.id }, { path: varsSrc, line: 25 }); 219 | scope = await getScopes(dc); 220 | expect(scope.scopes.body.scopes.length, 'Unexpected number of scopes returned').to.equal(1); 221 | vr = scope.scopes.body.scopes[0].variablesReference; 222 | vars = await dc.variablesRequest({ variablesReference: vr }); 223 | expect(vars.body.variables.length, 'There is a different number of variables than expected').to.equal(numVars); 224 | verifyVariable(vars.body.variables[7], 'g', 'int', '66'); 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /src/integration-tests/breakpoints.spec.ts: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2019 Arm and others 3 | * 4 | * This program and the accompanying materials are made 5 | * available under the terms of the Eclipse Public License 2.0 6 | * which is available at https://www.eclipse.org/legal/epl-2.0/ 7 | * 8 | * SPDX-License-Identifier: EPL-2.0 9 | *********************************************************************/ 10 | 11 | import * as path from 'path'; 12 | import { expect } from 'chai'; 13 | import { LaunchRequestArguments } from '../GDBDebugSession'; 14 | import { CdtDebugClient } from './debugClient'; 15 | import { 16 | standardBeforeEach, 17 | gdbPath, 18 | testProgramsDir, 19 | openGdbConsole, 20 | getScopes, 21 | verifyVariable, 22 | } from './utils'; 23 | import { DebugProtocol } from 'vscode-debugprotocol'; 24 | 25 | describe('breakpoints', async () => { 26 | let dc: CdtDebugClient; 27 | 28 | beforeEach(async () => { 29 | dc = await standardBeforeEach(); 30 | 31 | await dc.launchRequest({ 32 | verbose: true, 33 | gdb: gdbPath, 34 | program: path.join(testProgramsDir, 'count'), 35 | openGdbConsole, 36 | logFile: '/tmp/log', 37 | } as LaunchRequestArguments); 38 | }); 39 | 40 | afterEach(async () => { 41 | await dc.stop(); 42 | }); 43 | 44 | it('hits a standard breakpoint', async () => { 45 | const bpResp = await dc.setBreakpointsRequest({ 46 | source: { 47 | name: 'count.c', 48 | path: path.join(testProgramsDir, 'count.c'), 49 | }, 50 | breakpoints: [ 51 | { 52 | column: 1, 53 | line: 4, 54 | }, 55 | ], 56 | }); 57 | expect(bpResp.body.breakpoints.length).eq(1); 58 | expect(bpResp.body.breakpoints[0].verified).eq(true); 59 | expect(bpResp.body.breakpoints[0].message).eq(undefined); 60 | await dc.configurationDoneRequest(); 61 | const scope = await getScopes(dc); 62 | const vr = scope.scopes.body.scopes[0].variablesReference; 63 | const vars = await dc.variablesRequest({ variablesReference: vr }); 64 | verifyVariable(vars.body.variables[0], 'count', 'int', '0'); 65 | }); 66 | 67 | it('handles breakpoints in multiple files', async () => { 68 | await dc.setBreakpointsRequest({ 69 | source: { 70 | name: 'count.c', 71 | path: path.join(testProgramsDir, 'count.c'), 72 | }, 73 | breakpoints: [ 74 | { 75 | column: 1, 76 | line: 4, 77 | }, 78 | ], 79 | }); 80 | await dc.setBreakpointsRequest({ 81 | source: { 82 | name: 'count_other.c', 83 | path: path.join(testProgramsDir, 'count_other.c'), 84 | }, 85 | breakpoints: [ 86 | { 87 | column: 1, 88 | line: 2, 89 | }, 90 | ], 91 | }); 92 | await dc.configurationDoneRequest(); 93 | const scope = await getScopes(dc); 94 | const vr = scope.scopes.body.scopes[0].variablesReference; 95 | const vars = await dc.variablesRequest({ variablesReference: vr }); 96 | verifyVariable(vars.body.variables[0], 'count', 'int', '0'); 97 | }); 98 | 99 | it('fails gracefully on breakpoint on unknown file', async () => { 100 | const bpResp = await dc.setBreakpointsRequest({ 101 | source: { 102 | name: 'countBAD.c', 103 | path: path.join(testProgramsDir, 'countBAD.c'), 104 | }, 105 | breakpoints: [ 106 | { 107 | column: 1, 108 | line: 4, 109 | }, 110 | ], 111 | }); 112 | expect(bpResp.body.breakpoints.length).to.eq(1); 113 | expect(bpResp.body.breakpoints[0].verified).to.eq(false); 114 | expect(bpResp.body.breakpoints[0].message).not.eq(undefined); 115 | }); 116 | 117 | it('fails gracefully on breakpoint on bad line in otherwise good source', async () => { 118 | const bpResp = await dc.setBreakpointsRequest({ 119 | source: { 120 | name: 'count.c', 121 | path: path.join(testProgramsDir, 'count.c'), 122 | }, 123 | breakpoints: [ 124 | { 125 | column: 1, 126 | line: 4 + 100000000, 127 | }, 128 | ], 129 | }); 130 | expect(bpResp.body.breakpoints.length).to.eq(1); 131 | expect(bpResp.body.breakpoints[0].verified).to.eq(false); 132 | expect(bpResp.body.breakpoints[0].message).not.eq(undefined); 133 | }); 134 | 135 | it('maintains breakpoint order when modifying breakpoints in a file', async () => { 136 | const bpResp1 = await dc.setBreakpointsRequest({ 137 | source: { 138 | name: 'count.c', 139 | path: path.join(testProgramsDir, 'count.c'), 140 | }, 141 | breakpoints: [ 142 | { 143 | column: 1, 144 | line: 6, 145 | }, 146 | ], 147 | }); 148 | expect(bpResp1.body.breakpoints.length).to.eq(1); 149 | expect(bpResp1.body.breakpoints[0].line).eq(6); 150 | const bpResp2 = await dc.setBreakpointsRequest({ 151 | source: { 152 | name: 'count.c', 153 | path: path.join(testProgramsDir, 'count.c'), 154 | }, 155 | breakpoints: [ 156 | { 157 | column: 1, 158 | line: 4, 159 | }, 160 | { 161 | column: 1, 162 | line: 6, 163 | }, 164 | ], 165 | }); 166 | expect(bpResp2.body.breakpoints.length).to.eq(2); 167 | expect(bpResp2.body.breakpoints[0].line).eq(4); 168 | expect(bpResp2.body.breakpoints[1].line).eq(6); 169 | // Make sure the GDB id of the breakpoint on line 6 is unchanged 170 | expect(bpResp2.body.breakpoints[1].id).eq(bpResp1.body.breakpoints[0].id); 171 | }); 172 | 173 | // Pending support for testing multiple GDB versions - test requires 174 | // GDB >= 8.2 175 | it.skip('reports back relocated line number', async () => { 176 | const args = { 177 | source: { 178 | name: 'count.c', 179 | path: path.join(testProgramsDir, 'count.c'), 180 | }, 181 | breakpoints: [ 182 | { 183 | column: 1, 184 | line: 5, // this will be relocated to line 6 as no code on line 5 185 | }, 186 | ], 187 | } as DebugProtocol.SetBreakpointsArguments; 188 | const bpResp = await dc.setBreakpointsRequest(args); 189 | // Make sure the GDB id of the breakpoint is unchanged 190 | expect(bpResp.body.breakpoints[0].line).eq(6); 191 | }); 192 | 193 | // Pending support for testing multiple GDB versions - test requires 194 | // GDB >= 8.2 195 | it.skip('maintains gdb breakpoint when relocated', async () => { 196 | const args = { 197 | source: { 198 | name: 'count.c', 199 | path: path.join(testProgramsDir, 'count.c'), 200 | }, 201 | breakpoints: [ 202 | { 203 | column: 1, 204 | line: 5, // this will be relocated to line 6 as no code on line 5 205 | }, 206 | ], 207 | } as DebugProtocol.SetBreakpointsArguments; 208 | const bpResp1 = await dc.setBreakpointsRequest(args); 209 | expect(bpResp1.body.breakpoints.length).to.eq(1); 210 | expect(bpResp1.body.breakpoints[0].line).eq(6); 211 | const bpResp2 = await dc.setBreakpointsRequest(args); 212 | expect(bpResp2.body.breakpoints.length).to.eq(1); 213 | expect(bpResp2.body.breakpoints[0].line).eq(6); 214 | // Make sure the GDB id of the breakpoint is unchanged 215 | expect(bpResp2.body.breakpoints[0].id).eq(bpResp1.body.breakpoints[0].id); 216 | }); 217 | 218 | // Pending support for testing multiple GDB versions - test requires 219 | // GDB >= 8.2 220 | it.skip('maintains gdb breakpoint when relocated - files with spaces', async () => { 221 | const args = { 222 | source: { 223 | name: 'count space.c', 224 | path: path.join(testProgramsDir, 'count space.c'), 225 | }, 226 | breakpoints: [ 227 | { 228 | column: 1, 229 | line: 7, // this will be relocated to line 9 as no code on line 7 230 | }, 231 | ], 232 | } as DebugProtocol.SetBreakpointsArguments; 233 | const bpResp1 = await dc.setBreakpointsRequest(args); 234 | expect(bpResp1.body.breakpoints.length).to.eq(1); 235 | expect(bpResp1.body.breakpoints[0].line).eq(9); 236 | const bpResp2 = await dc.setBreakpointsRequest(args); 237 | expect(bpResp2.body.breakpoints.length).to.eq(1); 238 | expect(bpResp2.body.breakpoints[0].line).eq(9); 239 | // Make sure the GDB id of the breakpoint is unchanged 240 | expect(bpResp2.body.breakpoints[0].id).eq(bpResp1.body.breakpoints[0].id); 241 | }); 242 | 243 | it('hits a conditional breakpoint', async () => { 244 | await dc.setBreakpointsRequest({ 245 | source: { 246 | name: 'count.c', 247 | path: path.join(testProgramsDir, 'count.c'), 248 | }, 249 | breakpoints: [ 250 | { 251 | column: 1, 252 | line: 4, 253 | condition: 'count == 5', 254 | }, 255 | ], 256 | }); 257 | await dc.configurationDoneRequest(); 258 | const scope = await getScopes(dc); 259 | const vr = scope.scopes.body.scopes[0].variablesReference; 260 | const vars = await dc.variablesRequest({ variablesReference: vr }); 261 | verifyVariable(vars.body.variables[0], 'count', 'int', '5'); 262 | }); 263 | 264 | it('hits a hit conditional breakpoint with >', async () => { 265 | await dc.setBreakpointsRequest({ 266 | source: { 267 | name: 'count.c', 268 | path: path.join(testProgramsDir, 'count.c'), 269 | }, 270 | breakpoints: [ 271 | { 272 | column: 1, 273 | line: 4, 274 | hitCondition: '> 5', 275 | }, 276 | ], 277 | }); 278 | await dc.configurationDoneRequest(); 279 | const scope = await getScopes(dc); 280 | const vr = scope.scopes.body.scopes[0].variablesReference; 281 | const vars = await dc.variablesRequest({ variablesReference: vr }); 282 | verifyVariable(vars.body.variables[0], 'count', 'int', '5'); 283 | }); 284 | 285 | it('hits a hit conditional breakpoint without >', async () => { 286 | await dc.setBreakpointsRequest({ 287 | source: { 288 | name: 'count.c', 289 | path: path.join(testProgramsDir, 'count.c'), 290 | }, 291 | breakpoints: [ 292 | { 293 | column: 1, 294 | line: 4, 295 | hitCondition: '5', 296 | }, 297 | ], 298 | }); 299 | await dc.configurationDoneRequest(); 300 | const scope = await getScopes(dc); 301 | const vr = scope.scopes.body.scopes[0].variablesReference; 302 | const vars = await dc.variablesRequest({ variablesReference: vr }); 303 | verifyVariable(vars.body.variables[0], 'count', 'int', '4'); 304 | }); 305 | 306 | it('resolves breakpoints', async () => { 307 | let response = await dc.setBreakpointsRequest({ 308 | source: { 309 | name: 'count.c', 310 | path: path.join(testProgramsDir, 'count.c'), 311 | }, 312 | breakpoints: [ 313 | { 314 | column: 1, 315 | line: 2, 316 | }, 317 | ], 318 | }); 319 | expect(response.body.breakpoints.length).to.eq(1); 320 | 321 | await dc.configurationDoneRequest(); 322 | await dc.waitForEvent('stopped'); 323 | 324 | response = await dc.setBreakpointsRequest({ 325 | source: { 326 | name: 'count.c', 327 | path: path.join(testProgramsDir, 'count.c'), 328 | }, 329 | breakpoints: [ 330 | { 331 | column: 1, 332 | line: 2, 333 | }, 334 | { 335 | column: 1, 336 | line: 3, 337 | }, 338 | ], 339 | }); 340 | expect(response.body.breakpoints.length).to.eq(2); 341 | 342 | response = await dc.setBreakpointsRequest({ 343 | source: { 344 | name: 'count.c', 345 | path: path.join(testProgramsDir, 'count.c'), 346 | }, 347 | breakpoints: [ 348 | { 349 | column: 1, 350 | line: 2, 351 | condition: 'count == 5', 352 | }, 353 | { 354 | column: 1, 355 | line: 3, 356 | }, 357 | ], 358 | }); 359 | expect(response.body.breakpoints.length).to.eq(2); 360 | 361 | response = await dc.setBreakpointsRequest({ 362 | source: { 363 | name: 'count.c', 364 | path: path.join(testProgramsDir, 'count.c'), 365 | }, 366 | breakpoints: [ 367 | { 368 | column: 1, 369 | line: 2, 370 | condition: 'count == 3', 371 | }, 372 | ], 373 | }); 374 | expect(response.body.breakpoints.length).to.eq(1); 375 | }); 376 | }); 377 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | --------------------------------------------------------------------------------