Open40

VSCode拡張機能Native Debug雑記

素人の戯言素人の戯言

目的

  • NetBSD(i386)へSSH経由でのアタッチモードによるgdbデバッグを、起動中プロセス一覧から選んで実行できるかを確認(MS公式C/C++拡張機能同等)

前提

  • VSCode拡張機能開発経験なし

MS公式C/C++拡張機能不使用の理由

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 -SIGINTgdbに送って止める
    • 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 .
素人の戯言素人の戯言
launch.json
{
  "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でプログラムのデバッグもできるようになる。チェック処理がデバッグモードだと正しく動作しない?

src/gdb.ts
@@ -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を調査。

素人の戯言素人の戯言

サンプル拡張機能の修正

src/extension.ts
    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はいくつかプロパティを持たせられる。

最低限labeldescriptionさえあれば、アタッチプロセスID、プログラムのPickができるはず。

src/extension.ts
    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++公式拡張機能で使っているプロセス一覧用のコマンド

Extension/src/Debugger/attachToProcess.ts
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
src/extension.ts
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

src/extension.ts
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);
}
素人の戯言素人の戯言

リターンコードや標準エラーを拾えるように修正

src/extension.ts
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に追加し、選択したらメッセージを出す。

src/extension.ts
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処理の再利用はしていない。

package.json
@@ -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"
src/frontend/extension.ts
@@ -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で正規表現を用いている。

src/backend/mi_parse.ts
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,を設定していても起動時に実行中箇所で止まる。

src/backend/mi2/mi2.ts
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される。

素人の戯言素人の戯言

そもそものブレークポイント設定変更の継承イベントを検知できていない。
ログを入れて確認。

src/mibase.ts
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系に対応していないとか?

src/mibase.ts
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では出ない。

src/backend/mi2/mi2.ts
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されていない。アタッチ時のエラーを無視しているせい?

src/backend/mi2/mi2.ts
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にログを入れて確認。

src/backend/mi2/mi2.ts
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させなくなる。
これでデバッグ開始ができるようになる。

src/backend/mi2/mi2.ts
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-asyncmi-asyncの非推奨エイリアスらしい。

有効になっていると返ってきているが実際にはなっていない。おそらくOSへの対応ができていないと思われる。

1-gdb-set target-async on
1^done
(gdb)
2-gdb-show target-async
2^done,value="on"

以上。