VSCode拡張機能Native Debug雑記
目的
- NetBSD(i386)へSSH経由でのアタッチモードによる
gdb
デバッグを、起動中プロセス一覧から選んで実行できるかを確認(MS公式C/C++拡張機能同等)
前提
- VSCode拡張機能開発経験なし
MS公式C/C++拡張機能不使用の理由
- OSとしてBSDが弾かれる(Extension/src/Debugger/attachToProcess.ts#L135)
- デバッグ以外の機能が多く手が入れにくい
- Native Debugはかなりシンプルに作られている(ように見える)
NetBSD(i386)でNative Debugが使えない問題
Device busy
エラーが出てプロセスが落ちる(gdb-8.1.1
)。おそらくエラー判定されて止めている?
やるべきこと
- VSCode拡張機能開発の基本理解
- Native Debugの拡張機能自体のデバッグ
- MS公式C/C++拡張機能で使われているプロセスのPick機能理解
- プロセスPickerをNative Debugに適用できるか確認(一旦Ubuntu amd64等)
- ↑がNetBSD(i386)でできるか確認
- Native DebugがNetBSDで落ちる原因調査
- Native DebugのNetBSD対応(C/C++のみ)
結果
- 目的は達成可能。ただし一部条件あり
- おそらく非同期モードが不可により
break-insert
が効かないため、無理やりkill -SIGINT
をgdb
に送って止める -
kill -SIGINT
のタイミングで止まるので、ブレーク解除時も一回止まってしまう - デタッチ時にも
kill -SIGINT
を送り、止まったタイミングでexec-detach
を実行し終わるまで待てば正常にデタッチできる。
- おそらく非同期モードが不可により
これは設定ファイルには書かなくなるが入力を手動でやるだけ?
Docker環境 起動コマンド
docker run -itd --name vscodeex -v ./:/work -w /work -u node node:21.7.1-bookworm-slim
rootユーザーだとyo code
でエラーとなるので-u node
でログインユーザー指定しているが、ホストがrootだと一般ユーザーでは/work
書き込み権限がないのでchmod 777 .
などとして作業ディレクトリの書き込み権限をつけておく。
git
が入っていないのでコンテナにrootで入ってインストール
docker exec -it -u root vscodeex bash
apt update && apt install -y git
グローバルにnode moduleをインストールする場合はそのままrootのままでnpmコマンド。
初期化
yo code
TypeScript、sample、あとはすべてエンター。
上記記事を参考にしようとしたが、あまりにも適当な記事すぎる。
まずconst document = editor.document;
はどこにも使われてないから使い道がわからないし、undefined
を一切考慮していないので警告が出まくる。本当にTypeScript使ったことがあるのか疑問。
text
なんてどこにも宣言されていないのに補足も一切ない。
とりあえずF5右クリックでメニューは出るが、押下しても何もできない。コードが不完全なせいでしかない。
ブレークポイント貼っているのに止まらない。
記事参考にせずに↑の公式の手順通りのスケルトンプロジェクトで何もいじらなかったらちゃんと止まった。
npm run compile
もデバッグする段階では手動でやる必要がない。その説明も↑↑の記事になかったので誤解招く書き方になっている。やはりいきなり人の記事見るべきではない。
警告を出さずに動作を通す
const editor = vscode.window.activeTextEditor;
const selection = editor!.selection;
const text = editor!.document.getText(selection);
editor!.edit((edit: vscode.TextEditorEdit) => {
edit.replace(selection, text!.toUpperCase());
});
docker exec -it -u root vscodeex bash
apt install -y gdb gcc procps
exit
nodeユーザーでコンテナに入る
git clone https://github.com/WebFreak001/code-debug
cd code-debug
npm install
code .
Mcokのデバッグが必要?
{
"version": "0.2.0",
"configurations": [
{
"type": "gdb",
"request": "launch",
"name": "Launch Program",
"target": "/work/vscode-mock-debug/sampleWorkspace/sample",
"cwd": "${workspaceRoot}",
"valuesFormatting": "parseText",
"gdbpath": "/usr/bin/gdb"
}
]
}
gdb
のパス指定しても起動できない。
Configured debugger /usr/bin/gdb not found.
which gdb
/usr/bin/gdb
mock
は起動構成でみつからないからgdb
を使うのではと思ったがやり方が違う?
vscode-mock-debug
に関する日本語記事が1つもない。
チェック処理を消してout
ディレクトリを削除した後、自動で走るビルドを止めてからタスク実行→compile
したら通って起動構成Launch Extension
から↑のlaunch.json
でプログラムのデバッグもできるようになる。チェック処理がデバッグモードだと正しく動作しない?
@@ -54,10 +54,6 @@ class GDBDebugSession extends MI2DebugSession {
protected override launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void {
const dbgCommand = args.gdbpath || "gdb";
- if (this.checkCommand(dbgCommand)) {
- this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`);
- return;
- }
this.miDebugger = new MI2(dbgCommand, ["-q", "--interpreter=mi2"], args.debugger_args, args.env);
this.setPathSubstitutions(args.pathSubstitutions);
this.initDebugger();
@@ -100,10 +96,6 @@ class GDBDebugSession extends MI2DebugSession {
protected override attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void {
const dbgCommand = args.gdbpath || "gdb";
- if (this.checkCommand(dbgCommand)) {
- this.sendErrorResponse(response, 104, `Configured debugger ${dbgCommand} not found.`);
- return;
- }
this.miDebugger = new MI2(dbgCommand, ["-q", "--interpreter=mi2"], args.debugger_args, args.env);
this.setPathSubstitutions(args.pathSubstitutions);
this.initDebugger();
拡張機能自体のブレークポイントが効かない?サンプルの拡張機能は止まるからこの拡張機能固有の問題?
Debugger
For debugging more serious problems (such as an exception occurring in the extension itself), it may be useful to run the debug adapter itself within the debugger. Debug Adapter extensions run as a process separate from VSCode, so you have to perform some additional setup in order to debug them. Similar to the "Launch Extension" documented above, this will consist of running two separate VSCode instances.
From the first/main instance you will run both the "Launch Extension" to launch the secondary VSCode instance (which is the same as was described above). In addition you will need to run the "code-debug server" launch configuration. This "code-debug server" configuration will launch the debug adapter and also provides it with the special option --server=4711, causing it to be configured to communicate over the specified port number. Additionally, this launched debug adapter is run in the debugger of the first VSCode instance. You can set breakpoints or other conditions that you want to investigate in the debug adapter source code as you would any other source that you'd attempt to debug.
Note: there is a convenience compound launch configuration "Extension Debugging (Extension + Debug Server)" which runs both of these for you so you don't have to start them separately.
サーバーというほうも起動する必要がある?起動構成Extension + Debug Server
を使ってみたが変化なし。
デバッグ無しで作りこむしかないかもしれない。C/C++公式拡張で使われているPickerを調査。
サンプル拡張機能の修正
let disposable = vscode.commands.registerCommand(
"sample2.helloWorld",
() => {
const items = ["item1", "item2", "item3"];
vscode.window.showQuickPick(items).then((item) => {
if (item !== undefined) {
vscode.window.showInformationMessage(
`selected item is ${item}`
);
}
});
}
);
起動後文字列選択右クリックメニューを選んだあと、items
のリストが画面上部で選択できるようになり、いずれかを選択するとToastが出る。
vscode.QuickPickItem
はいくつかプロパティを持たせられる。
最低限label
とdescription
さえあれば、アタッチプロセスID、プログラムのPickができるはず。
let disposable = vscode.commands.registerCommand(
"sample2.helloWorld",
() => {
vscode.window
.showQuickPick([
{
label: "item1",
description: "desc1",
},
{
label: "item2",
description: "desc2",
},
{
label: "item3",
description: "desc3",
},
])
.then((item) => {
if (item !== undefined) {
vscode.window.showInformationMessage(
`selected item is ${item.label}`
);
}
});
}
);
C/C++公式拡張機能で使っているプロセス一覧用のコマンド
export class PsProcessParser {
private static get secondColumnCharacters(): number { return 50; }
private static get commColumnTitle(): string { return Array(PsProcessParser.secondColumnCharacters).join("a"); }
// the BSD version of ps uses '-c' to have 'comm' only output the executable name and not
// the full path. The Linux version of ps has 'comm' to only display the name of the executable
// Note that comm on Linux systems is truncated to 16 characters:
// https://bugzilla.redhat.com/show_bug.cgi?id=429565
// Since 'args' contains the full path to the executable, even if truncated, searching will work as desired.
public static get psLinuxCommand(): string { return `ps axww -o pid=,comm=${PsProcessParser.commColumnTitle},args=`; }
public static get psDarwinCommand(): string { return `ps axww -o pid=,comm=${PsProcessParser.commColumnTitle},args= -c`; }
public static get psToyboxCommand(): string { return `ps -A -o pid=,comm=${PsProcessParser.commColumnTitle},args=`; }
コメントでBSDに言及されているが選択可能なOSはLinux/Darwin/Toyboxしかない。
適当にSSH接続してコマンド結果を受け取る。
npm install ssh2
npm i --save-dev @types/ssh2
import { Client, ClientChannel } from "ssh2";
・・・
let disposable = vscode.commands.registerCommand(
"sample2.helloWorld",
() => {
const conn = new Client();
conn.on("ready", () => {
conn.exec(
"ls -al",
(err: Error | undefined, stream: ClientChannel) => {
if (err) {
vscode.window.showInformationMessage(
`error: ${err}`
);
return;
}
stream.on("data", (data: Buffer) => {
vscode.window.showInformationMessage(
`stdout: ${data}`
);
});
stream.stderr.on("data", (data: Buffer) => {
vscode.window.showInformationMessage(
`stderr: ${data}`
);
});
stream.on("close", (code: number, signal: string) => {
conn.end();
vscode.window
.showQuickPick([
{
label: "item1",
description: "desc1",
},
{
label: "item2",
description: "desc2",
},
{
label: "item3",
description: "desc3",
},
])
.then((item) => {
if (item !== undefined) {
vscode.window.showInformationMessage(
`selected item is ${item.label}`
);
}
});
});
}
);
}).connect({
host: "ホスト名",
port: ポート番号,
username: "ユーザー名",
password: "パスワード",
});
}
);
ls -al
を実行してToastで結果を表示し、CloseのタイミングでPickを表示
Callbackをシンプルな構造にするやり方がよくわからない。
ChatGPT
interface SSHConfig {
host: string;
port: number;
username: string;
password: string;
}
function connectSSH(config: SSHConfig): Promise<Client> {
return new Promise((resolve, reject) => {
const conn = new Client();
conn.on("ready", () => {
resolve(conn);
})
.on("error", (err) => {
reject(err);
})
.connect(config);
});
}
function executeCommand(conn: Client, command: string): Promise<string> {
return new Promise((resolve, reject) => {
conn.exec(command, (err: Error | undefined, stream: ClientChannel) => {
if (err) {
reject(err);
return;
}
let output = "";
stream.on("data", (data: Buffer) => {
output += data.toString();
});
stream.on("close", (code: number, signal: string) => {
if (code === 0) {
resolve(output);
} else {
reject(
new Error(
`Command execution failed with code ${code} and signal ${signal}`
)
);
}
});
});
});
}
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "sample2" is now active!');
let disposable = vscode.commands.registerCommand(
"sample2.helloWorld",
async () => {
try {
const conn = await connectSSH({
host: "ホスト名",
port: ポート番号,
username: "ユーザー名",
password: "パスワード",
});
const commandOutput = await executeCommand(conn, "ls -al");
conn.end();
vscode.window.showInformationMessage(
`result: ${commandOutput}`
);
vscode.window
.showQuickPick([
{
label: "item1",
description: "desc1",
},
{
label: "item2",
description: "desc2",
},
{
label: "item3",
description: "desc3",
},
])
.then((item) => {
if (item !== undefined) {
vscode.window.showInformationMessage(
`selected item is ${item.label}`
);
}
});
} catch (error) {
vscode.window.showErrorMessage(`error: ${error}`);
}
}
);
context.subscriptions.push(disposable);
}
リターンコードや標準エラーを拾えるように修正
interface SSHCmdResult {
stdout: string | null;
stderr: string | null;
code: number;
signal: string;
}
・・・
function executeCommand(conn: Client, command: string): Promise<SSHCmdResult> {
return new Promise((resolve, reject) => {
conn.exec(command, (err: Error | undefined, stream: ClientChannel) => {
if (err) {
reject(err);
return;
}
let stdout: string | null = null;
stream.on("data", (data: Buffer) => {
if (stdout === null) {
stdout = "";
}
stdout += data.toString();
});
let stderr: string | null = null;
stream.stderr.on("data", (data: Buffer) => {
if (stderr === null) {
stderr = "";
}
stderr += data.toString();
});
stream.on("close", (code: number, signal: string) => {
resolve({
stdout: stdout,
stderr: stderr,
code: code,
signal: signal,
});
});
});
});
}
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "sample2" is now active!');
let disposable = vscode.commands.registerCommand(
"sample2.helloWorld",
async () => {
try {
const conn = await connectSSH({
host: "",
port: ,
username: "",
password: "",
});
const cmdResult = await executeCommand(conn, "ls -al");
conn.end();
vscode.window.showInformationMessage(
`stdout: ${cmdResult.stdout}, code: ${cmdResult.code}`
);
・・・
Ubuntuの場合
ps axww | awk '{$2=$3=$4=""; print $1 substr($0, 5)}'
でプロセスIDとコマンド全文字列が取れる。$2-$4の空白書き換えがやや気持ち悪いがChatGPTの回答。
NetBSD(i386)でも同じコマンドで取れることを確認。
LinuxかNetBSDならプロセス一覧を取得してPickItemに追加し、選択したらメッセージを出す。
const getProcessCommand: string =
`if [ "$(uname)" = "Linux" ] || [ "$(uname)" = "NetBSD" ]; then ` +
`ps axww | awk '{$2=$3=$4=""; print $1 substr($0, 5)}'; ` +
`else echo "Unsupported OS: $(uname)" >&2 && exit 1; fi`;
const processRegExp: RegExp = new RegExp(`^\\s*([0-9]+)\\s+(.*)$`);
function parseProcessOverSSH(conn: Client): Promise<ProcessInfo[]> {
return new Promise(async (resolve, reject) => {
const psResult = await executeCommand(conn, getProcessCommand);
if (psResult.code !== 0) {
reject(
`get processes over SSH failed: ${psResult.code} stderr: ${psResult.stderr}`
);
return;
}
if (psResult.stdout === null) {
reject("get processes over SSH failed: stdout is null");
return;
}
let processes: ProcessInfo[] = [];
const stdOutLines: string[] = psResult.stdout.split("\n");
for (let i: number = 1; i < stdOutLines.length; i++) {
const matches: RegExpExecArray | null = processRegExp.exec(
stdOutLines[i]
);
if (matches) {
processes.push({
id: parseInt(matches[1].trim()),
command: matches[2].trim(),
});
}
}
resolve(processes);
});
}
・・・
let disposable = vscode.commands.registerCommand(
"sample2.helloWorld",
async () => {
try {
const conn = await connectSSH({
host: "",
port: ,
username: "",
password: "",
});
const processes = await parseProcessOverSSH(conn);
conn.end();
if (processes.length === 0) {
vscode.window.showErrorMessage(`no process`);
return;
}
const items: vscode.QuickPickItem[] = processes.map((e) => {
const processItem: vscode.QuickPickItem = {
label: e.id.toString(),
description: e.command,
};
return processItem;
});
vscode.window.showQuickPick(items).then((item) => {
if (item !== undefined) {
vscode.window.showInformationMessage(
`selected item is ${item.label} command: ${item.description}`
);
}
});
C/C++公式拡張機能の処理を参考。
Native Debugのsrc/gdb.ts::GDBDebugSession
の継承元はnode_modules/vscode-debugadapter/lib/debugSession.d.ts::DebugSession
。シンプルに見えたのはすでにあるフレームワークを継承していただけのため。
Native Debugの作りがよくわからない。SSH処理のコア部分はあるが、ここにPickの関数を入れようとして非同期かコールバックを組み込んでもデバッグ対象のアタッチのlaunchがすぐ終わって始まらない。デバッグできないから解析が難しい。
そもそもエンドポイントがわからない。DebugSession.run(GDBDebugSession);
が最初?
実はC/C++公式拡張機能から読み解いたほうが早いかもしれない。
できた。src/frontend/extension.ts
側に手を入れる必要がある。とりあえずつなぐだけでssh処理の再利用はしていない。
@@ -20,7 +20,8 @@
"activationEvents": [
"onCommand:code-debug.examineMemoryLocation",
"onCommand:code-debug.getFileNameNoExt",
- "onCommand:code-debug.getFileBasenameNoExt"
+ "onCommand:code-debug.getFileBasenameNoExt",
+ "onCommand:code-debug.pickRemoteNativeProcess"
],
"categories": [
"Debuggers"
@@ -3,6 +3,33 @@ import * as net from "net";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
+import { Client, ClientChannel } from "ssh2";
+
+interface SSHConfig {
+ host: string;
+ port: number;
+ username: string;
+ password: string;
+}
+
+interface SSHCmdResult {
+ stdout?: string;
+ stderr?: string;
+ code: number;
+ signal: string;
+}
+
+interface ProcessInfo {
+ id: number;
+ command: string;
+}
+
+const getProcessCommand: string =
+ `if [ "$(uname)" = "Linux" ] || [ "$(uname)" = "NetBSD" ]; then ` +
+ `ps axww | awk '{$2=$3=$4=""; print $1 substr($0, 5)}'; ` +
+ `else echo "Unsupported OS: $(uname)" >&2 && exit 1; fi`;
+
+const processRegExp: RegExp = new RegExp(`^\\s*([0-9]+)\\s+(.*)$`);
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider("debugmemory", new MemoryContentProvider()));
@@ -25,8 +52,127 @@ export function activate(context: vscode.ExtensionContext) {
const ext = path.extname(fileName);
return fileName.substring(0, fileName.length - ext.length);
}));
+ context.subscriptions.push(vscode.commands.registerCommand("code-debug.pickRemoteNativeProcess", (any) => ShowAttachEntries(any)));
+}
+function connectSSH(config: SSHConfig): Promise<Client> {
+ return new Promise((resolve, reject) => {
+ const conn = new Client();
+ conn.on("ready", () => {
+ resolve(conn);
+ })
+ .on("error", (err) => {
+ reject(err);
+ })
+ .connect(config);
+ });
}
+function executeCommand(conn: Client, command: string): Promise<SSHCmdResult> {
+ return new Promise((resolve, reject) => {
+ conn.exec(command, (err: Error | undefined, stream: ClientChannel) => {
+ if (err) {
+ reject(err);
+ return;
+ }
+
+ let stdout: string | undefined = undefined;
+ stream.on("data", (data: Buffer) => {
+ if (stdout === undefined) {
+ stdout = "";
+ }
+ stdout += data.toString();
+ });
+
+ let stderr: string | undefined = undefined;
+ stream.stderr.on("data", (data: Buffer) => {
+ if (stderr === undefined) {
+ stderr = "";
+ }
+ stderr += data.toString();
+ });
+
+ stream.on("close", (code: number, signal: string) => {
+ resolve({
+ stdout: stdout,
+ stderr: stderr,
+ code: code,
+ signal: signal,
+ });
+ });
+ });
+ });
+}
+
+function parseProcessOverSSH(conn: Client): Promise<ProcessInfo[]> {
+ return new Promise((resolve, reject) => {
+ executeCommand(conn, getProcessCommand).then((psResult: SSHCmdResult) => {
+ if (psResult.code !== 0) {
+ reject(
+ `get processes over SSH failed: ${psResult.code} stderr: ${psResult.stderr}`
+ );
+ return;
+ }
+ if (psResult.stdout === undefined) {
+ reject("get processes over SSH failed: stdout is null");
+ return;
+ }
+ const processes: ProcessInfo[] = [];
+ const stdOutLines: string[] = psResult.stdout.split("\n");
+ for (let i: number = 1; i < stdOutLines.length; i++) {
+ const matches: RegExpExecArray | null = processRegExp.exec(
+ stdOutLines[i]
+ );
+ if (matches) {
+ processes.push({
+ id: parseInt(matches[1].trim()),
+ command: matches[2].trim(),
+ });
+ }
+ }
+ resolve(processes);
+ });
+ });
+}
+
+async function ShowAttachEntries(any: any): Promise<String> {
+ try {
+ const conn = await connectSSH({
+ host: any.ssh.host,
+ port: any.ssh.port || 22,
+ username: any.ssh.user,
+ password: any.ssh.password,
+ });
+ const processes = await parseProcessOverSSH(conn);
+ conn.end();
+ if (processes.length === 0) {
+ vscode.window.showErrorMessage("no process");
+ return;
+ }
+ const items: vscode.QuickPickItem[] = processes.map((e) => {
+ const processItem: vscode.QuickPickItem = {
+ label: e.id.toString(),
+ description: e.command,
+ };
+ return processItem;
+ });
+ const pid: String = await selectProcess(items);
+ return pid;
+ } catch (error) {
+ throw Error(`error: ${error}`);
+ }
+}
+
+function selectProcess(items: vscode.QuickPickItem[]): Promise<String> {
+ return new Promise((resolve, reject) => {
+ vscode.window.showQuickPick(items).then((item) => {
+ if (item !== undefined) {
+ resolve(item.label);
+ return;
+ }
+ reject("no item selected");
+ });
+ });
+}
const memoryLocationRegex = /^0x[0-9a-f]+$/;
function getMemoryRange(range: string) {
あとはlaunch.json
のconfigで"target": "${command:pickRemoteProcess}"
とすればF5押下時にリモートのプロセス一覧を表示して、選択すればそのプロセスにアタッチして起動できる。
とりあえずUbuntuは問題なくアタッチできたが、NetBSD(i386)はプロセス一覧から選べるものの、gdb-8.1.1だと以下エラーになりプロセスも落ちる。
Failed to SSH: Couldn't get registers: Device busy. (from target-attach 711)
launch.json
で"printCalls": true, "showDevDebugOutput": true
Running /usr/local/bin/gdb over ssh...
1-gdb-set target-async on
2-list-features
3-environment-directory "/root/sample"
4-environment-cd "/root/sample"
5-target-attach 706
GDB -> App: {"outOfBandRecord":[{"isStream":false,"type":"notify","asyncClass":"thread-group-added","output":[["id","i1"]],"content":""}]}
GDB -> App: {"token":1,"outOfBandRecord":[],"resultRecords":{"resultClass":"done","results":[]}}
GDB -> App: {"token":2,"outOfBandRecord":[],"resultRecords":{"resultClass":"done","results":[["features",["frozen-varobjs","pending-breakpoints","thread-info","data-read-memory-bytes","breakpoint-notifications","ada-task-info","language-option","info-gdb-mi-command","undefined-command-error-code","exec-run-start-option"]]]}}
GDB -> App: {"token":3,"outOfBandRecord":[],"resultRecords":{"resultClass":"done","results":[["source-path","/root/sample:$cdir:$cwd"]]}}
GDB -> App: {"token":4,"outOfBandRecord":[],"resultRecords":{"resultClass":"done","results":[]}}
GDB -> App: {"outOfBandRecord":[{"isStream":false,"type":"notify","asyncClass":"thread-group-started","output":[["id","i1"],["pid","706"]],"content":""}]}
GDB -> App: {"outOfBandRecord":[{"isStream":false,"type":"notify","asyncClass":"thread-created","output":[["id","1"],["group-id","i1"]],"content":""}]}
6-thread-info
GDB -> App: {"token":5,"outOfBandRecord":[],"resultRecords":{"resultClass":"error","results":[["msg","Couldn't get registers: Device busy."]]}}
エラー判定はsrc/backend/mi_parse.ts::resultRecordRegex
で正規表現を用いている。
const resultRecordRegex = /^(\d*)\^(done|running|connected|error|exit)/;
強引にerror
を排除したら最初だけブレークポイントで止まったが実行中のところで止まったが、一度でも継続するかデバッグを止めるとそのプロセスはもうデバッグできなくなる。追加のブレークポイントも効かない。
なんらかのプロセス操作コマンドが通っていないと思われる。
src/backend/mi2/mi2.ts::onOutput()
にthis.log("stdout", `
str: ${str}`
);入れて動作を見たら、continueしたあとに返ってきてない。
GDB -> App: {"token":66,"outOfBandRecord":[],"resultRecords":{"resultClass":"done","results":[["variables",[[["name","target_value"],["type","int"],["value","10"]],[["name","current_value"],["type","int"],["value","0"]]]]]}}
67-exec-continue
str: 67^running
*running,thread-id="all"
(gdb)
GDB -> App: {"token":67,"outOfBandRecord":[],"resultRecords":{"resultClass":"running","results":[]}}
GDB -> App: {"outOfBandRecord":[{"isStream":false,"type":"exec","asyncClass":"running","output":[["thread-id","all"]],"content":""}]}
sendRaw
で送るデータがそのままgdb
のインタラクティブへのコマンドになっている。数字-**-**
らしい。
gdb
直接起動で同じコマンドで返って来ていない。
/usr/local/bin/gdb -q --interpreter=mi2
1-gdb-set target-async on
2-list-features
3-environment-directory "/root/sample"
4-environment-cd "/root/sample"
5-target-attach 1965
6-thread-info
7-thread-info
8-stack-info-depth --thread 1
9-stack-info-depth --thread 1
10-stack-list-frames --thread 1 0 2
11-stack-list-frames --thread 1 0 2
12-stack-list-variables --thread 1 --frame 2 --simple-values
13-exec-continue
そもそも起動時のブレークポイントが設定できていない。ではなぜいきなり実行中のところで止まっているのか?
src/backend/mi2/mi2.ts::MI2::ssh
で無理やりブレークポイントをハードコーディングしたら止まるようになった。"stopAtConnect": false, "stopAtEntry": false,
を設定していても起動時に実行中箇所で止まる。
if (attach) {
// Attach to local process
promises.push(this.sendCommand("target-attach " + target));
promises.push(this.sendCommand("break-insert sample.c:10"));
} else if (procArgs && procArgs.length)
・・・
VSCode上のブレークポイントの設定変更とリンクできればうまく行けるかもしれない。
ただしデタッチするとプロセスがkillされる。
そもそものブレークポイント設定変更の継承イベントを検知できていない。
ログを入れて確認。
protected override setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void {
let path = args.source.path;
this.miDebugger.log("stdout", args.breakpoints.map((brk => `${path}:${brk.line}`)).join("\n"));
if (this.isSSH) {
Ubuntuの場合はブレークポイント変更時にthis.miDebugger.log
でログが取れたがNetBSD(i386)だと出ない。Native Debug側に渡すまでのいずれかの処理ケースで何らかのイベントを送出出来ないパターンに入って届かなくなっているようにみえる。
もしかしてソケット間通信ができていないせい?net.Server
はNode.js標準モジュールだからこれ自体がi386アーキやBSD系に対応していないとか?
protected commandServer: net.Server;
・・・
if (!fs.existsSync(systemPath.join(os.tmpdir(), "code-debug-sockets")))
fs.mkdirSync(systemPath.join(os.tmpdir(), "code-debug-sockets"));
this.commandServer.listen(this.serverPath = systemPath.join(os.tmpdir(), "code-debug-sockets", ("Debug-Instance-" + Math.floor(Math.random() * 36 * 36 * 36 * 36).toString(36)).toLowerCase()));
以下でUbuntuだとログが出るがNetBSDでは出ない。
promises.push(...autorun.map(value => { return this.sendUserInput(value); }));
Promise.all(promises).then(() => {
this.log("stdout", new Error().stack);
this.emit("debug-ready");
resolve(undefined);
逆にPromise.all
の中で呼んでいるthis.emit("debug-ready")
を外に出したらブレークポイントで止まるようになったので、このpromises
のいずれかが返ってきていない模様。
promises.push(...autorun.map(value => { return this.sendUserInput(value); }));
Promise.all(promises).then(() => {
this.log("stdout", new Error().stack);
resolve(undefined);
}, reject);
this.emit("debug-ready");
アタッチコマンドの実行Promiseがresolvedされていない。アタッチ時のエラーを無視しているせい?
sendCommand(command: string, suppressFailure: boolean = false): Thenable<MINode> {
const sel = this.currentToken++;
return new Promise((resolve, reject) => {
this.handlers[sel] = (node: MINode) => {
if (node && node.resultRecords && node.resultRecords.resultClass === "error") {
if (suppressFailure) {
this.log("stderr", `WARNING: Error executing command '${command}'`);
this.log("stdout", `resolved executing command with warning '${command}'`);
resolve(node);
} else {
this.log("stderr", `rejected executing command '${command}' ${node.result("msg")}`);
reject(new MIError(node.result("msg") || "Internal error", command));
}
} else {
this.log("stdout", `resolved executing command '${command}'`);
resolve(node);
}
};
this.sendRaw(sel + "-" + command);
Ubuntuの場合
resolved executing command 'gdb-set target-async on'
resolved executing command 'list-features'
resolved executing command 'environment-directory "/work"'
resolved executing command 'environment-cd "/work"'
resolved executing command 'target-attach 25'
resolved executing command 'break-insert -f "/work/sample.c:9"'
resolved executing command 'thread-info'
resolved executing command 'thread-info'
resolved executing command 'exec-continue'
resolved executing command 'thread-info'
resolved executing command 'thread-info'
resolved executing command 'stack-info-depth --thread 1'
resolved executing command 'stack-list-frames --thread 1 0 0'
resolved executing command 'stack-list-variables --thread 1 --frame 0 --simple-values'
resolved executing command 'exec-continue'
NetBSDの場合
resolved executing command 'gdb-set target-async on'
resolved executing command 'list-features'
resolved executing command 'environment-directory "/root/sample"'
resolved executing command 'environment-cd "/root/sample"'
resolved executing command 'thread-info'
resolved executing command 'thread-info'
resolved executing command 'stack-info-depth --thread 1'
resolved executing command 'stack-info-depth --thread 1'
resolved executing command 'stack-list-frames --thread 1 0 2'
resolved executing command 'stack-list-frames --thread 1 0 2'
resolved executing command 'stack-list-variables --thread 1 --frame 2 --simple-values'
resolved executing command 'exec-continue'
正規表現のエラーを元に戻し、src/backend/mi2/mi2.ts::MI2::onOutput
にログを入れて確認。
onOutput(str: string) {
this.log("stdout", `on output: ${str}`);
on output: 5^error,msg="Couldn't get registers: Device busy."
(gdb)
GDB -> App: {"token":5,"outOfBandRecord":[],"resultRecords":{"resultClass":"error","results":[["msg","Couldn't get registers: Device busy."]]}}
rejected executing command 'target-attach 1016' Couldn't get registers: Device busy.
このエラーを無視してdoneに書き換えれば進みそう?
sendCommand
の第二引数をtrue
にしておくとエラー発生時にもrejectさせなくなる。
これでデバッグ開始ができるようになる。
if (attach) {
// Attach to local process
promises.push(this.sendCommand("target-attach " + target, true));
・・・
sendCommand(command: string, suppressFailure: boolean = false): Thenable<MINode> {
残りはデタッチ時にkillされない対策のみ。
そもそも追加のブレークポイントなどが設定できないあたり、continue状態でgdbコマンドが飛ばせていないようにみえる。デタッチも同様にコマンドが飛ばずに予期せぬエラーで落ちている?gdb側の修正が必要かもしれない。
gdbプロセスに対してkill -SIGINT
を送信すれば一応割り込める。これをinterrupt
代わりに送れないか?
代わりに送ることはできるが、そのタイミングで現在行で止まるので、ブレークポイント変更のたびに変更箇所以外で止まるところがやや厄介。その代わりcontinue状態から復帰できるのでgdbコマンドが期待通り送信できる。
一方で、デタッチのタイミングはまだ解決できず、killされない代わりに再デバッグができなくなった。
src/backend/mi2/mi2.ts::MI2::detach
を非同期関数にし、interrupt
した後にawait this.sendCommand("target-detach");
を実行すれば正常にデタッチできる。再アタッチデバッグ可能。
Ubuntuでもそもそもbreak-insert
を単純送信しただけではレスポンスがなかった。何か別の割り込みをしている模様。
NetBSDでbreak-insert
が送信できないのは、最初のコマンド1-gdb-set target-async on
で非同期モードが有効になっていないせいの模様。
target-async
はmi-async
の非推奨エイリアスらしい。
有効になっていると返ってきているが実際にはなっていない。おそらくOSへの対応ができていないと思われる。
1-gdb-set target-async on
1^done
(gdb)
2-gdb-show target-async
2^done,value="on"
以上。