[Lldb-commits] [lldb] DO NOT MERGE - Android support in LLDB-DAP (PR #182504)

Gabriele Mondada via lldb-commits lldb-commits at lists.llvm.org
Thu Mar 12 09:54:21 PDT 2026


https://github.com/gmondada updated https://github.com/llvm/llvm-project/pull/182504

>From 786ca269efdeac39854b753032cc150f14617753 Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Sat, 13 Dec 2025 20:24:46 +0100
Subject: [PATCH 01/18] [lldb-dap] add support for the ADB (Android Debug
 Bridge) protocol

---
 .../lldb-dap/src-ts/android/adb-client.ts     | 192 ++++++++++++
 .../lldb-dap/src-ts/android/adb-connection.ts | 280 ++++++++++++++++++
 .../src-ts/android/apk-debug-session.ts       | 230 ++++++++++++++
 .../lldb-dap/src-ts/android/connection.ts     | 233 +++++++++++++++
 lldb/tools/lldb-dap/src-ts/android/env.ts     |  57 ++++
 lldb/tools/lldb-dap/src-ts/android/jdwp.ts    | 150 ++++++++++
 6 files changed, 1142 insertions(+)
 create mode 100644 lldb/tools/lldb-dap/src-ts/android/adb-client.ts
 create mode 100644 lldb/tools/lldb-dap/src-ts/android/adb-connection.ts
 create mode 100644 lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
 create mode 100644 lldb/tools/lldb-dap/src-ts/android/connection.ts
 create mode 100644 lldb/tools/lldb-dap/src-ts/android/env.ts
 create mode 100644 lldb/tools/lldb-dap/src-ts/android/jdwp.ts

diff --git a/lldb/tools/lldb-dap/src-ts/android/adb-client.ts b/lldb/tools/lldb-dap/src-ts/android/adb-client.ts
new file mode 100644
index 0000000000000..a2c31bdd3e7e9
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/android/adb-client.ts
@@ -0,0 +1,192 @@
+import * as process from 'node:process';
+import { AdbConnection } from './adb-connection';
+import { Connection } from './connection';
+import Jdwp from './jdwp';
+
+/**
+ * ADB (Android Debug Bridge) client.
+ * An ADB client has access to multiple Android devices.
+ * Many functionalities exposed by this class need the target device
+ * to be designated before invoking that functionality.
+ * The target device can be selected by invoking setDeviceId() or autoDetectDeviceId().
+ * This client expects the ADB daemon to be running on the local machine.
+ */
+export class AdbClient {
+
+    private deviceId: string | undefined = undefined;
+
+    async getDeviceList(): Promise<string[]> {
+        const connection = await this.createAdbConnection();
+        try {
+            const devices = await connection.getDeviceList();
+            return devices;
+        } finally {
+            connection.close();
+        }
+    }
+
+    async autoDetectDeviceId(): Promise<void> {
+        const connection = await this.createAdbConnection();
+        try {
+            const devices = await connection.getDeviceList();
+            if (devices.length === 1) {
+                this.deviceId = devices[0];
+                return;
+            }
+            if (devices.length === 0) {
+                throw new Error('No connected Android devices found');
+            }
+            throw new Error('Multiple connected Android devices found, please specify a device ID');
+        } finally {
+            connection.close();
+        }
+    }
+
+    setDeviceId(deviceId: string) {
+        this.deviceId = deviceId;
+    }
+
+    getDeviceId(): string {
+        if (this.deviceId === undefined) {
+            throw new Error('Device ID is not set');
+        }
+        return this.deviceId;
+    }
+
+    async shellCommand(command: string): Promise<void> {
+        const deviceId = this.getDeviceId();
+        const connection = await this.createAdbConnection();
+        try {
+            await connection.setTargetDevice(deviceId);
+            await connection.shellCommand(command);
+        } finally {
+            connection.close();
+        }
+    }
+
+    async shellCommandToString(command: string): Promise<string> {
+        const deviceId = this.getDeviceId();
+        const connection = await this.createAdbConnection();
+        try {
+            await connection.setTargetDevice(deviceId);
+            const output = await connection.shellCommandToString(command);
+            return output;
+        } finally {
+            connection.close();
+        }
+    }
+
+    async shellCommandToStream(command: string, writer: (data: Uint8Array) => Promise<void>, abort: AbortSignal): Promise<void> {
+        const deviceId = this.getDeviceId();
+        const connection = await this.createAdbConnection();
+        abort.addEventListener('abort', () => {
+            connection.close();
+        });
+        try {
+            await connection.setTargetDevice(deviceId);
+            await connection.shellCommandToStream(command, writer);
+        } finally {
+            connection.close();
+        }
+    }
+
+    async getPid(packageName: string): Promise<number> {
+        const deviceId = this.getDeviceId();
+        const connection = await this.createAdbConnection();
+        try {
+            await connection.setTargetDevice(deviceId);
+            const pid = await connection.getPid(packageName);
+            return pid;
+        } finally {
+            connection.close();
+        }
+    }
+
+    async addPortForwarding(remotePort: number | string, localPort: number = 0): Promise<number> {
+        const deviceId = this.getDeviceId();
+        const connection = await this.createAdbConnection();
+        try {
+            const port = await connection.addPortForwarding(deviceId, remotePort, localPort);
+            return port;
+        } finally {
+            connection.close();
+        }
+    }
+
+    async removePortForwarding(localPort: number): Promise<void> {
+        const deviceId = this.getDeviceId();
+        const connection = await this.createAdbConnection();
+        try {
+            await connection.removePortForwarding(deviceId, localPort);
+        } finally {
+            connection.close();
+        }
+    }
+
+    async getPortForwardingList(): Promise<{ device: string, localPort: string, remotePort: string }[]> {
+        const connection = await this.createAdbConnection();
+        try {
+            return await connection.getPortForwardingList();
+        } finally {
+            connection.close();
+        }
+    }
+
+    async dismissWaitingForDebuggerDialog(pid: number): Promise<void> {
+        const port = await this.addPortForwarding(`jdwp:${pid}`);
+        try {
+            const connection = new Connection();
+            await connection.connect('127.0.0.1', port);
+            try {
+                await Jdwp.handshake(connection);
+                // Dalvik is able to reply to handshake and DDM commands (command set 199)
+                // without loading the JDWP agent.
+                // By sending a version command, we force it to load the JDWP agent, which
+                // causes the "waiting for debugger" popup to be dismissed.
+                const version = await Jdwp.getVersion(connection);
+                console.log("JDWP Version:", JSON.stringify(version));
+                // TODO: understand why we need to keep the connection active for a while
+                await new Promise(resolve => setTimeout(resolve, 200));
+            } finally {
+                connection.close();
+            }
+        } finally {
+            await this.removePortForwarding(port);
+        }
+    }
+
+    async pushData(data: Uint8Array, remoteFilePath: string): Promise<void> {
+        const deviceId = this.getDeviceId();
+        const connection = await this.createAdbConnection();
+        try {
+            await connection.setTargetDevice(deviceId);
+            await connection.enterSyncMode();
+            await connection.pushData(data, remoteFilePath);
+        } finally {
+            connection.close();
+        }
+    }
+
+    async pushFile(localFilePath: string, remoteFilePath: string): Promise<void> {
+        const deviceId = this.getDeviceId();
+        const connection = await this.createAdbConnection();
+        try {
+            await connection.setTargetDevice(deviceId);
+            await connection.enterSyncMode();
+            await connection.pushFile(localFilePath, remoteFilePath);
+        } finally {
+            connection.close();
+        }
+    }
+
+    private getAdbPort(): number {
+        const envPort = process.env.ANDROID_ADB_SERVER_PORT;
+        return envPort ? parseInt(envPort, 10) : 5037;
+    }
+
+    private async createAdbConnection(): Promise<AdbConnection> {
+        const connection = new AdbConnection();
+        await connection.connect('127.0.0.1', this.getAdbPort());
+        return connection;
+    }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/android/adb-connection.ts b/lldb/tools/lldb-dap/src-ts/android/adb-connection.ts
new file mode 100644
index 0000000000000..f1bd254db05a3
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/android/adb-connection.ts
@@ -0,0 +1,280 @@
+import * as fs from "node:fs/promises";
+import { Connection } from "./connection";
+
+/**
+ * This class is an implementation of the ADB (Android Debug Bridge) protocol.
+ * It implements the fundamental building blocks, but it doesn't assemble them
+ * into a complete client.
+ * For a complete ADB client, see AdbClient.
+ */
+export class AdbConnection extends Connection {
+
+    async sendAdbMessage(packet: string): Promise<void> {
+        const enc = new TextEncoder();
+        const data = enc.encode(packet);
+        if (data.length > 0xFFFF) {
+            throw new Error('Packet size exceeds maximum allowed length of 65535 bytes');
+        }
+        const head = enc.encode(data.length.toString(16).padStart(4, '0'));
+        await this.write(head);
+        await this.write(data);
+    }
+
+    async sendDeviceMessage(deviceId: string, packet: string): Promise<void> {
+        const msg = `host-serial:${deviceId}:${packet}`;
+        await this.sendAdbMessage(msg);
+    }
+
+    async readAdbMessage(): Promise<Uint8Array> {
+        const head = await this.read(4);
+        if (head.length !== 4) {
+            throw new Error('Incomplete ADB message head received');
+        }
+        const dec = new TextDecoder();
+        const size = parseInt(dec.decode(head), 16);
+        const message = await this.read(size);
+        if (message.length !== size) {
+            throw new Error('Incomplete ADB message received');
+        }
+        return message;
+    }
+
+    async readResponseStatus(): Promise<void> {
+        const data = await this.read(4);
+        if (data.length !== 4) {
+            throw new Error('Incomplete ADB message head received');
+        }
+        const dec = new TextDecoder();
+        const status = dec.decode(data);
+        switch (status) {
+            case 'OKAY':
+                return;
+            case 'FAIL':
+                const errorMsg = await this.readAdbMessage();
+                throw new AdbCommandFailed(`ADB command failed: ${dec.decode(errorMsg)}`);
+            default:
+                throw new Error(`Unknown ADB response status: ${status}`);
+        }
+    }
+
+    /**
+     * The ADB server closes the connection after executing this command.
+     */
+    async getDeviceList(): Promise<string[]> {
+        await this.sendAdbMessage('host:devices');
+        await this.readResponseStatus();
+        const data = await this.readAdbMessage();
+
+        const dec = new TextDecoder();
+        const response = dec.decode(data);
+        const deviceList: string[] = [];
+        const lines = response.split('\n');
+        for (const line of lines) {
+            const [device] = line.split('\t');
+            if (device) {
+                deviceList.push(device);
+            }
+        }
+        return deviceList;
+    }
+
+    async setTargetDevice(deviceId: string): Promise<void> {
+        await this.sendAdbMessage(`host:transport:${deviceId}`);
+        await this.readResponseStatus();
+    }
+
+    /**
+     * The ADB server closes the connection after executing this command.
+     */
+    async shellCommandToStream(command: string, writer: (data: Uint8Array) => Promise<void>): Promise<void> {
+        if (command === "") {
+            throw new Error('Shell command cannot be empty');
+        }
+        const message = `shell:${command}`;
+        await this.sendAdbMessage(message);
+        await this.readResponseStatus();
+        for (;;) {
+            const data = await this.read();
+            if (data.length === 0) {
+                break;
+            }
+            await writer(data);
+        }
+    }
+
+    async shellCommandToString(command: string): Promise<string> {
+        const output: Uint8Array[] = [];
+        const writer = async (data: Uint8Array) => {
+            output.push(data);
+        };
+        await this.shellCommandToStream(command, writer);
+        const totalLength = output.reduce((sum, buf) => sum + buf.length, 0);
+        const combined = new Uint8Array(totalLength);
+        let offset = 0;
+        for (const buf of output) {
+            combined.set(buf, offset);
+            offset += buf.length;
+        }
+        const dec = new TextDecoder();
+        return dec.decode(combined);
+    }
+
+    async shellCommand(command: string): Promise<void> {
+        const writer = async (data: Uint8Array) => {};
+        await this.shellCommandToStream(command, writer);
+    }
+
+    /**
+     * The ADB server closes the connection after executing this command.
+     */
+    async getPid(packageName: string): Promise<number> {
+        const output = await this.shellCommandToString(`pidof ${packageName}`);
+        const pid = parseInt(output.trim(), 10);
+        if (isNaN(pid)) {
+            throw new AdbCommandFailed(`Failed to get PID for package: ${packageName}`);
+        }
+        return pid;
+    }
+
+    async addPortForwarding(deviceId: string, remotePort: number | string, localPort: number = 0): Promise<number> {
+        if (typeof remotePort === 'number') {
+            remotePort = `tcp:${remotePort}`;
+        }
+        const message = `forward:tcp:${localPort};${remotePort}`;
+        await this.sendDeviceMessage(deviceId, message);
+        await this.readResponseStatus();
+        await this.readResponseStatus();
+        const result = await this.readAdbMessage();
+        const dec = new TextDecoder();
+        const port = parseInt(dec.decode(result), 10);
+        if (isNaN(port)) {
+            throw new Error('Failed to add port forwarding');
+        }
+        return port;
+    }
+
+    async removePortForwarding(deviceId: string, localPort: number): Promise<void> {
+        const message = `killforward:tcp:${localPort}`;
+        await this.sendDeviceMessage(deviceId, message);
+        await this.readResponseStatus();
+    }
+
+    async getPortForwardingList(): Promise<{ device: string, localPort: string, remotePort: string }[]> {
+        const message = `host:list-forward`;
+        await this.sendAdbMessage(message);
+        await this.readResponseStatus();
+        const result = await this.readAdbMessage();
+        const dec = new TextDecoder();
+        const list = dec.decode(result);
+        const ret: { device: string, localPort: string, remotePort: string }[] = [];
+        for (const line of list.split('\n')) {
+            const elems = line.split(' ');
+            if (elems.length === 3) {
+                ret.push({ device: elems[0], localPort: elems[1], remotePort: elems[2] });
+            }
+        }
+        return ret;
+    }
+
+    async enterSyncMode(): Promise<void> {
+        await this.sendAdbMessage('sync:');
+        await this.readResponseStatus();
+    }
+
+    async writeSyncHeader(requestId: string, dataLen: number): Promise<void> {
+        const enc = new TextEncoder();
+        const requestIdData = enc.encode(requestId);
+        if (requestIdData.length !== 4) {
+            throw new Error('Sync request ID must be 4 characters long');
+        }
+        const buf = new Uint8Array(8);
+        const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
+        buf.set(requestIdData, 0);
+        view.setUint32(4, dataLen, true);
+        await this.write(buf);
+    }
+
+    async writeSyncData(requestId: string, data: Uint8Array): Promise<void> {
+        await this.writeSyncHeader(requestId, data.length);
+        await this.write(data);
+    }
+
+    async readSyncHeader(): Promise<{ responseId: string; dataLen: number }> {
+        const header = await this.read(8);
+        if (header.length !== 8) {
+            throw new Error('Incomplete sync header received');
+        }
+        const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
+        const dec = new TextDecoder();
+        const responseId = dec.decode(header.subarray(0, 4));
+        const dataLen = view.getUint32(4, true);
+        return { responseId, dataLen };
+    }
+
+    async pushStream(reader: (maxSize: number) => Promise<Uint8Array>, remoteFilePath: string): Promise<void> {
+        const defaultMode = 0o100770;
+        const maxChunkSize = 2 * 1024;
+
+        if (remoteFilePath.indexOf(',') !== -1) {
+            throw new Error('Remote file path cannot contain commas');
+        }
+
+        const enc = new TextEncoder();
+        const body = `${remoteFilePath},0${defaultMode.toString(8)}`;
+        await this.writeSyncData('SEND', enc.encode(body));
+
+        for (;;) {
+            const chunk = await reader(maxChunkSize);
+            if (chunk.length === 0) {
+                break;
+            }
+            await this.writeSyncData('DATA', chunk);
+        }
+
+        const mtime = Math.floor(Date.now() / 1000);
+        await this.writeSyncHeader('DONE', mtime); // TODO: year 2038 bug
+
+        const { responseId, dataLen } = await this.readSyncHeader();
+        if (responseId === 'FAIL') {
+            const errorMessageData = await this.read(dataLen);
+            const dec = new TextDecoder();
+            const errorMessage = dec.decode(errorMessageData);
+            throw new AdbCommandFailed(`Failed to push file "${remoteFilePath}": ${errorMessage}`);
+        } else if (responseId !== 'OKAY') {
+            throw new Error(`Unexpected sync response: ${responseId}`);
+        }
+        if (dataLen !== 0) {
+            throw new Error('Unexpected data in sync OKAY response');
+        }
+    }
+
+    async pushData(data: Uint8Array, remoteFilePath: string): Promise<void> {
+        let offset = 0;
+        const reader = async (maxSize: number): Promise<Uint8Array> => {
+            const chunk = data.subarray(offset, offset + maxSize);
+            offset += chunk.length;
+            return chunk;
+        };
+        await this.pushStream(reader, remoteFilePath);
+    }
+
+    async pushFile(localFilePath: string, remoteFilePath: string): Promise<void> {
+        const handle = await fs.open(localFilePath, "r");
+        let buf = new Uint8Array(0);
+        const reader = async (maxSize: number): Promise<Uint8Array> => {
+            if (buf.length < maxSize) {
+                buf = new Uint8Array(maxSize);
+            }
+            const { bytesRead } = await handle.read(buf, 0, maxSize, null);
+            return buf.subarray(0, bytesRead);
+        };
+        await this.pushStream(reader, remoteFilePath);
+    }
+}
+
+export class AdbCommandFailed extends Error {
+    constructor(message: string) {
+        super(message);
+        this.name = 'AdbCommandFailed';
+    }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts b/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
new file mode 100644
index 0000000000000..3479feb37e2d2
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
@@ -0,0 +1,230 @@
+import * as fs from "node:fs/promises";
+import * as os from "node:os";
+import * as path from "node:path";
+import { AdbClient } from "./adb-client";
+import Env from "./env";
+
+/**
+ * This class controls the execution of an Android APK for debugging.
+ * It can install the lldb-server in the app sandbox, start the APK and the
+ * lldb-server, dismiss the "waiting for debugger" dialog and stop everything.
+ * It assumes the target device is connected (or the emulator is running), the
+ * APK is installed, and the ADB daemon is running.
+ */
+export class ApkDebugSession {
+
+    private runningSession: SessionInfo | undefined;
+
+    /**
+     * Component name is in the form "com.example.app/.MainActivity".
+     * `wfd` stays for "waiting for debugger".
+     */
+    async start(deviceId: string | undefined, componentName: string, wfd: boolean) {
+        const addId = componentName.split('/')[0];
+        const adb = new AdbClient();
+        if (deviceId !== undefined) {
+            adb.setDeviceId(deviceId);
+        } else {
+            await adb.autoDetectDeviceId();
+        }
+        const arch = (await adb.shellCommandToString("uname -m")).trim();
+        const lldbServerPath = await Env.getLldbServerPath(arch);
+        if (!lldbServerPath) {
+            throw new Error("Could not find LLDB server in Android NDK");
+        }
+        await this.stop();
+        await this.cleanUpEarlierDebugSessions(adb, addId);
+        await this.installLldbServer(adb, addId, lldbServerPath);
+        const pid = await this.startApk(adb, componentName, wfd);
+
+        const abortController = new AbortController();
+        const endPromise = this.startLldbServer(adb, addId, abortController.signal);
+        this.runningSession = {
+            adb,
+            addId,
+            endPromise,
+            abortController,
+        };
+
+        await this.waitLldbServerReachable(adb, addId);
+
+        // In this early version, we save session data for debugging purposes.
+        // But this is temporay and will be removed in future versions.
+        // TODO: remove or put this data in a log.
+        await this.saveSessionData();
+    }
+
+    async stop() {
+        if (this.runningSession !== undefined) {
+            try {
+                await this.runningSession.adb.shellCommand(`am force-stop ${this.runningSession.addId}`);
+            } catch {}
+            this.runningSession.abortController.abort();
+            try {
+                await this.runningSession.endPromise;
+            } catch {}
+            this.runningSession = undefined;
+        }
+    }
+
+    async dismissWaitingForDebuggerDialog() {
+        if (this.runningSession !== undefined) {
+            const pid = await this.runningSession.adb.getPid(this.runningSession.addId);
+            await this.runningSession.adb.dismissWaitingForDebuggerDialog(pid);
+        }
+    }
+
+    /**
+     * This function tries to clean up anything left behind by earlier debug
+     * sessions.
+     * The current implementation is a bit aggressive, and could impact debug
+     * sessions running in parallel on other apps.
+     * However, the current debugging solution is not super stable and a deep
+     * clean-up phase is really needed. At the same time, cases where two debug
+     * sessions run in parallel are rare.
+     */
+    private async cleanUpEarlierDebugSessions(adb: AdbClient, addId: string) {
+        const deviceId = adb.getDeviceId();
+
+        // stop the app
+        await adb.shellCommand(`am force-stop ${addId}`);
+
+        // kill existing gdbserver processes
+        await adb.shellCommand(`run-as ${addId} killall lldb-server`);
+
+        // clean up port forwarding
+        await this.cleanUpPortForwarding(adb, addId);
+
+        // clean up unix-domain socket files in the app data folder
+        await adb.shellCommand(`run-as ${addId} find /data/data/${addId} -name 'gdbserver.*' -exec rm {} \\;`);
+    }
+
+    private async cleanUpPortForwarding(adb: AdbClient, addId: string) {
+        const deviceId = adb.getDeviceId();
+        const list = await adb.getPortForwardingList();
+        const filteredList = list.filter(item => {
+            if (item.device !== deviceId) {
+                return false;
+            }
+            const pattern1 = `localabstract:/${addId}`;
+            const regex1 = new RegExp(pattern1);
+            if (regex1.test(item.remotePort)) {
+                return true;
+            }
+            const pattern2 = `localabstract:gdbserver.`;
+            const regex2 = new RegExp(pattern2);
+            if (regex2.test(item.remotePort)) {
+                return true;
+            }
+            return false;
+        });
+        for (const item of filteredList) {
+            const localPort = parseInt(item.localPort.replace(/^tcp:/, ""), 10);
+            console.log(`Removing port forwarding for local port ${localPort} (remote: ${item.remotePort})`);
+            await adb.removePortForwarding(localPort);
+        }
+    }
+
+    private async installLldbServer(adb: AdbClient, addId: string, lldbServerPath: string) {
+        await adb.shellCommand(`mkdir -p /data/local/tmp/lldb-stuff`);
+        await adb.pushFile(lldbServerPath, `/data/local/tmp/lldb-stuff/lldb-server`);
+        await adb.shellCommand(`run-as ${addId} mkdir -p /data/data/${addId}/lldb-stuff`);
+        await adb.shellCommand(`cat /data/local/tmp/lldb-stuff/lldb-server | run-as ${addId} sh -c 'cat > /data/data/${addId}/lldb-stuff/lldb-server'`);
+        await adb.shellCommand(`run-as ${addId} chmod 700 /data/data/${addId}/lldb-stuff/lldb-server`);
+    }
+
+    private startLldbServer(adb: AdbClient, addId: string, abort: AbortSignal) {
+        const command = `run-as ${addId} /data/data/${addId}/lldb-stuff/lldb-server`
+            + ` platform --server --listen unix-abstract:///${addId}/lldb-platform.sock`
+            + ` --log-channels "lldb process:gdb-remote packets"`;
+        // TODO: open log file
+        const writer = async () => {};
+        return adb.shellCommandToStream(command, writer, abort)
+            .then(() => {
+                // TODO: close log file
+            });
+    }
+
+    private async waitLldbServerReachable(adb: AdbClient, addId: string) {
+        const t1 = Date.now();
+        for (;;) {
+            const t2 = Date.now();
+            if (t2 - t1 > 10000) {
+                throw new Error("Timeout waiting for lldb-server to start");
+            }
+            const result = await adb.shellCommandToString(`cat /proc/net/unix | grep ${addId}/lldb-platform.sock`);
+            if (result.trim().length > 0) {
+                return;
+            }
+            await new Promise(resolve => setTimeout(resolve, 100));
+        }
+    }
+
+    private async startApk(adb: AdbClient, componentName: string, wfd: boolean): Promise<number> {
+        const parts = componentName.split('/');
+        const addId = parts[0];
+        if (parts.length === 1) {
+            componentName = parts[0] + "/.MainActivity";
+        }
+
+        await adb.shellCommand(`am start -n ${componentName} -a android.intent.action.MAIN -c android.intent.category.LAUNCHER ${wfd ? '-D' : ''}`);
+
+        const t1 = Date.now();
+        for (;;) {
+            const t2 = Date.now();
+            if (t2 - t1 > 10000) {
+                throw new Error("Timeout waiting for app to start");
+            }
+            try {
+                const pid = await adb.getPid(addId);
+                return pid;
+            } catch {}
+            await new Promise(resolve => setTimeout(resolve, 100));
+        }
+    }
+
+    private async getDataFolder(): Promise<string | undefined> {
+        const home = os.homedir();
+        try {
+            await fs.access(home, fs.constants.R_OK | fs.constants.W_OK);
+            const dataFolder = path.join(home, ".lldb", "android");
+            await fs.mkdir(dataFolder, { recursive: true });
+            return dataFolder;
+        } catch {}
+        return undefined;
+    }
+
+    private async saveSessionData() {
+        if (this.runningSession === undefined)
+            return;
+        const dir = await this.getDataFolder();
+        if (dir === undefined)
+            return;
+        const pid = await this.runningSession.adb.getPid(this.runningSession.addId);
+        fs.writeFile(path.join(dir, `last-env`),
+            `ANDROID_DEVICE_ID=${this.runningSession.adb.getDeviceId()}\n` +
+            `ANDROID_APP_ID=${this.runningSession.addId}\n` +
+            `ANDROID_PID=${pid}\n`);
+        fs.writeFile(path.join(dir, `last-start-commands`),
+            `platform select remote-android\n` +
+            `command alias a0 platform connect unix-abstract-connect:///${this.runningSession.addId}/lldb-platform.sock\n` +
+            `command alias a1 process attach --pid ${pid}\n` +
+            `command alias a1n process attach --name ${this.runningSession.addId}\n` +
+            `command alias a2 process handle SIGSEGV -n false -p true -s false\n` +
+            `command alias a3 process handle SIGBUS -n false -p true -s false\n` +
+            `command alias a4 process handle SIGCHLD -n false -p true -s false\n` +
+            `a0\n` +
+            `a1\n` +
+            `a2\n` +
+            `a3\n` +
+            `a4\n`
+        );
+    }
+}
+
+type SessionInfo = {
+    adb: AdbClient;
+    addId: string;
+    endPromise: Promise<void>;
+    abortController: AbortController;
+}
diff --git a/lldb/tools/lldb-dap/src-ts/android/connection.ts b/lldb/tools/lldb-dap/src-ts/android/connection.ts
new file mode 100644
index 0000000000000..810e31687483b
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/android/connection.ts
@@ -0,0 +1,233 @@
+import * as net from 'node:net';
+
+/**
+ * This class is a TCP client.
+ */
+export class Connection {
+
+    private socket: net.Socket | null = null;
+    private rxBuffer: Buffer[] = [];
+    private rxSignal: Signal | null = null;
+    private rxEndOfStream = false;
+    private inError: Error | null = null;
+
+    async connect(host: string, port: number) {
+        if (this.socket) {
+            throw new Error('Connection is already established');
+        }
+        const socket = await connectSocket(host, port);
+        this.socket = socket;
+
+        socket.on('data', (data: Buffer) => {
+            this.rxBuffer.push(data);
+            if (this.rxSignal) {
+                this.rxSignal.fire();
+                this.rxSignal = null;
+            }
+        });
+        socket.on('end', () => {
+            // the peer has performed a tx shutdown or closed the connection
+            this.rxEndOfStream = true;
+            if (this.rxSignal) {
+                this.rxSignal.fire();
+                this.rxSignal = null;
+            }
+        });
+        socket.on('error', (err: Error) => {
+            console.error(`Socket error: ${err.message}`);
+            this.inError = err;
+            this.socket?.destroy();
+            this.socket = null;
+            this.rxBuffer = [];
+            if (this.rxSignal) {
+                this.rxSignal.fire();
+                this.rxSignal = null;
+            }
+            this.rxEndOfStream = false;
+        });
+    }
+
+    /**
+     * Signal the end of data transmission. Reception of data may still continue.
+     */
+    async end(): Promise<void> {
+        if (this.inError) {
+            const err = this.inError;
+            this.inError = null;
+            throw err;
+        }
+        const socket = this.socket;
+        if (!socket) {
+            throw new Error('Connection is closed');
+        }
+        return new Promise((resolve, reject) => {
+            socket.end(() => {
+                resolve();
+            });
+        });
+    }
+
+    /**
+     * Close the connection immediately.
+     * This discards buffered data.
+     */
+    close(): void {
+        const socket = this.socket;
+        if (socket) {
+            socket.destroy();
+            this.socket = null;
+            this.rxBuffer = [];
+            if (this.rxSignal) {
+                this.rxSignal.fire();
+                this.rxSignal = null;
+            }
+            this.rxEndOfStream = false;
+        }
+    }
+
+    get isConnected(): boolean {
+        return this.socket !== null;
+    }
+
+    async write(data: Uint8Array): Promise<void> {
+        if (this.inError) {
+            const err = this.inError;
+            this.inError = null;
+            throw err;
+        }
+        const socket = this.socket;
+        if (!socket) {
+            throw new Error('Connection is closed');
+        }
+        if (data.length === 0) {
+            return;
+        }
+        return new Promise((resolve, reject) => {
+            socket.write(data, (err) => {
+                if (err) {
+                    return reject(err);
+                }
+                resolve();
+            });
+        });
+    }
+
+    /**
+     * Get the number of bytes currently available in the receive buffer.
+     */
+    get availableData(): number {
+        let totalLength = 0;
+        for (const buf of this.rxBuffer) {
+            totalLength += buf.length;
+        }
+        return totalLength;
+    }
+
+    /**
+     * Read `size` bytes from the receive buffer. If `size` is undefined, read
+     * all available data, but at least one byte.
+     * If the requested data is not yet available, wait until it is received.
+     * If the end of the stream is reached before the requested data is
+     * available, returns whatever is available (may be zero bytes).
+     */
+    async read(size?: number): Promise<Uint8Array> {
+        return new Promise(async (resolve, reject) => {
+            for (;;) {
+                if (this.inError) {
+                    const err = this.inError;
+                    this.inError = null;
+                    reject(err);
+                    return;
+                }
+                if (!this.socket) {
+                    reject(new Error('Connection is closed'));
+                    return;
+                }
+                if (this.rxSignal) {
+                    reject(new Error('Concurrent read operations are not supported'));
+                    return;
+                }
+                const available = this.availableData;
+                if (available >= (size ?? 1)) {
+                    const buffer = this.readFromRxBuffer(size ?? available);
+                    resolve(buffer);
+                    return;
+                }
+                if (this.rxEndOfStream) {
+                    const buffer = this.readFromRxBuffer(available);
+                    resolve(buffer);
+                    return;
+                }
+                this.rxSignal = new Signal();
+                await this.rxSignal.promise;
+            }
+        });
+    }
+
+    private readFromRxBuffer(size: number): Uint8Array {
+        const buffer = new Uint8Array(size);
+        let offset = 0;
+        while (offset < size) {
+            if (this.rxBuffer.length === 0) {
+                throw new Error('Not enough data in rxBuffer');
+            }
+            const chunk = this.rxBuffer[0];
+            const toCopy = Math.min(size - offset, chunk.length);
+            buffer.set(chunk.subarray(0, toCopy), offset);
+            offset += toCopy;
+            if (toCopy < chunk.length) {
+                this.rxBuffer[0] = chunk.subarray(toCopy);
+            } else {
+                this.rxBuffer.shift();
+            }
+        }
+        return buffer;
+    }
+}
+
+class Signal {
+    private _resolve!: () => void;
+    private _reject!: (error: Error) => void;
+
+    readonly promise: Promise<void>;
+
+    constructor() {
+        this.promise = new Promise<void>((res, rej) => {
+            this._resolve = res;
+            this._reject = rej;
+        });
+    }
+
+    fire() {
+        this._resolve();
+    }
+
+    fireError(error: Error) {
+        this._reject(error);
+    }
+}
+
+async function connectSocket(host: string, port: number): Promise<net.Socket> {
+    return new Promise((resolve, reject) => {
+        let socket: net.Socket;
+
+        const errorHandler = (err: Error) => {
+            console.error(`Connection error: ${err.message}`);
+            reject(err);
+        };
+
+        const endHandler = () => {
+            // can this happen?
+            errorHandler(new Error('Connection ended unexpectedly'));
+        };
+
+        socket = net.createConnection({ host, port }, () => {
+            socket.off('error', errorHandler);
+            socket.off('end', endHandler);
+            resolve(socket);
+        });
+
+        socket.on('error', errorHandler);
+        socket.on('end', endHandler);
+    });
+}
diff --git a/lldb/tools/lldb-dap/src-ts/android/env.ts b/lldb/tools/lldb-dap/src-ts/android/env.ts
new file mode 100644
index 0000000000000..d6d0fabfb6ea4
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/android/env.ts
@@ -0,0 +1,57 @@
+import * as path from "node:path";
+import * as fs from "node:fs/promises";
+import * as os from "node:os";
+
+/**
+ * TODO: revisit everything!
+ */
+namespace Env {
+
+    export async function getAndroidNdkPath(): Promise<string | undefined> {
+        const home = os.homedir();
+        const ndk = path.join(home, "Library", "Android", "sdk", "ndk");
+        const entries = await fs.readdir(ndk);
+        if (entries.length === 0) {
+            return undefined;
+        }
+        entries.sort((a, b) => b.localeCompare(a, 'en-US', { numeric: true }));
+        return path.join(ndk, entries[0]);
+    }
+
+    export async function getLldbServerPath(arch: string): Promise<string | undefined> {
+        // supported arch: aarch64, riscv64, arm, x86_64, i386
+        const ndk = await getAndroidNdkPath();
+        if (ndk) {
+            const root1 = path.join(ndk, "toolchains", "llvm", "prebuilt");
+            try {
+                const entries1 = await fs.readdir(root1);
+                for (const entry1 of entries1) {
+                    if (entry1.startsWith("darwin-")) {
+                        const root2 = path.join(root1, entry1, "lib", "clang");
+                        try {
+                            const entries2 = await fs.readdir(root2);
+                            for (const entry2 of entries2) {
+                                const root3 = path.join(root2, entry2, "lib", "linux");
+                                try {
+                                    const entries3 = await fs.readdir(root3);
+                                    for (const entry3 of entries3) {
+                                        if (entry3 === arch) {
+                                            const candidate = path.join(root3, entry3, "lldb-server");
+                                            try {
+                                                await fs.access(candidate, fs.constants.R_OK);
+                                                return candidate;
+                                            } catch {}
+                                        }
+                                    }
+                                } catch {}
+                            }
+                        } catch {}
+                    }
+                }
+            } catch {}
+        }
+        return undefined;
+    }
+}
+
+export default Env;
diff --git a/lldb/tools/lldb-dap/src-ts/android/jdwp.ts b/lldb/tools/lldb-dap/src-ts/android/jdwp.ts
new file mode 100644
index 0000000000000..1132da40ea6cc
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/android/jdwp.ts
@@ -0,0 +1,150 @@
+import { Connection } from "./connection";
+
+/*
+ * Protocol specs: https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/jdwp-spec.html
+ */
+
+namespace Jdwp {
+
+    enum CommandFlag {
+        REPLY = 0x80,
+    }
+
+    enum CommandSet {
+        VM = 1,
+        CLASS = 3,
+        OBJECT = 9,
+    }
+
+    enum VmCommand {
+        VERSION = 1,
+    }
+
+    type CommandPacket = {
+        id: number;
+        flags: number;
+        commandSet: CommandSet;
+        command: number;
+        data: Uint8Array;
+    }
+
+    type ReplayPacket = {
+        id: number;
+        flags: number;
+        errorCode: number;
+        data: Uint8Array;
+    }
+
+    function encodePacket(command: CommandPacket): Uint8Array {
+        const packet = new Uint8Array(11 + command.data.length);
+        const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength);
+        view.setUint32(0, 11 + command.data.length, false); // length
+        view.setUint32(4, command.id, false); // id
+        view.setUint8(8, command.flags); // flags
+        view.setUint8(9, command.commandSet); // commandSet
+        view.setUint8(10, command.command); // command
+        packet.set(command.data, 11);
+        return packet;
+    }
+
+    function decodePacket(buffer: Uint8Array): ReplayPacket {
+        const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
+        const length = view.getUint32(0, false);
+        const id = view.getUint32(4, false);
+        const flags = view.getUint8(8);
+        const errorCode = view.getUint16(9, false);
+        const data = buffer.subarray(11, length);
+        return {
+            id,
+            flags,
+            errorCode,
+            data,
+        };
+    }
+
+    function encodeVersionCommand() {
+        const command: CommandPacket = {
+            id: 1,
+            flags: 0,
+            commandSet: CommandSet.VM,
+            command: VmCommand.VERSION,
+            data: new Uint8Array(0),
+        };
+        return encodePacket(command);
+    }
+
+    function decodeVersionReply(buffer: Uint8Array) {
+        const packet = decodePacket(buffer);
+        if ((packet.flags & CommandFlag.REPLY) === 0) {
+            throw new Error('Not a reply packet');
+        }
+        if (packet.errorCode !== 0) {
+            throw new Error(`JDWP error code: ${packet.errorCode}`);
+        }
+        const view = new DataView(packet.data.buffer, packet.data.byteOffset, packet.data.byteLength);
+        const dec = new TextDecoder();
+        let offset = 0;
+        const descLength = view.getUint32(offset, false);
+        offset += 4;
+        const desc = dec.decode(packet.data.subarray(offset, offset + descLength));
+        offset += descLength;
+        const jdwpMajor = view.getUint32(offset, false);
+        offset += 4;
+        const jdwpMinor = view.getUint32(offset, false);
+        offset += 4;
+        const vmVersionLength = view.getUint32(offset, false);
+        offset += 4;
+        const vmVersion = dec.decode(packet.data.subarray(offset, offset + vmVersionLength));
+        offset += vmVersionLength;
+        const vmNameLength = view.getUint32(offset, false);
+        offset += 4;
+        const vmName = dec.decode(packet.data.subarray(offset, offset + vmNameLength));
+        offset += vmNameLength;
+        if (offset !== packet.data.length) {
+            throw new Error('Unexpected data in version reply packet');
+        }
+        return {
+            description: desc,
+            jdwpMajor,
+            jdwpMinor,
+            vmVersion,
+            vmName,
+        };
+    }
+
+    async function readPacket(c: Connection): Promise<Uint8Array> {
+        const head = await c.read(11);
+        if (head.length !== 11) {
+            throw new Error('Incomplete JDWP packet header received');
+        }
+        const view = new DataView(head.buffer);
+        const length = view.getUint32(0, false);
+        const body = await c.read(length - 11);
+        if (body.length !== length - 11) {
+            throw new Error('Incomplete JDWP packet body received');
+        }
+        const full = new Uint8Array(length);
+        full.set(head, 0);
+        full.set(body, 11);
+        return full;
+    }
+
+    export async function handshake(c: Connection) {
+        c.write(Buffer.from("JDWP-Handshake"));
+        const reply = await c.read(14);
+        const dec = new TextDecoder();
+        if (dec.decode(reply) !== "JDWP-Handshake") {
+            throw new Error('Invalid JDWP handshake reply');
+        }
+    }
+
+    export async function getVersion(c: Connection) {
+        const versionCommand = encodeVersionCommand();
+        await c.write(versionCommand);
+        const replyBuffer = await readPacket(c);
+        const versionReply = decodeVersionReply(replyBuffer);
+        return versionReply;
+    }
+}
+
+export default Jdwp;

>From 0756fe31a26293a7a8669cd9a76a7a9043b62426 Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Mon, 15 Dec 2025 18:12:31 +0100
Subject: [PATCH 02/18] [lldb-dap] add support for Android debug sessions

---
 lldb/tools/lldb-dap/extension/package.json    | 11 ++++
 .../extension/src/debug-adapter-factory.ts    | 10 +++
 .../src/debug-configuration-provider.ts       | 13 ++++
 .../extension/src/debug-session-tracker.ts    | 20 ++++++
 .../android/android-component-tracker.ts      | 62 +++++++++++++++++++
 .../src-ts/android/apk-debug-configuration.ts | 13 ++++
 6 files changed, 129 insertions(+)
 create mode 100644 lldb/tools/lldb-dap/src-ts/android/android-component-tracker.ts
 create mode 100644 lldb/tools/lldb-dap/src-ts/android/apk-debug-configuration.ts

diff --git a/lldb/tools/lldb-dap/extension/package.json b/lldb/tools/lldb-dap/extension/package.json
index 923c437006556..2ebfa7552a778 100644
--- a/lldb/tools/lldb-dap/extension/package.json
+++ b/lldb/tools/lldb-dap/extension/package.json
@@ -281,6 +281,12 @@
             },
             "description": "Commands executed when the debugging session ends.",
             "default": []
+          },
+          "lldb-dap.androidComponent": {
+            "order": 8,
+            "type": "string",
+            "description": "If non-empty, defines which Android activity to be launched and debugged. The name must have the format `package/name`. For example, `com.example.app/.MainActivity`.",
+            "default": ""
           }
         }
       }
@@ -679,6 +685,11 @@
                 "type": "string",
                 "description": "If non-empty, threads will have descriptions generated based on the provided format. See https://lldb.llvm.org/use/formatting.html for an explanation on format strings for threads. If the format string contains errors, an error message will be displayed on the Debug Console and the default thread names will be used. This might come with a performance cost because debug information might need to be processed to generate the description.",
                 "default": ""
+              },
+              "androidComponent": {
+                "type": "string",
+                "description": "If non-empty, defines which Android activity to be launched and debugged. The name must have the format `package/name`. For example, `com.example.app/.MainActivity`.",
+                "default": ""
               }
             }
           },
diff --git a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
index e415097d1b8be..2f57de2c6ac0b 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
@@ -7,6 +7,7 @@ import { LogFilePathProvider, LogType } from "./logging";
 import { ErrorWithNotification } from "./ui/error-with-notification";
 import { ConfigureButton, OpenSettingsButton } from "./ui/show-error-message";
 import { expandUser } from "./utils";
+import { AndroidComponentTracker } from "./android/android-component-tracker";
 
 const exec = util.promisify(child_process.execFile);
 
@@ -328,6 +329,15 @@ export class LLDBDapDescriptorFactory
       throw error;
     }
 
+    if (session.configuration.androidComponent && session.configuration.request === "launch") {
+      this.logger.info(
+        `Session "${session.name}" is an Android debug session for component ${session.configuration.androidComponent}.`,
+      );
+      const tracker = new AndroidComponentTracker(session, session.configuration.androidComponent);
+      // TODO: handled exceptions
+      await tracker.startDebugSession();
+    }
+
     // Use a server connection if the debugAdapterPort is provided
     if (session.configuration.debugAdapterPort) {
       this.logger.info(
diff --git a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
index a3925ecfdfa58..292e34c63c34d 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
@@ -6,6 +6,7 @@ import { LLDBDapServer } from "./lldb-dap-server";
 import { LogFilePathProvider } from "./logging";
 import { ErrorWithNotification } from "./ui/error-with-notification";
 import { ConfigureButton } from "./ui/show-error-message";
+import { ApkDebugConfiguration } from "./android/apk-debug-configuration";
 
 const exec = util.promisify(child_process.execFile);
 
@@ -231,6 +232,18 @@ export class LLDBDapConfigurationProvider
         }
       }
 
+      if (debugConfiguration.androidComponent && debugConfiguration.request === "launch") {
+        // TODO: check ADB, connected devices and anything needed to run the android debug session
+        if (
+          !debugConfiguration.launchCommands ||
+          debugConfiguration.launchCommands.length === 0
+        ) {
+          // TODO: manage deviceId
+          debugConfiguration.launchCommands =
+            ApkDebugConfiguration.getLldbLaunchCommands(undefined, debugConfiguration.androidComponent);
+        }
+      }
+
       this.logger.info(
         "Resolved debug configuration:\n" +
           JSON.stringify(debugConfiguration, undefined, 2),
diff --git a/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts b/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
index 5c11e37e892d9..dfa4381e8ba4d 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
@@ -1,5 +1,6 @@
 import { DebugProtocol } from "@vscode/debugprotocol";
 import * as vscode from "vscode";
+import { AndroidComponentTracker } from "./android/android-component-tracker";
 
 export interface LLDBDapCapabilities extends DebugProtocol.Capabilities {
   /** The debug adapter supports the `moduleSymbols` request. */
@@ -86,6 +87,7 @@ export class DebugSessionTracker
     let stopping = false;
     return {
       onError: (error) => !stopping && this.logger.error(error), // Can throw benign read errors when shutting down.
+      onWillReceiveMessage: (message) => this.onWillReceiveMessage(session, message),
       onDidSendMessage: (message) => this.onDidSendMessage(session, message),
       onWillStopSession: () => (stopping = true),
       onExit: () => this.onExit(session),
@@ -103,6 +105,11 @@ export class DebugSessionTracker
 
   /** Clear information from the active session. */
   private onExit(session: vscode.DebugSession) {
+    const androidComponentTracker = AndroidComponentTracker.getFromSession(session);
+    if (androidComponentTracker) {
+      this.logger.info(`Stopping android APK "${androidComponentTracker.componentName}"`);
+      androidComponentTracker.stopDebugSession().catch();
+    }
     this.modules.delete(session);
     this.modulesChanged.fire(undefined);
   }
@@ -127,6 +134,19 @@ export class DebugSessionTracker
     }
   }
 
+  private onWillReceiveMessage(session: vscode.DebugSession, message: DebugProtocol.Request) {
+    this.logger.info(`Received message: ${JSON.stringify(message)}`);
+    if (message.command === "configurationDone") {
+      const androidComponentTracker = AndroidComponentTracker.getFromSession(session);
+      if (androidComponentTracker) {
+        this.logger.info(
+          `Dismissing Waiting-For-Debugger dialog on Android APK "${androidComponentTracker.componentName}"`
+        );
+        androidComponentTracker.dismissWaitingForDebuggerDialog().catch();
+      }
+    }
+  }
+
   private onDidSendMessage(
     session: vscode.DebugSession,
     message: DebugProtocol.ProtocolMessage,
diff --git a/lldb/tools/lldb-dap/src-ts/android/android-component-tracker.ts b/lldb/tools/lldb-dap/src-ts/android/android-component-tracker.ts
new file mode 100644
index 0000000000000..2bcc79d417465
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/android/android-component-tracker.ts
@@ -0,0 +1,62 @@
+import * as vscode from "vscode";
+import { ApkDebugSession } from "./apk-debug-session";
+
+/**
+ * Tracks an Android component associated with a VS Code debug session.
+ * Normally, the component is an activity inside an APK.
+ * This class includes everything needed to start and stop the activity
+ * and lldb-server on the target device, and dismiss the "waiting for debugger"
+ * dialog.
+ * Here, we assume the target device is connected (or the emulator is running),
+ * the APK is installed, and the ADB daemon is running.
+ */
+export class AndroidComponentTracker {
+
+    private static catalog = new WeakMap<vscode.DebugSession, AndroidComponentTracker>();
+
+    static getFromSession(session: vscode.DebugSession): AndroidComponentTracker | undefined {
+        return AndroidComponentTracker.catalog.get(session);
+    }
+
+    readonly componentName: string;
+
+    private apkDebugSession = new ApkDebugSession();
+
+    /**
+     * Component name is in the form "com.example.app/.MainActivity".
+     * TODO: allow selecting the deviceId
+     */
+    constructor(session: vscode.DebugSession, componentName: string) {
+        this.componentName = componentName;
+        AndroidComponentTracker.catalog.set(session, this);
+    }
+
+    getAppId(): string {
+        const parts = this.componentName.split('/');
+        return parts[0];
+    }
+
+    getActivity(): string {
+        const parts = this.componentName.split('/');
+        if (parts.length === 1) {
+            return parts[0] + ".MainActivity";
+        }
+        if (parts[1].startsWith('.')) {
+            return parts[0] + parts[1];
+        }
+        return parts[1];
+    }
+
+    async startDebugSession() {
+        // TODO: allow selecting the deviceId
+        await this.apkDebugSession.start(undefined, this.componentName, true);
+    }
+
+    async stopDebugSession() {
+        await this.apkDebugSession.stop();
+    }
+
+    async dismissWaitingForDebuggerDialog() {
+        await this.apkDebugSession.dismissWaitingForDebuggerDialog();
+    }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/android/apk-debug-configuration.ts b/lldb/tools/lldb-dap/src-ts/android/apk-debug-configuration.ts
new file mode 100644
index 0000000000000..79de37b217346
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/android/apk-debug-configuration.ts
@@ -0,0 +1,13 @@
+import * as os from "node:os";
+
+export class ApkDebugConfiguration {
+
+    static getLldbLaunchCommands(deviceId: string | undefined, componentName: string): string[] {
+        // TODO: return the real commands needed to connect to lldb-server; last-start-commands should not be used here
+        // TODO: manage deviceId
+        const home = os.homedir();
+        return [
+            `command source -s 0 -e 0 '${home}/.lldb/android/last-start-commands'`
+        ];
+    }
+}

>From af0870016a15e7d598e9f1037f6a8c747cedce16 Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Thu, 18 Dec 2025 14:19:58 +0100
Subject: [PATCH 03/18] [lldb-dap] generate lldb launch commands for android
 APKs

---
 .../src-ts/android/apk-debug-configuration.ts | 17 ++++---
 .../src-ts/android/apk-debug-session.ts       | 46 -------------------
 lldb/tools/lldb-dap/src-ts/android/env.ts     | 11 +++++
 3 files changed, 22 insertions(+), 52 deletions(-)

diff --git a/lldb/tools/lldb-dap/src-ts/android/apk-debug-configuration.ts b/lldb/tools/lldb-dap/src-ts/android/apk-debug-configuration.ts
index 79de37b217346..f6e6e393e9f6d 100644
--- a/lldb/tools/lldb-dap/src-ts/android/apk-debug-configuration.ts
+++ b/lldb/tools/lldb-dap/src-ts/android/apk-debug-configuration.ts
@@ -1,13 +1,18 @@
-import * as os from "node:os";
 
 export class ApkDebugConfiguration {
 
-    static getLldbLaunchCommands(deviceId: string | undefined, componentName: string): string[] {
-        // TODO: return the real commands needed to connect to lldb-server; last-start-commands should not be used here
-        // TODO: manage deviceId
-        const home = os.homedir();
+    static getLldbLaunchCommands(deviceSerial: string | undefined, componentName: string): string[] {
+        if (deviceSerial === undefined) {
+            deviceSerial = "";
+        }
+        const appId = componentName.split("/")[0];
         return [
-            `command source -s 0 -e 0 '${home}/.lldb/android/last-start-commands'`
+            `platform select remote-android`,
+            `platform connect unix-abstract-connect://${deviceSerial}/${appId}/lldb-platform.sock`,
+            `process attach --name ${appId}`,
+            `process handle SIGSEGV -n false -p true -s false`,
+            `process handle SIGBUS -n false -p true -s false`,
+            `process handle SIGCHLD -n false -p true -s false`,
         ];
     }
 }
diff --git a/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts b/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
index 3479feb37e2d2..c6df69741e696 100644
--- a/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
+++ b/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
@@ -1,6 +1,3 @@
-import * as fs from "node:fs/promises";
-import * as os from "node:os";
-import * as path from "node:path";
 import { AdbClient } from "./adb-client";
 import Env from "./env";
 
@@ -47,11 +44,6 @@ export class ApkDebugSession {
         };
 
         await this.waitLldbServerReachable(adb, addId);
-
-        // In this early version, we save session data for debugging purposes.
-        // But this is temporay and will be removed in future versions.
-        // TODO: remove or put this data in a log.
-        await this.saveSessionData();
     }
 
     async stop() {
@@ -182,44 +174,6 @@ export class ApkDebugSession {
             await new Promise(resolve => setTimeout(resolve, 100));
         }
     }
-
-    private async getDataFolder(): Promise<string | undefined> {
-        const home = os.homedir();
-        try {
-            await fs.access(home, fs.constants.R_OK | fs.constants.W_OK);
-            const dataFolder = path.join(home, ".lldb", "android");
-            await fs.mkdir(dataFolder, { recursive: true });
-            return dataFolder;
-        } catch {}
-        return undefined;
-    }
-
-    private async saveSessionData() {
-        if (this.runningSession === undefined)
-            return;
-        const dir = await this.getDataFolder();
-        if (dir === undefined)
-            return;
-        const pid = await this.runningSession.adb.getPid(this.runningSession.addId);
-        fs.writeFile(path.join(dir, `last-env`),
-            `ANDROID_DEVICE_ID=${this.runningSession.adb.getDeviceId()}\n` +
-            `ANDROID_APP_ID=${this.runningSession.addId}\n` +
-            `ANDROID_PID=${pid}\n`);
-        fs.writeFile(path.join(dir, `last-start-commands`),
-            `platform select remote-android\n` +
-            `command alias a0 platform connect unix-abstract-connect:///${this.runningSession.addId}/lldb-platform.sock\n` +
-            `command alias a1 process attach --pid ${pid}\n` +
-            `command alias a1n process attach --name ${this.runningSession.addId}\n` +
-            `command alias a2 process handle SIGSEGV -n false -p true -s false\n` +
-            `command alias a3 process handle SIGBUS -n false -p true -s false\n` +
-            `command alias a4 process handle SIGCHLD -n false -p true -s false\n` +
-            `a0\n` +
-            `a1\n` +
-            `a2\n` +
-            `a3\n` +
-            `a4\n`
-        );
-    }
 }
 
 type SessionInfo = {
diff --git a/lldb/tools/lldb-dap/src-ts/android/env.ts b/lldb/tools/lldb-dap/src-ts/android/env.ts
index d6d0fabfb6ea4..1b07aae25d2bc 100644
--- a/lldb/tools/lldb-dap/src-ts/android/env.ts
+++ b/lldb/tools/lldb-dap/src-ts/android/env.ts
@@ -52,6 +52,17 @@ namespace Env {
         }
         return undefined;
     }
+
+    async function getDataFolder(): Promise<string | undefined> {
+        const home = os.homedir();
+        try {
+            await fs.access(home, fs.constants.R_OK | fs.constants.W_OK);
+            const dataFolder = path.join(home, ".lldb", "android");
+            await fs.mkdir(dataFolder, { recursive: true });
+            return dataFolder;
+        } catch {}
+        return undefined;
+    }    
 }
 
 export default Env;

>From 69be9f5f3f4a78d99c0ed6032b57be54f674fa00 Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Thu, 18 Dec 2025 16:28:19 +0100
Subject: [PATCH 04/18] [lldb-dap] use term "device serial" instead of "device
 id"

---
 .../lldb-dap/src-ts/android/adb-client.ts     | 54 +++++++++----------
 .../lldb-dap/src-ts/android/adb-connection.ts | 17 +++---
 .../src-ts/android/apk-debug-session.ts       | 14 ++---
 3 files changed, 43 insertions(+), 42 deletions(-)

diff --git a/lldb/tools/lldb-dap/src-ts/android/adb-client.ts b/lldb/tools/lldb-dap/src-ts/android/adb-client.ts
index a2c31bdd3e7e9..64030657aeda7 100644
--- a/lldb/tools/lldb-dap/src-ts/android/adb-client.ts
+++ b/lldb/tools/lldb-dap/src-ts/android/adb-client.ts
@@ -8,12 +8,12 @@ import Jdwp from './jdwp';
  * An ADB client has access to multiple Android devices.
  * Many functionalities exposed by this class need the target device
  * to be designated before invoking that functionality.
- * The target device can be selected by invoking setDeviceId() or autoDetectDeviceId().
+ * The target device can be selected by invoking setDeviceSerial() or autoDetectDeviceSerial().
  * This client expects the ADB daemon to be running on the local machine.
  */
 export class AdbClient {
 
-    private deviceId: string | undefined = undefined;
+    private deviceSerial: string | undefined = undefined;
 
     async getDeviceList(): Promise<string[]> {
         const connection = await this.createAdbConnection();
@@ -25,39 +25,39 @@ export class AdbClient {
         }
     }
 
-    async autoDetectDeviceId(): Promise<void> {
+    async autoDetectDeviceSerial(): Promise<void> {
         const connection = await this.createAdbConnection();
         try {
             const devices = await connection.getDeviceList();
             if (devices.length === 1) {
-                this.deviceId = devices[0];
+                this.deviceSerial = devices[0];
                 return;
             }
             if (devices.length === 0) {
                 throw new Error('No connected Android devices found');
             }
-            throw new Error('Multiple connected Android devices found, please specify a device ID');
+            throw new Error('Multiple connected Android devices found, please specify a device SN');
         } finally {
             connection.close();
         }
     }
 
-    setDeviceId(deviceId: string) {
-        this.deviceId = deviceId;
+    setDeviceSerial(deviceSerial: string) {
+        this.deviceSerial = deviceSerial;
     }
 
-    getDeviceId(): string {
-        if (this.deviceId === undefined) {
-            throw new Error('Device ID is not set');
+    getDeviceSerial(): string {
+        if (this.deviceSerial === undefined) {
+            throw new Error('Device SN is not set');
         }
-        return this.deviceId;
+        return this.deviceSerial;
     }
 
     async shellCommand(command: string): Promise<void> {
-        const deviceId = this.getDeviceId();
+        const deviceSerial = this.getDeviceSerial();
         const connection = await this.createAdbConnection();
         try {
-            await connection.setTargetDevice(deviceId);
+            await connection.setTargetDevice(deviceSerial);
             await connection.shellCommand(command);
         } finally {
             connection.close();
@@ -65,10 +65,10 @@ export class AdbClient {
     }
 
     async shellCommandToString(command: string): Promise<string> {
-        const deviceId = this.getDeviceId();
+        const deviceSerial = this.getDeviceSerial();
         const connection = await this.createAdbConnection();
         try {
-            await connection.setTargetDevice(deviceId);
+            await connection.setTargetDevice(deviceSerial);
             const output = await connection.shellCommandToString(command);
             return output;
         } finally {
@@ -77,13 +77,13 @@ export class AdbClient {
     }
 
     async shellCommandToStream(command: string, writer: (data: Uint8Array) => Promise<void>, abort: AbortSignal): Promise<void> {
-        const deviceId = this.getDeviceId();
+        const deviceSerial = this.getDeviceSerial();
         const connection = await this.createAdbConnection();
         abort.addEventListener('abort', () => {
             connection.close();
         });
         try {
-            await connection.setTargetDevice(deviceId);
+            await connection.setTargetDevice(deviceSerial);
             await connection.shellCommandToStream(command, writer);
         } finally {
             connection.close();
@@ -91,10 +91,10 @@ export class AdbClient {
     }
 
     async getPid(packageName: string): Promise<number> {
-        const deviceId = this.getDeviceId();
+        const deviceSerial = this.getDeviceSerial();
         const connection = await this.createAdbConnection();
         try {
-            await connection.setTargetDevice(deviceId);
+            await connection.setTargetDevice(deviceSerial);
             const pid = await connection.getPid(packageName);
             return pid;
         } finally {
@@ -103,10 +103,10 @@ export class AdbClient {
     }
 
     async addPortForwarding(remotePort: number | string, localPort: number = 0): Promise<number> {
-        const deviceId = this.getDeviceId();
+        const deviceSerial = this.getDeviceSerial();
         const connection = await this.createAdbConnection();
         try {
-            const port = await connection.addPortForwarding(deviceId, remotePort, localPort);
+            const port = await connection.addPortForwarding(deviceSerial, remotePort, localPort);
             return port;
         } finally {
             connection.close();
@@ -114,10 +114,10 @@ export class AdbClient {
     }
 
     async removePortForwarding(localPort: number): Promise<void> {
-        const deviceId = this.getDeviceId();
+        const deviceSerial = this.getDeviceSerial();
         const connection = await this.createAdbConnection();
         try {
-            await connection.removePortForwarding(deviceId, localPort);
+            await connection.removePortForwarding(deviceSerial, localPort);
         } finally {
             connection.close();
         }
@@ -156,10 +156,10 @@ export class AdbClient {
     }
 
     async pushData(data: Uint8Array, remoteFilePath: string): Promise<void> {
-        const deviceId = this.getDeviceId();
+        const deviceSerial = this.getDeviceSerial();
         const connection = await this.createAdbConnection();
         try {
-            await connection.setTargetDevice(deviceId);
+            await connection.setTargetDevice(deviceSerial);
             await connection.enterSyncMode();
             await connection.pushData(data, remoteFilePath);
         } finally {
@@ -168,10 +168,10 @@ export class AdbClient {
     }
 
     async pushFile(localFilePath: string, remoteFilePath: string): Promise<void> {
-        const deviceId = this.getDeviceId();
+        const deviceSerial = this.getDeviceSerial();
         const connection = await this.createAdbConnection();
         try {
-            await connection.setTargetDevice(deviceId);
+            await connection.setTargetDevice(deviceSerial);
             await connection.enterSyncMode();
             await connection.pushFile(localFilePath, remoteFilePath);
         } finally {
diff --git a/lldb/tools/lldb-dap/src-ts/android/adb-connection.ts b/lldb/tools/lldb-dap/src-ts/android/adb-connection.ts
index f1bd254db05a3..1c1b1b64bc18e 100644
--- a/lldb/tools/lldb-dap/src-ts/android/adb-connection.ts
+++ b/lldb/tools/lldb-dap/src-ts/android/adb-connection.ts
@@ -20,8 +20,8 @@ export class AdbConnection extends Connection {
         await this.write(data);
     }
 
-    async sendDeviceMessage(deviceId: string, packet: string): Promise<void> {
-        const msg = `host-serial:${deviceId}:${packet}`;
+    async sendDeviceMessage(deviceSerial: string, packet: string): Promise<void> {
+        const msg = `host-serial:${deviceSerial}:${packet}`;
         await this.sendAdbMessage(msg);
     }
 
@@ -58,6 +58,7 @@ export class AdbConnection extends Connection {
     }
 
     /**
+     * Return a list of device serial numbers connected to the ADB server.
      * The ADB server closes the connection after executing this command.
      */
     async getDeviceList(): Promise<string[]> {
@@ -78,8 +79,8 @@ export class AdbConnection extends Connection {
         return deviceList;
     }
 
-    async setTargetDevice(deviceId: string): Promise<void> {
-        await this.sendAdbMessage(`host:transport:${deviceId}`);
+    async setTargetDevice(deviceSerial: string): Promise<void> {
+        await this.sendAdbMessage(`host:transport:${deviceSerial}`);
         await this.readResponseStatus();
     }
 
@@ -136,12 +137,12 @@ export class AdbConnection extends Connection {
         return pid;
     }
 
-    async addPortForwarding(deviceId: string, remotePort: number | string, localPort: number = 0): Promise<number> {
+    async addPortForwarding(deviceSerial: string, remotePort: number | string, localPort: number = 0): Promise<number> {
         if (typeof remotePort === 'number') {
             remotePort = `tcp:${remotePort}`;
         }
         const message = `forward:tcp:${localPort};${remotePort}`;
-        await this.sendDeviceMessage(deviceId, message);
+        await this.sendDeviceMessage(deviceSerial, message);
         await this.readResponseStatus();
         await this.readResponseStatus();
         const result = await this.readAdbMessage();
@@ -153,9 +154,9 @@ export class AdbConnection extends Connection {
         return port;
     }
 
-    async removePortForwarding(deviceId: string, localPort: number): Promise<void> {
+    async removePortForwarding(deviceSerial: string, localPort: number): Promise<void> {
         const message = `killforward:tcp:${localPort}`;
-        await this.sendDeviceMessage(deviceId, message);
+        await this.sendDeviceMessage(deviceSerial, message);
         await this.readResponseStatus();
     }
 
diff --git a/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts b/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
index c6df69741e696..bb51386c2da26 100644
--- a/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
+++ b/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
@@ -16,13 +16,13 @@ export class ApkDebugSession {
      * Component name is in the form "com.example.app/.MainActivity".
      * `wfd` stays for "waiting for debugger".
      */
-    async start(deviceId: string | undefined, componentName: string, wfd: boolean) {
+    async start(deviceSerial: string | undefined, componentName: string, wfd: boolean) {
         const addId = componentName.split('/')[0];
         const adb = new AdbClient();
-        if (deviceId !== undefined) {
-            adb.setDeviceId(deviceId);
+        if (deviceSerial !== undefined) {
+            adb.setDeviceSerial(deviceSerial);
         } else {
-            await adb.autoDetectDeviceId();
+            await adb.autoDetectDeviceSerial();
         }
         const arch = (await adb.shellCommandToString("uname -m")).trim();
         const lldbServerPath = await Env.getLldbServerPath(arch);
@@ -76,7 +76,7 @@ export class ApkDebugSession {
      * sessions run in parallel are rare.
      */
     private async cleanUpEarlierDebugSessions(adb: AdbClient, addId: string) {
-        const deviceId = adb.getDeviceId();
+        const deviceSerial = adb.getDeviceSerial();
 
         // stop the app
         await adb.shellCommand(`am force-stop ${addId}`);
@@ -92,10 +92,10 @@ export class ApkDebugSession {
     }
 
     private async cleanUpPortForwarding(adb: AdbClient, addId: string) {
-        const deviceId = adb.getDeviceId();
+        const deviceSerial = adb.getDeviceSerial();
         const list = await adb.getPortForwardingList();
         const filteredList = list.filter(item => {
-            if (item.device !== deviceId) {
+            if (item.device !== deviceSerial) {
                 return false;
             }
             const pattern1 = `localabstract:/${addId}`;

>From 5a4f942957ace7fd978429640f451065b106f80a Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Thu, 18 Dec 2025 17:05:34 +0100
Subject: [PATCH 05/18] [lldb-dap] implement Android device selection

---
 lldb/tools/lldb-dap/extension/package.json    | 11 +++++
 .../extension/src/debug-adapter-factory.ts    |  2 +-
 .../src/debug-configuration-provider.ts       |  8 +++-
 .../lldb-dap/src-ts/android/adb-client.ts     | 12 ++++++
 .../android/android-component-tracker.ts      |  6 +--
 .../android/android-configuration-builder.ts  | 42 +++++++++++++++++++
 6 files changed, 74 insertions(+), 7 deletions(-)
 create mode 100644 lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts

diff --git a/lldb/tools/lldb-dap/extension/package.json b/lldb/tools/lldb-dap/extension/package.json
index 2ebfa7552a778..4148f66f2eb98 100644
--- a/lldb/tools/lldb-dap/extension/package.json
+++ b/lldb/tools/lldb-dap/extension/package.json
@@ -287,6 +287,12 @@
             "type": "string",
             "description": "If non-empty, defines which Android activity to be launched and debugged. The name must have the format `package/name`. For example, `com.example.app/.MainActivity`.",
             "default": ""
+          },
+          "lldb-dap.androidDevice": {
+            "order": 9,
+            "type": "string",
+            "description": "If non-empty, defines the name or serial number of the Android device or emulator used for debugging.",
+            "default": ""
           }
         }
       }
@@ -690,6 +696,11 @@
                 "type": "string",
                 "description": "If non-empty, defines which Android activity to be launched and debugged. The name must have the format `package/name`. For example, `com.example.app/.MainActivity`.",
                 "default": ""
+              },
+              "androidDevice": {
+                "type": "string",
+                "description": "If non-empty, defines the name or serial number of the Android device or emulator used for debugging.",
+                "default": ""
               }
             }
           },
diff --git a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
index 2f57de2c6ac0b..59ef46f3b157c 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
@@ -335,7 +335,7 @@ export class LLDBDapDescriptorFactory
       );
       const tracker = new AndroidComponentTracker(session, session.configuration.androidComponent);
       // TODO: handled exceptions
-      await tracker.startDebugSession();
+      await tracker.startDebugSession(session.configuration.androidDeviceSerial);
     }
 
     // Use a server connection if the debugAdapterPort is provided
diff --git a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
index 292e34c63c34d..337af1b0e60b7 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
@@ -7,6 +7,7 @@ import { LogFilePathProvider } from "./logging";
 import { ErrorWithNotification } from "./ui/error-with-notification";
 import { ConfigureButton } from "./ui/show-error-message";
 import { ApkDebugConfiguration } from "./android/apk-debug-configuration";
+import { AndroidConfigurationBuilder } from "./android/android-configuration-builder";
 
 const exec = util.promisify(child_process.execFile);
 
@@ -238,9 +239,12 @@ export class LLDBDapConfigurationProvider
           !debugConfiguration.launchCommands ||
           debugConfiguration.launchCommands.length === 0
         ) {
-          // TODO: manage deviceId
+          if (!debugConfiguration.androidDeviceSerial) {
+            debugConfiguration.androidDeviceSerial = await AndroidConfigurationBuilder.resolveDeviceSerial(debugConfiguration.androidDevice);
+            console.log(`Android device serial number: ${debugConfiguration.androidDeviceSerial}`);
+          }
           debugConfiguration.launchCommands =
-            ApkDebugConfiguration.getLldbLaunchCommands(undefined, debugConfiguration.androidComponent);
+            ApkDebugConfiguration.getLldbLaunchCommands(debugConfiguration.androidDeviceSerial, debugConfiguration.androidComponent);
         }
       }
 
diff --git a/lldb/tools/lldb-dap/src-ts/android/adb-client.ts b/lldb/tools/lldb-dap/src-ts/android/adb-client.ts
index 64030657aeda7..723e4adac779b 100644
--- a/lldb/tools/lldb-dap/src-ts/android/adb-client.ts
+++ b/lldb/tools/lldb-dap/src-ts/android/adb-client.ts
@@ -53,6 +53,18 @@ export class AdbClient {
         return this.deviceSerial;
     }
 
+    async getDeviceName(): Promise<string> {
+        let emulatorName = await this.shellCommandToString("getprop ro.boot.qemu.avd_name");
+        emulatorName = emulatorName.trim();
+        if (emulatorName)
+            return emulatorName;
+        let deviceName = await this.shellCommandToString("settings get global device_name");
+        deviceName = deviceName.trim();
+        if (deviceName)
+            return deviceName;
+        return this.getDeviceSerial();
+    }
+
     async shellCommand(command: string): Promise<void> {
         const deviceSerial = this.getDeviceSerial();
         const connection = await this.createAdbConnection();
diff --git a/lldb/tools/lldb-dap/src-ts/android/android-component-tracker.ts b/lldb/tools/lldb-dap/src-ts/android/android-component-tracker.ts
index 2bcc79d417465..80c551ba88f48 100644
--- a/lldb/tools/lldb-dap/src-ts/android/android-component-tracker.ts
+++ b/lldb/tools/lldb-dap/src-ts/android/android-component-tracker.ts
@@ -24,7 +24,6 @@ export class AndroidComponentTracker {
 
     /**
      * Component name is in the form "com.example.app/.MainActivity".
-     * TODO: allow selecting the deviceId
      */
     constructor(session: vscode.DebugSession, componentName: string) {
         this.componentName = componentName;
@@ -47,9 +46,8 @@ export class AndroidComponentTracker {
         return parts[1];
     }
 
-    async startDebugSession() {
-        // TODO: allow selecting the deviceId
-        await this.apkDebugSession.start(undefined, this.componentName, true);
+    async startDebugSession(deviceSerial: string | undefined) {
+        await this.apkDebugSession.start(deviceSerial, this.componentName, true);
     }
 
     async stopDebugSession() {
diff --git a/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts b/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts
new file mode 100644
index 0000000000000..cb66969de7322
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts
@@ -0,0 +1,42 @@
+import { ErrorWithNotification } from "../ui/error-with-notification";
+import { ConfigureButton } from "../ui/show-error-message";
+import { AdbClient } from "./adb-client";
+
+export class AndroidConfigurationBuilder {
+
+    static async resolveDeviceSerial(device?: string): Promise<string> {
+        const adbClient = new AdbClient();
+        const deviceSerials = await adbClient.getDeviceList();
+        if (!device) {
+            if (deviceSerials.length === 1) {
+                return deviceSerials[0];
+            }
+            if (deviceSerials.length > 1) {
+                throw new ErrorWithNotification(
+                    `Multiple connected Android devices found, please specify a device name or serial number in your launch configuration, property "androidDevice".`,
+                    new ConfigureButton(),
+                );
+            }
+            throw new ErrorWithNotification(
+                `No connected Android devices found.`,
+            );
+        }
+        for (const deviceSerial of deviceSerials) {
+            if (deviceSerial === device) {
+                return deviceSerial;
+            }
+        }
+        for (const deviceSerial of deviceSerials) {
+            const adbClient = new AdbClient();
+            adbClient.setDeviceSerial(deviceSerial);
+            const name = await adbClient.getDeviceName();
+            if (name === device) {
+                return deviceSerial;
+            }
+        }
+        throw new ErrorWithNotification(
+            `Android devices "${device}" not found, please connect this device.`,
+            new ConfigureButton(),
+        );
+    }
+}

>From 59b23035facb1a93b750b15419bfc814d63ad2d3 Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Thu, 18 Dec 2025 17:13:31 +0100
Subject: [PATCH 06/18] [lldb-dap] group android configuration builders

---
 .../src/debug-configuration-provider.ts        |  5 ++---
 .../android/android-configuration-builder.ts   | 15 +++++++++++++++
 .../src-ts/android/apk-debug-configuration.ts  | 18 ------------------
 3 files changed, 17 insertions(+), 21 deletions(-)
 delete mode 100644 lldb/tools/lldb-dap/src-ts/android/apk-debug-configuration.ts

diff --git a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
index 337af1b0e60b7..f1b1b0b107d5e 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
@@ -5,8 +5,7 @@ import { createDebugAdapterExecutable } from "./debug-adapter-factory";
 import { LLDBDapServer } from "./lldb-dap-server";
 import { LogFilePathProvider } from "./logging";
 import { ErrorWithNotification } from "./ui/error-with-notification";
-import { ConfigureButton } from "./ui/show-error-message";
-import { ApkDebugConfiguration } from "./android/apk-debug-configuration";
+import { LogFilePathProvider } from "./logging";
 import { AndroidConfigurationBuilder } from "./android/android-configuration-builder";
 
 const exec = util.promisify(child_process.execFile);
@@ -244,7 +243,7 @@ export class LLDBDapConfigurationProvider
             console.log(`Android device serial number: ${debugConfiguration.androidDeviceSerial}`);
           }
           debugConfiguration.launchCommands =
-            ApkDebugConfiguration.getLldbLaunchCommands(debugConfiguration.androidDeviceSerial, debugConfiguration.androidComponent);
+            AndroidConfigurationBuilder.getLldbLaunchCommands(debugConfiguration.androidDeviceSerial, debugConfiguration.androidComponent);
         }
       }
 
diff --git a/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts b/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts
index cb66969de7322..bdc28fee50302 100644
--- a/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts
+++ b/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts
@@ -39,4 +39,19 @@ export class AndroidConfigurationBuilder {
             new ConfigureButton(),
         );
     }
+
+    static getLldbLaunchCommands(deviceSerial: string | undefined, componentName: string): string[] {
+        if (deviceSerial === undefined) {
+            deviceSerial = "";
+        }
+        const appId = componentName.split("/")[0];
+        return [
+            `platform select remote-android`,
+            `platform connect unix-abstract-connect://${deviceSerial}/${appId}/lldb-platform.sock`,
+            `process attach --name ${appId}`,
+            `process handle SIGSEGV -n false -p true -s false`,
+            `process handle SIGBUS -n false -p true -s false`,
+            `process handle SIGCHLD -n false -p true -s false`,
+        ];
+    }
 }
diff --git a/lldb/tools/lldb-dap/src-ts/android/apk-debug-configuration.ts b/lldb/tools/lldb-dap/src-ts/android/apk-debug-configuration.ts
deleted file mode 100644
index f6e6e393e9f6d..0000000000000
--- a/lldb/tools/lldb-dap/src-ts/android/apk-debug-configuration.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-
-export class ApkDebugConfiguration {
-
-    static getLldbLaunchCommands(deviceSerial: string | undefined, componentName: string): string[] {
-        if (deviceSerial === undefined) {
-            deviceSerial = "";
-        }
-        const appId = componentName.split("/")[0];
-        return [
-            `platform select remote-android`,
-            `platform connect unix-abstract-connect://${deviceSerial}/${appId}/lldb-platform.sock`,
-            `process attach --name ${appId}`,
-            `process handle SIGSEGV -n false -p true -s false`,
-            `process handle SIGBUS -n false -p true -s false`,
-            `process handle SIGCHLD -n false -p true -s false`,
-        ];
-    }
-}

>From a2e164e1acac0819d5fc5140a11c5a11cc01944c Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Fri, 19 Dec 2025 08:04:27 +0100
Subject: [PATCH 07/18] [lldb-dap] restructure the android session tracker

---
 .../extension/src/debug-adapter-factory.ts    |  6 +-
 .../extension/src/debug-session-tracker.ts    | 10 ++--
 .../android/android-component-tracker.ts      | 60 -------------------
 .../android/android-configuration-builder.ts  | 15 +----
 .../src-ts/android/android-session-tracker.ts | 41 +++++++++++++
 .../src-ts/android/apk-debug-session.ts       | 35 +++++++++--
 6 files changed, 82 insertions(+), 85 deletions(-)
 delete mode 100644 lldb/tools/lldb-dap/src-ts/android/android-component-tracker.ts
 create mode 100644 lldb/tools/lldb-dap/src-ts/android/android-session-tracker.ts

diff --git a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
index 59ef46f3b157c..be9abed44229a 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
@@ -7,7 +7,7 @@ import { LogFilePathProvider, LogType } from "./logging";
 import { ErrorWithNotification } from "./ui/error-with-notification";
 import { ConfigureButton, OpenSettingsButton } from "./ui/show-error-message";
 import { expandUser } from "./utils";
-import { AndroidComponentTracker } from "./android/android-component-tracker";
+import { AndroidSessionTracker } from "./android/android-session-tracker";
 
 const exec = util.promisify(child_process.execFile);
 
@@ -333,9 +333,9 @@ export class LLDBDapDescriptorFactory
       this.logger.info(
         `Session "${session.name}" is an Android debug session for component ${session.configuration.androidComponent}.`,
       );
-      const tracker = new AndroidComponentTracker(session, session.configuration.androidComponent);
+      const tracker = new AndroidSessionTracker(session);
       // TODO: handled exceptions
-      await tracker.startDebugSession(session.configuration.androidDeviceSerial);
+      await tracker.startDebugSession();
     }
 
     // Use a server connection if the debugAdapterPort is provided
diff --git a/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts b/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
index dfa4381e8ba4d..b56f0dbc133ce 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
@@ -1,6 +1,6 @@
 import { DebugProtocol } from "@vscode/debugprotocol";
 import * as vscode from "vscode";
-import { AndroidComponentTracker } from "./android/android-component-tracker";
+import { AndroidSessionTracker } from "./android/android-session-tracker";
 
 export interface LLDBDapCapabilities extends DebugProtocol.Capabilities {
   /** The debug adapter supports the `moduleSymbols` request. */
@@ -105,9 +105,9 @@ export class DebugSessionTracker
 
   /** Clear information from the active session. */
   private onExit(session: vscode.DebugSession) {
-    const androidComponentTracker = AndroidComponentTracker.getFromSession(session);
+    const androidComponentTracker = AndroidSessionTracker.getFromSession(session);
     if (androidComponentTracker) {
-      this.logger.info(`Stopping android APK "${androidComponentTracker.componentName}"`);
+      this.logger.info(`Stopping android APK "${session.configuration.androidComponent}"`);
       androidComponentTracker.stopDebugSession().catch();
     }
     this.modules.delete(session);
@@ -137,10 +137,10 @@ export class DebugSessionTracker
   private onWillReceiveMessage(session: vscode.DebugSession, message: DebugProtocol.Request) {
     this.logger.info(`Received message: ${JSON.stringify(message)}`);
     if (message.command === "configurationDone") {
-      const androidComponentTracker = AndroidComponentTracker.getFromSession(session);
+      const androidComponentTracker = AndroidSessionTracker.getFromSession(session);
       if (androidComponentTracker) {
         this.logger.info(
-          `Dismissing Waiting-For-Debugger dialog on Android APK "${androidComponentTracker.componentName}"`
+          `Dismissing Waiting-For-Debugger dialog on Android APK "${session.configuration.androidComponent}"`
         );
         androidComponentTracker.dismissWaitingForDebuggerDialog().catch();
       }
diff --git a/lldb/tools/lldb-dap/src-ts/android/android-component-tracker.ts b/lldb/tools/lldb-dap/src-ts/android/android-component-tracker.ts
deleted file mode 100644
index 80c551ba88f48..0000000000000
--- a/lldb/tools/lldb-dap/src-ts/android/android-component-tracker.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import * as vscode from "vscode";
-import { ApkDebugSession } from "./apk-debug-session";
-
-/**
- * Tracks an Android component associated with a VS Code debug session.
- * Normally, the component is an activity inside an APK.
- * This class includes everything needed to start and stop the activity
- * and lldb-server on the target device, and dismiss the "waiting for debugger"
- * dialog.
- * Here, we assume the target device is connected (or the emulator is running),
- * the APK is installed, and the ADB daemon is running.
- */
-export class AndroidComponentTracker {
-
-    private static catalog = new WeakMap<vscode.DebugSession, AndroidComponentTracker>();
-
-    static getFromSession(session: vscode.DebugSession): AndroidComponentTracker | undefined {
-        return AndroidComponentTracker.catalog.get(session);
-    }
-
-    readonly componentName: string;
-
-    private apkDebugSession = new ApkDebugSession();
-
-    /**
-     * Component name is in the form "com.example.app/.MainActivity".
-     */
-    constructor(session: vscode.DebugSession, componentName: string) {
-        this.componentName = componentName;
-        AndroidComponentTracker.catalog.set(session, this);
-    }
-
-    getAppId(): string {
-        const parts = this.componentName.split('/');
-        return parts[0];
-    }
-
-    getActivity(): string {
-        const parts = this.componentName.split('/');
-        if (parts.length === 1) {
-            return parts[0] + ".MainActivity";
-        }
-        if (parts[1].startsWith('.')) {
-            return parts[0] + parts[1];
-        }
-        return parts[1];
-    }
-
-    async startDebugSession(deviceSerial: string | undefined) {
-        await this.apkDebugSession.start(deviceSerial, this.componentName, true);
-    }
-
-    async stopDebugSession() {
-        await this.apkDebugSession.stop();
-    }
-
-    async dismissWaitingForDebuggerDialog() {
-        await this.apkDebugSession.dismissWaitingForDebuggerDialog();
-    }
-}
diff --git a/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts b/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts
index bdc28fee50302..6ca5349727f39 100644
--- a/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts
+++ b/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts
@@ -1,6 +1,7 @@
 import { ErrorWithNotification } from "../ui/error-with-notification";
 import { ConfigureButton } from "../ui/show-error-message";
 import { AdbClient } from "./adb-client";
+import { ApkDebugSession } from "./apk-debug-session";
 
 export class AndroidConfigurationBuilder {
 
@@ -41,17 +42,7 @@ export class AndroidConfigurationBuilder {
     }
 
     static getLldbLaunchCommands(deviceSerial: string | undefined, componentName: string): string[] {
-        if (deviceSerial === undefined) {
-            deviceSerial = "";
-        }
-        const appId = componentName.split("/")[0];
-        return [
-            `platform select remote-android`,
-            `platform connect unix-abstract-connect://${deviceSerial}/${appId}/lldb-platform.sock`,
-            `process attach --name ${appId}`,
-            `process handle SIGSEGV -n false -p true -s false`,
-            `process handle SIGBUS -n false -p true -s false`,
-            `process handle SIGCHLD -n false -p true -s false`,
-        ];
+        const apkDebugSession = new ApkDebugSession(deviceSerial, componentName);
+        return apkDebugSession.getLldbLaunchCommands();
     }
 }
diff --git a/lldb/tools/lldb-dap/src-ts/android/android-session-tracker.ts b/lldb/tools/lldb-dap/src-ts/android/android-session-tracker.ts
new file mode 100644
index 0000000000000..c170557a0a4f5
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/android/android-session-tracker.ts
@@ -0,0 +1,41 @@
+import * as vscode from "vscode";
+import { ApkDebugSession } from "./apk-debug-session";
+
+/**
+ * This class is for tracking the Android APK debug session associated with the
+ * VS Code debug session.
+ * It includes everything needed to start and stop the Android activity
+ * and lldb-server on the target device, and dismiss the "waiting for debugger"
+ * dialog.
+ * It assumes that the target device is connected (or the emulator is running),
+ * the APK is installed, and the ADB daemon is running.
+ */
+export class AndroidSessionTracker {
+
+    private static catalog = new WeakMap<vscode.DebugSession, AndroidSessionTracker>();
+
+    static getFromSession(session: vscode.DebugSession): AndroidSessionTracker | undefined {
+        return AndroidSessionTracker.catalog.get(session);
+    }
+
+    private apkDebugSession: ApkDebugSession;
+
+    constructor(session: vscode.DebugSession) {
+        const deviceSerial = session.configuration.androidDeviceSerial;
+        const componentName = session.configuration.androidComponent;
+        this.apkDebugSession = new ApkDebugSession(deviceSerial, componentName);
+        AndroidSessionTracker.catalog.set(session, this);
+    }
+
+    async startDebugSession() {
+        await this.apkDebugSession.start(true);
+    }
+
+    async stopDebugSession() {
+        await this.apkDebugSession.stop();
+    }
+
+    async dismissWaitingForDebuggerDialog() {
+        await this.apkDebugSession.dismissWaitingForDebuggerDialog();
+    }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts b/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
index bb51386c2da26..2a2fc65106099 100644
--- a/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
+++ b/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
@@ -11,16 +11,41 @@ import Env from "./env";
 export class ApkDebugSession {
 
     private runningSession: SessionInfo | undefined;
+    readonly deviceSerial: string | undefined;
+    readonly componentName: string;
 
     /**
      * Component name is in the form "com.example.app/.MainActivity".
+     */
+    constructor(deviceSerial: string | undefined, componentName: string) {
+        this.deviceSerial = deviceSerial;
+        this.componentName = componentName;
+    }
+
+    getLldbLaunchCommands(): string[] {
+        let deviceSerial = this.deviceSerial;
+        if (deviceSerial === undefined) {
+            deviceSerial = "";
+        }
+        const appId = this.componentName.split("/")[0];
+        return [
+            `platform select remote-android`,
+            `platform connect unix-abstract-connect://${deviceSerial}/${appId}/lldb-platform.sock`,
+            `process attach --name ${appId}`,
+            `process handle SIGSEGV -n false -p true -s false`,
+            `process handle SIGBUS -n false -p true -s false`,
+            `process handle SIGCHLD -n false -p true -s false`,
+        ];
+    }
+
+    /**
      * `wfd` stays for "waiting for debugger".
      */
-    async start(deviceSerial: string | undefined, componentName: string, wfd: boolean) {
-        const addId = componentName.split('/')[0];
+    async start(wfd: boolean) {
+        const addId = this.componentName.split('/')[0];
         const adb = new AdbClient();
-        if (deviceSerial !== undefined) {
-            adb.setDeviceSerial(deviceSerial);
+        if (this.deviceSerial !== undefined) {
+            adb.setDeviceSerial(this.deviceSerial);
         } else {
             await adb.autoDetectDeviceSerial();
         }
@@ -32,7 +57,7 @@ export class ApkDebugSession {
         await this.stop();
         await this.cleanUpEarlierDebugSessions(adb, addId);
         await this.installLldbServer(adb, addId, lldbServerPath);
-        const pid = await this.startApk(adb, componentName, wfd);
+        await this.startApk(adb, this.componentName, wfd);
 
         const abortController = new AbortController();
         const endPromise = this.startLldbServer(adb, addId, abortController.signal);

>From 7409e67b6ab63772a341643187a52577e0ef8e40 Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Fri, 19 Dec 2025 08:18:40 +0100
Subject: [PATCH 08/18] [lldb-dap] better report ADB connection problems

---
 .../android/android-configuration-builder.ts       | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts b/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts
index 6ca5349727f39..2a7df793a709a 100644
--- a/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts
+++ b/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts
@@ -7,7 +7,14 @@ export class AndroidConfigurationBuilder {
 
     static async resolveDeviceSerial(device?: string): Promise<string> {
         const adbClient = new AdbClient();
-        const deviceSerials = await adbClient.getDeviceList();
+        let deviceSerials: string[];
+        try {
+            deviceSerials = await adbClient.getDeviceList();
+        } catch (e) {
+            throw new ErrorWithNotification(
+                `Could not connect to ADB server. Please make sure the ADB server is running and at least one device or emulator is connected.`,
+            );
+        }
         if (!device) {
             if (deviceSerials.length === 1) {
                 return deviceSerials[0];
@@ -19,7 +26,7 @@ export class AndroidConfigurationBuilder {
                 );
             }
             throw new ErrorWithNotification(
-                `No connected Android devices found.`,
+                `No Android devices found. Please verify that at least one device or emulator is connected to the ADB server.`,
             );
         }
         for (const deviceSerial of deviceSerials) {
@@ -36,8 +43,7 @@ export class AndroidConfigurationBuilder {
             }
         }
         throw new ErrorWithNotification(
-            `Android devices "${device}" not found, please connect this device.`,
-            new ConfigureButton(),
+            `Android devices "${device}" not found. Please connect this device or emulator to the ADB server.`,
         );
     }
 

>From e0d232f10f6f3bd4ecbc7c24c2bb975e8ca9951d Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Wed, 7 Jan 2026 15:24:25 +0100
Subject: [PATCH 09/18] [lldb-dap] adopt new source file hierarchy

---
 .../lldb-dap/{src-ts => extension/src}/android/adb-client.ts    | 0
 .../{src-ts => extension/src}/android/adb-connection.ts         | 0
 .../src}/android/android-configuration-builder.ts               | 0
 .../src}/android/android-session-tracker.ts                     | 0
 .../{src-ts => extension/src}/android/apk-debug-session.ts      | 0
 .../lldb-dap/{src-ts => extension/src}/android/connection.ts    | 0
 lldb/tools/lldb-dap/{src-ts => extension/src}/android/env.ts    | 0
 lldb/tools/lldb-dap/{src-ts => extension/src}/android/jdwp.ts   | 0
 .../lldb-dap/extension/src/debug-configuration-provider.ts      | 2 +-
 9 files changed, 1 insertion(+), 1 deletion(-)
 rename lldb/tools/lldb-dap/{src-ts => extension/src}/android/adb-client.ts (100%)
 rename lldb/tools/lldb-dap/{src-ts => extension/src}/android/adb-connection.ts (100%)
 rename lldb/tools/lldb-dap/{src-ts => extension/src}/android/android-configuration-builder.ts (100%)
 rename lldb/tools/lldb-dap/{src-ts => extension/src}/android/android-session-tracker.ts (100%)
 rename lldb/tools/lldb-dap/{src-ts => extension/src}/android/apk-debug-session.ts (100%)
 rename lldb/tools/lldb-dap/{src-ts => extension/src}/android/connection.ts (100%)
 rename lldb/tools/lldb-dap/{src-ts => extension/src}/android/env.ts (100%)
 rename lldb/tools/lldb-dap/{src-ts => extension/src}/android/jdwp.ts (100%)

diff --git a/lldb/tools/lldb-dap/src-ts/android/adb-client.ts b/lldb/tools/lldb-dap/extension/src/android/adb-client.ts
similarity index 100%
rename from lldb/tools/lldb-dap/src-ts/android/adb-client.ts
rename to lldb/tools/lldb-dap/extension/src/android/adb-client.ts
diff --git a/lldb/tools/lldb-dap/src-ts/android/adb-connection.ts b/lldb/tools/lldb-dap/extension/src/android/adb-connection.ts
similarity index 100%
rename from lldb/tools/lldb-dap/src-ts/android/adb-connection.ts
rename to lldb/tools/lldb-dap/extension/src/android/adb-connection.ts
diff --git a/lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts b/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
similarity index 100%
rename from lldb/tools/lldb-dap/src-ts/android/android-configuration-builder.ts
rename to lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
diff --git a/lldb/tools/lldb-dap/src-ts/android/android-session-tracker.ts b/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
similarity index 100%
rename from lldb/tools/lldb-dap/src-ts/android/android-session-tracker.ts
rename to lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
diff --git a/lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts b/lldb/tools/lldb-dap/extension/src/android/apk-debug-session.ts
similarity index 100%
rename from lldb/tools/lldb-dap/src-ts/android/apk-debug-session.ts
rename to lldb/tools/lldb-dap/extension/src/android/apk-debug-session.ts
diff --git a/lldb/tools/lldb-dap/src-ts/android/connection.ts b/lldb/tools/lldb-dap/extension/src/android/connection.ts
similarity index 100%
rename from lldb/tools/lldb-dap/src-ts/android/connection.ts
rename to lldb/tools/lldb-dap/extension/src/android/connection.ts
diff --git a/lldb/tools/lldb-dap/src-ts/android/env.ts b/lldb/tools/lldb-dap/extension/src/android/env.ts
similarity index 100%
rename from lldb/tools/lldb-dap/src-ts/android/env.ts
rename to lldb/tools/lldb-dap/extension/src/android/env.ts
diff --git a/lldb/tools/lldb-dap/src-ts/android/jdwp.ts b/lldb/tools/lldb-dap/extension/src/android/jdwp.ts
similarity index 100%
rename from lldb/tools/lldb-dap/src-ts/android/jdwp.ts
rename to lldb/tools/lldb-dap/extension/src/android/jdwp.ts
diff --git a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
index f1b1b0b107d5e..7e59ebb04f10c 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
@@ -5,7 +5,7 @@ import { createDebugAdapterExecutable } from "./debug-adapter-factory";
 import { LLDBDapServer } from "./lldb-dap-server";
 import { LogFilePathProvider } from "./logging";
 import { ErrorWithNotification } from "./ui/error-with-notification";
-import { LogFilePathProvider } from "./logging";
+import { ConfigureButton } from "./ui/show-error-message";
 import { AndroidConfigurationBuilder } from "./android/android-configuration-builder";
 
 const exec = util.promisify(child_process.execFile);

>From 384eac1384fe4ac3e1b75de1f6f6a64c8a2ce58c Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Thu, 8 Jan 2026 14:35:23 +0100
Subject: [PATCH 10/18] [lldb-dap] add configuration property androidNDKPath

---
 lldb/tools/lldb-dap/extension/package.json    | 19 ++++--
 .../android/android-configuration-builder.ts  | 48 ++++++++++++-
 .../src/android/android-session-tracker.ts    |  5 +-
 .../src/android/apk-debug-session.ts          | 43 +++++++++---
 .../lldb-dap/extension/src/android/env.ts     | 52 +-------------
 .../lldb-dap/extension/src/android/ndk.ts     | 67 +++++++++++++++++++
 .../src/debug-configuration-provider.ts       | 41 ++++++++++--
 7 files changed, 206 insertions(+), 69 deletions(-)
 create mode 100644 lldb/tools/lldb-dap/extension/src/android/ndk.ts

diff --git a/lldb/tools/lldb-dap/extension/package.json b/lldb/tools/lldb-dap/extension/package.json
index 4148f66f2eb98..051425fe68b47 100644
--- a/lldb/tools/lldb-dap/extension/package.json
+++ b/lldb/tools/lldb-dap/extension/package.json
@@ -282,10 +282,10 @@
             "description": "Commands executed when the debugging session ends.",
             "default": []
           },
-          "lldb-dap.androidComponent": {
+          "lldb-dap.androidNDKPath": {
             "order": 8,
             "type": "string",
-            "description": "If non-empty, defines which Android activity to be launched and debugged. The name must have the format `package/name`. For example, `com.example.app/.MainActivity`.",
+            "description": "The path to the Android NDK",
             "default": ""
           },
           "lldb-dap.androidDevice": {
@@ -293,6 +293,12 @@
             "type": "string",
             "description": "If non-empty, defines the name or serial number of the Android device or emulator used for debugging.",
             "default": ""
+          },
+          "lldb-dap.androidComponent": {
+            "order": 10,
+            "type": "string",
+            "description": "If non-empty, defines which Android activity to be launched and debugged. The name must have the format `package/name`. For example, `com.example.app/.MainActivity`.",
+            "default": ""
           }
         }
       }
@@ -692,15 +698,20 @@
                 "description": "If non-empty, threads will have descriptions generated based on the provided format. See https://lldb.llvm.org/use/formatting.html for an explanation on format strings for threads. If the format string contains errors, an error message will be displayed on the Debug Console and the default thread names will be used. This might come with a performance cost because debug information might need to be processed to generate the description.",
                 "default": ""
               },
-              "androidComponent": {
+              "androidNDKPath": {
                 "type": "string",
-                "description": "If non-empty, defines which Android activity to be launched and debugged. The name must have the format `package/name`. For example, `com.example.app/.MainActivity`.",
+                "description": "The path to the Android NDK",
                 "default": ""
               },
               "androidDevice": {
                 "type": "string",
                 "description": "If non-empty, defines the name or serial number of the Android device or emulator used for debugging.",
                 "default": ""
+              },
+              "androidComponent": {
+                "type": "string",
+                "description": "If non-empty, defines which Android activity to be launched and debugged. The name must have the format `package/name`. For example, `com.example.app/.MainActivity`.",
+                "default": ""
               }
             }
           },
diff --git a/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts b/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
index 2a7df793a709a..8b2fda6808bcc 100644
--- a/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
@@ -1,10 +1,33 @@
 import { ErrorWithNotification } from "../ui/error-with-notification";
-import { ConfigureButton } from "../ui/show-error-message";
+import { ConfigureButton, OpenSettingsButton } from "../ui/show-error-message";
 import { AdbClient } from "./adb-client";
 import { ApkDebugSession } from "./apk-debug-session";
+import { Ndk } from "./ndk";
 
 export class AndroidConfigurationBuilder {
 
+    static async getDefaultNdkPath(): Promise<string> {
+        const path = await Ndk.getDefaultPath();
+        if (!path) {
+            throw new ErrorWithNotification(
+                `Unable to find the Android NDK. Please install it in its default location or define its path in the settings.`,
+                new OpenSettingsButton("lldb-dap.androidNDKPath"),
+            );
+        }
+        return path;
+    }
+
+    static async checkNdkAndRetrieveVersion(ndkPath: string): Promise<string> {
+        const version = await Ndk.getVersion(ndkPath);
+        if (!version) {
+            throw new ErrorWithNotification(
+                `Invalid Android NDK path "${ndkPath}". Please ensure the NDK is installed and the path is properly defined in the settings.`,
+                new OpenSettingsButton("lldb-dap.androidNDKPath"),
+            );
+        }
+        return version;
+    }
+
     static async resolveDeviceSerial(device?: string): Promise<string> {
         const adbClient = new AdbClient();
         let deviceSerials: string[];
@@ -47,8 +70,29 @@ export class AndroidConfigurationBuilder {
         );
     }
 
+    /**
+     * Returned arch can be: aarch64, riscv64, arm, x86_64, i386
+     */
+    static async getTargetArch(deviceSerial: string): Promise<string> {
+        const adbClient = new AdbClient();
+        adbClient.setDeviceSerial(deviceSerial);
+        const arch = await adbClient.shellCommandToString("uname -m");
+        return arch.trim();
+    }
+
+    static async getLldbServerPath(ndkPath: string, targetArch: string): Promise<string | undefined> {
+        const path = await Ndk.getLldbServerPath(ndkPath, targetArch);
+        if (!path) {
+            throw new ErrorWithNotification(
+                `Could not find lldb-server in the NDK at path "${ndkPath}" for target architecture "${targetArch}". Please verify that the NDK path is correct and that the NDK includes support for this architecture.`,
+            );
+        }
+        return path;
+    }
+
     static getLldbLaunchCommands(deviceSerial: string | undefined, componentName: string): string[] {
-        const apkDebugSession = new ApkDebugSession(deviceSerial, componentName);
+        // We create a temporary ApkDebugSession instance just to get the launch commands.
+        const apkDebugSession = new ApkDebugSession(undefined, deviceSerial, componentName);
         return apkDebugSession.getLldbLaunchCommands();
     }
 }
diff --git a/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts b/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
index c170557a0a4f5..51a3aaeeffa23 100644
--- a/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
@@ -1,5 +1,5 @@
 import * as vscode from "vscode";
-import { ApkDebugSession } from "./apk-debug-session";
+import { ApkDebugEnv, ApkDebugSession } from "./apk-debug-session";
 
 /**
  * This class is for tracking the Android APK debug session associated with the
@@ -21,9 +21,10 @@ export class AndroidSessionTracker {
     private apkDebugSession: ApkDebugSession;
 
     constructor(session: vscode.DebugSession) {
+        const env = { lldbServerPath: session.configuration.androidLldbServerPath };
         const deviceSerial = session.configuration.androidDeviceSerial;
         const componentName = session.configuration.androidComponent;
-        this.apkDebugSession = new ApkDebugSession(deviceSerial, componentName);
+        this.apkDebugSession = new ApkDebugSession(env, deviceSerial, componentName);
         AndroidSessionTracker.catalog.set(session, this);
     }
 
diff --git a/lldb/tools/lldb-dap/extension/src/android/apk-debug-session.ts b/lldb/tools/lldb-dap/extension/src/android/apk-debug-session.ts
index 2a2fc65106099..7b648f43f0a36 100644
--- a/lldb/tools/lldb-dap/extension/src/android/apk-debug-session.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/apk-debug-session.ts
@@ -1,5 +1,5 @@
 import { AdbClient } from "./adb-client";
-import Env from "./env";
+import { Ndk } from "./ndk";
 
 /**
  * This class controls the execution of an Android APK for debugging.
@@ -11,13 +11,17 @@ import Env from "./env";
 export class ApkDebugSession {
 
     private runningSession: SessionInfo | undefined;
+    readonly env: ApkDebugEnv | undefined;
     readonly deviceSerial: string | undefined;
     readonly componentName: string;
 
     /**
      * Component name is in the form "com.example.app/.MainActivity".
+     * If the env is undefined, a default one will be created when starting.
+     * If the deviceSerial is undefined, we expect that only one device is connected.
      */
-    constructor(deviceSerial: string | undefined, componentName: string) {
+    constructor(env: ApkDebugEnv | undefined, deviceSerial: string | undefined, componentName: string) {
+        this.env = env;
         this.deviceSerial = deviceSerial;
         this.componentName = componentName;
     }
@@ -39,6 +43,7 @@ export class ApkDebugSession {
     }
 
     /**
+     * Start the debug session.
      * `wfd` stays for "waiting for debugger".
      */
     async start(wfd: boolean) {
@@ -49,14 +54,14 @@ export class ApkDebugSession {
         } else {
             await adb.autoDetectDeviceSerial();
         }
-        const arch = (await adb.shellCommandToString("uname -m")).trim();
-        const lldbServerPath = await Env.getLldbServerPath(arch);
-        if (!lldbServerPath) {
-            throw new Error("Could not find LLDB server in Android NDK");
+        let env = this.env;
+        if (!env) {
+            const targetArch = (await adb.shellCommandToString("uname -m")).trim();
+            env = await this.createDefaultEnv(targetArch);
         }
         await this.stop();
         await this.cleanUpEarlierDebugSessions(adb, addId);
-        await this.installLldbServer(adb, addId, lldbServerPath);
+        await this.installLldbServer(adb, addId, env.lldbServerPath);
         await this.startApk(adb, this.componentName, wfd);
 
         const abortController = new AbortController();
@@ -199,9 +204,31 @@ export class ApkDebugSession {
             await new Promise(resolve => setTimeout(resolve, 100));
         }
     }
+
+    private async createDefaultEnv(targetArch: string): Promise<ApkDebugEnv> {
+        const ndkPath = await Ndk.getDefaultPath();
+        if (!ndkPath) {
+            throw new Error("NDK not found");
+        }
+        const ndkVersion = await Ndk.getVersion(ndkPath);
+        if (!ndkVersion) {
+            throw new Error(`Invalid NDK at path "${ndkPath}"`);
+        }
+        const lldbServerPath = await Ndk.getLldbServerPath(ndkPath, targetArch);
+        if (!lldbServerPath) {
+            throw new Error(`Could not find lldb-server in the NDK at path "${ndkPath}" for target architecture "${targetArch}"`);
+        }
+        return {
+            lldbServerPath,
+        };
+    }
+}
+
+export interface ApkDebugEnv {
+    lldbServerPath: string; // path to the lldb-server executable on the host machine
 }
 
-type SessionInfo = {
+interface SessionInfo {
     adb: AdbClient;
     addId: string;
     endPromise: Promise<void>;
diff --git a/lldb/tools/lldb-dap/extension/src/android/env.ts b/lldb/tools/lldb-dap/extension/src/android/env.ts
index 1b07aae25d2bc..2c406dd62f811 100644
--- a/lldb/tools/lldb-dap/extension/src/android/env.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/env.ts
@@ -3,56 +3,10 @@ import * as fs from "node:fs/promises";
 import * as os from "node:os";
 
 /**
- * TODO: revisit everything!
+ * TODO: probably not useful anymore
+ * @deprecated
  */
 namespace Env {
-
-    export async function getAndroidNdkPath(): Promise<string | undefined> {
-        const home = os.homedir();
-        const ndk = path.join(home, "Library", "Android", "sdk", "ndk");
-        const entries = await fs.readdir(ndk);
-        if (entries.length === 0) {
-            return undefined;
-        }
-        entries.sort((a, b) => b.localeCompare(a, 'en-US', { numeric: true }));
-        return path.join(ndk, entries[0]);
-    }
-
-    export async function getLldbServerPath(arch: string): Promise<string | undefined> {
-        // supported arch: aarch64, riscv64, arm, x86_64, i386
-        const ndk = await getAndroidNdkPath();
-        if (ndk) {
-            const root1 = path.join(ndk, "toolchains", "llvm", "prebuilt");
-            try {
-                const entries1 = await fs.readdir(root1);
-                for (const entry1 of entries1) {
-                    if (entry1.startsWith("darwin-")) {
-                        const root2 = path.join(root1, entry1, "lib", "clang");
-                        try {
-                            const entries2 = await fs.readdir(root2);
-                            for (const entry2 of entries2) {
-                                const root3 = path.join(root2, entry2, "lib", "linux");
-                                try {
-                                    const entries3 = await fs.readdir(root3);
-                                    for (const entry3 of entries3) {
-                                        if (entry3 === arch) {
-                                            const candidate = path.join(root3, entry3, "lldb-server");
-                                            try {
-                                                await fs.access(candidate, fs.constants.R_OK);
-                                                return candidate;
-                                            } catch {}
-                                        }
-                                    }
-                                } catch {}
-                            }
-                        } catch {}
-                    }
-                }
-            } catch {}
-        }
-        return undefined;
-    }
-
     async function getDataFolder(): Promise<string | undefined> {
         const home = os.homedir();
         try {
@@ -62,7 +16,7 @@ namespace Env {
             return dataFolder;
         } catch {}
         return undefined;
-    }    
+    }
 }
 
 export default Env;
diff --git a/lldb/tools/lldb-dap/extension/src/android/ndk.ts b/lldb/tools/lldb-dap/extension/src/android/ndk.ts
new file mode 100644
index 0000000000000..a7508f834aac6
--- /dev/null
+++ b/lldb/tools/lldb-dap/extension/src/android/ndk.ts
@@ -0,0 +1,67 @@
+import * as path from "node:path";
+import * as fs from "node:fs/promises";
+import * as os from "node:os";
+
+/**
+ * Gives access to elements of the Android NDK.
+ */
+export class Ndk {
+
+    static async getDefaultPath(): Promise<string | undefined> {
+        const home = os.homedir();
+        const ndk = path.join(home, "Library", "Android", "sdk", "ndk");
+        let entries: string[] = [];
+        try {
+            entries = await fs.readdir(ndk);
+        } catch {}
+        if (entries.length === 0) {
+            return undefined;
+        }
+        entries.sort((a, b) => b.localeCompare(a, 'en-US', { numeric: true }));
+        return path.join(ndk, entries[0]);
+    }
+
+    static async getVersion(ndkPath: string): Promise<string | undefined> {
+        const sourcePropsPath = path.join(ndkPath, "source.properties");
+        try {
+            const content = await fs.readFile(sourcePropsPath, { encoding: "utf-8" });
+            const lines = content.split("\n");
+            for (const line of lines) {
+                const match = line.match(/^Pkg.Revision\s*=\s*(.+)$/);
+                if (match) {
+                    return match[1].trim();
+                }
+            }
+        } catch {}
+    }
+
+    static async getLldbServerPath(ndkPath: string, targetArch: string): Promise<string | undefined> {
+        const root1 = path.join(ndkPath, "toolchains", "llvm", "prebuilt");
+        try {
+            const entries1 = await fs.readdir(root1);
+            for (const entry1 of entries1) {
+                if (entry1.startsWith("darwin-")) {
+                    const root2 = path.join(root1, entry1, "lib", "clang");
+                    try {
+                        const entries2 = await fs.readdir(root2);
+                        for (const entry2 of entries2) {
+                            const root3 = path.join(root2, entry2, "lib", "linux");
+                            try {
+                                const entries3 = await fs.readdir(root3);
+                                for (const entry3 of entries3) {
+                                    if (entry3 === targetArch) {
+                                        const candidate = path.join(root3, entry3, "lldb-server");
+                                        try {
+                                            await fs.access(candidate, fs.constants.R_OK);
+                                            return candidate;
+                                        } catch {}
+                                    }
+                                }
+                            } catch {}
+                        }
+                    } catch {}
+                }
+            }
+        } catch {}
+    }
+}
diff --git a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
index 7e59ebb04f10c..8d25b03b27c3e 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
@@ -68,6 +68,11 @@ const configurations: Record<string, DefaultConfig> = {
   stopCommands: { type: "stringArray", default: [] },
   exitCommands: { type: "stringArray", default: [] },
   terminateCommands: { type: "stringArray", default: [] },
+
+  // Keys for Android debugging.
+  androidNDKPath: { type: "string", default: "" },
+  androidDevice: { type: "string", default: "" },
+  androidComponent: { type: "string", default: "" },
 };
 
 export function getDefaultConfigKey(
@@ -233,17 +238,45 @@ export class LLDBDapConfigurationProvider
       }
 
       if (debugConfiguration.androidComponent && debugConfiguration.request === "launch") {
-        // TODO: check ADB, connected devices and anything needed to run the android debug session
         if (
           !debugConfiguration.launchCommands ||
           debugConfiguration.launchCommands.length === 0
         ) {
           if (!debugConfiguration.androidDeviceSerial) {
-            debugConfiguration.androidDeviceSerial = await AndroidConfigurationBuilder.resolveDeviceSerial(debugConfiguration.androidDevice);
-            console.log(`Android device serial number: ${debugConfiguration.androidDeviceSerial}`);
+            debugConfiguration.androidDeviceSerial =
+              await AndroidConfigurationBuilder.resolveDeviceSerial(
+                debugConfiguration.androidDevice
+              );
+          }
+          this.logger.info(`Android device serial number: ${debugConfiguration.androidDeviceSerial}`);
+          if (!debugConfiguration.androidTargetArch) {
+            debugConfiguration.androidTargetArch =
+              await AndroidConfigurationBuilder.getTargetArch(
+                debugConfiguration.androidDeviceSerial
+              );
+          }
+          this.logger.info(`Android target architecture: ${debugConfiguration.androidTargetArch}`);
+          if (!debugConfiguration.androidLldbServerPath) {
+            if (!debugConfiguration.androidNDKPath) {
+              debugConfiguration.androidNDKPath =
+                  await AndroidConfigurationBuilder.getDefaultNdkPath();
+            }
+            const ndkVersion = await AndroidConfigurationBuilder.checkNdkAndRetrieveVersion(
+              debugConfiguration.androidNDKPath
+            );
+            this.logger.info(`Android NDK path: ${debugConfiguration.androidNDKPath}`);
+            this.logger.info(`Android NDK version: ${ndkVersion}`);
+            debugConfiguration.androidLldbServerPath =
+              await AndroidConfigurationBuilder.getLldbServerPath(
+                debugConfiguration.androidNDKPath,
+                debugConfiguration.androidTargetArch
+              );
           }
           debugConfiguration.launchCommands =
-            AndroidConfigurationBuilder.getLldbLaunchCommands(debugConfiguration.androidDeviceSerial, debugConfiguration.androidComponent);
+            AndroidConfigurationBuilder.getLldbLaunchCommands(
+              debugConfiguration.androidDeviceSerial,
+              debugConfiguration.androidComponent
+            );
         }
       }
 

>From e587c1f6dd82c863cb246bb96fce0c321fb94c8d Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Thu, 8 Jan 2026 14:41:40 +0100
Subject: [PATCH 11/18] [lldb-dap] group source that can run outside vs-code

---
 .../extension/src/android/android-configuration-builder.ts  | 6 +++---
 .../extension/src/android/android-session-tracker.ts        | 2 +-
 .../lldb-dap/extension/src/android/{ => core}/adb-client.ts | 0
 .../extension/src/android/{ => core}/adb-connection.ts      | 0
 .../extension/src/android/{ => core}/apk-debug-session.ts   | 0
 .../lldb-dap/extension/src/android/{ => core}/connection.ts | 0
 lldb/tools/lldb-dap/extension/src/android/{ => core}/env.ts | 0
 .../tools/lldb-dap/extension/src/android/{ => core}/jdwp.ts | 0
 lldb/tools/lldb-dap/extension/src/android/{ => core}/ndk.ts | 0
 9 files changed, 4 insertions(+), 4 deletions(-)
 rename lldb/tools/lldb-dap/extension/src/android/{ => core}/adb-client.ts (100%)
 rename lldb/tools/lldb-dap/extension/src/android/{ => core}/adb-connection.ts (100%)
 rename lldb/tools/lldb-dap/extension/src/android/{ => core}/apk-debug-session.ts (100%)
 rename lldb/tools/lldb-dap/extension/src/android/{ => core}/connection.ts (100%)
 rename lldb/tools/lldb-dap/extension/src/android/{ => core}/env.ts (100%)
 rename lldb/tools/lldb-dap/extension/src/android/{ => core}/jdwp.ts (100%)
 rename lldb/tools/lldb-dap/extension/src/android/{ => core}/ndk.ts (100%)

diff --git a/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts b/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
index 8b2fda6808bcc..7d87e8cdbcb9a 100644
--- a/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
@@ -1,8 +1,8 @@
 import { ErrorWithNotification } from "../ui/error-with-notification";
 import { ConfigureButton, OpenSettingsButton } from "../ui/show-error-message";
-import { AdbClient } from "./adb-client";
-import { ApkDebugSession } from "./apk-debug-session";
-import { Ndk } from "./ndk";
+import { AdbClient } from "./core/adb-client";
+import { ApkDebugSession } from "./core/apk-debug-session";
+import { Ndk } from "./core/ndk";
 
 export class AndroidConfigurationBuilder {
 
diff --git a/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts b/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
index 51a3aaeeffa23..af0539506cafe 100644
--- a/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
@@ -1,5 +1,5 @@
 import * as vscode from "vscode";
-import { ApkDebugEnv, ApkDebugSession } from "./apk-debug-session";
+import { ApkDebugEnv, ApkDebugSession } from "./core/apk-debug-session";
 
 /**
  * This class is for tracking the Android APK debug session associated with the
diff --git a/lldb/tools/lldb-dap/extension/src/android/adb-client.ts b/lldb/tools/lldb-dap/extension/src/android/core/adb-client.ts
similarity index 100%
rename from lldb/tools/lldb-dap/extension/src/android/adb-client.ts
rename to lldb/tools/lldb-dap/extension/src/android/core/adb-client.ts
diff --git a/lldb/tools/lldb-dap/extension/src/android/adb-connection.ts b/lldb/tools/lldb-dap/extension/src/android/core/adb-connection.ts
similarity index 100%
rename from lldb/tools/lldb-dap/extension/src/android/adb-connection.ts
rename to lldb/tools/lldb-dap/extension/src/android/core/adb-connection.ts
diff --git a/lldb/tools/lldb-dap/extension/src/android/apk-debug-session.ts b/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
similarity index 100%
rename from lldb/tools/lldb-dap/extension/src/android/apk-debug-session.ts
rename to lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
diff --git a/lldb/tools/lldb-dap/extension/src/android/connection.ts b/lldb/tools/lldb-dap/extension/src/android/core/connection.ts
similarity index 100%
rename from lldb/tools/lldb-dap/extension/src/android/connection.ts
rename to lldb/tools/lldb-dap/extension/src/android/core/connection.ts
diff --git a/lldb/tools/lldb-dap/extension/src/android/env.ts b/lldb/tools/lldb-dap/extension/src/android/core/env.ts
similarity index 100%
rename from lldb/tools/lldb-dap/extension/src/android/env.ts
rename to lldb/tools/lldb-dap/extension/src/android/core/env.ts
diff --git a/lldb/tools/lldb-dap/extension/src/android/jdwp.ts b/lldb/tools/lldb-dap/extension/src/android/core/jdwp.ts
similarity index 100%
rename from lldb/tools/lldb-dap/extension/src/android/jdwp.ts
rename to lldb/tools/lldb-dap/extension/src/android/core/jdwp.ts
diff --git a/lldb/tools/lldb-dap/extension/src/android/ndk.ts b/lldb/tools/lldb-dap/extension/src/android/core/ndk.ts
similarity index 100%
rename from lldb/tools/lldb-dap/extension/src/android/ndk.ts
rename to lldb/tools/lldb-dap/extension/src/android/core/ndk.ts

>From 257723be1aa1e66f522c419e018e84247e7b0fbd Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Thu, 8 Jan 2026 14:46:01 +0100
Subject: [PATCH 12/18] [lldb-dap] apply project code formatting

---
 .../android/android-configuration-builder.ts  | 167 +++---
 .../src/android/android-session-tracker.ts    |  66 +--
 .../extension/src/android/core/adb-client.ts  | 402 +++++++-------
 .../src/android/core/adb-connection.ts        | 490 ++++++++++--------
 .../src/android/core/apk-debug-session.ts     | 428 ++++++++-------
 .../extension/src/android/core/connection.ts  | 405 ++++++++-------
 .../extension/src/android/core/env.ts         |  22 +-
 .../extension/src/android/core/jdwp.ts        | 265 +++++-----
 .../extension/src/android/core/ndk.ts         | 106 ++--
 .../extension/src/debug-adapter-factory.ts    |   7 +-
 .../src/debug-configuration-provider.ts       |  34 +-
 .../extension/src/debug-session-tracker.ts    |  20 +-
 12 files changed, 1273 insertions(+), 1139 deletions(-)

diff --git a/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts b/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
index 7d87e8cdbcb9a..d25ae86884f91 100644
--- a/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
@@ -5,94 +5,103 @@ import { ApkDebugSession } from "./core/apk-debug-session";
 import { Ndk } from "./core/ndk";
 
 export class AndroidConfigurationBuilder {
-
-    static async getDefaultNdkPath(): Promise<string> {
-        const path = await Ndk.getDefaultPath();
-        if (!path) {
-            throw new ErrorWithNotification(
-                `Unable to find the Android NDK. Please install it in its default location or define its path in the settings.`,
-                new OpenSettingsButton("lldb-dap.androidNDKPath"),
-            );
-        }
-        return path;
+  static async getDefaultNdkPath(): Promise<string> {
+    const path = await Ndk.getDefaultPath();
+    if (!path) {
+      throw new ErrorWithNotification(
+        `Unable to find the Android NDK. Please install it in its default location or define its path in the settings.`,
+        new OpenSettingsButton("lldb-dap.androidNDKPath"),
+      );
     }
+    return path;
+  }
 
-    static async checkNdkAndRetrieveVersion(ndkPath: string): Promise<string> {
-        const version = await Ndk.getVersion(ndkPath);
-        if (!version) {
-            throw new ErrorWithNotification(
-                `Invalid Android NDK path "${ndkPath}". Please ensure the NDK is installed and the path is properly defined in the settings.`,
-                new OpenSettingsButton("lldb-dap.androidNDKPath"),
-            );
-        }
-        return version;
+  static async checkNdkAndRetrieveVersion(ndkPath: string): Promise<string> {
+    const version = await Ndk.getVersion(ndkPath);
+    if (!version) {
+      throw new ErrorWithNotification(
+        `Invalid Android NDK path "${ndkPath}". Please ensure the NDK is installed and the path is properly defined in the settings.`,
+        new OpenSettingsButton("lldb-dap.androidNDKPath"),
+      );
     }
+    return version;
+  }
 
-    static async resolveDeviceSerial(device?: string): Promise<string> {
-        const adbClient = new AdbClient();
-        let deviceSerials: string[];
-        try {
-            deviceSerials = await adbClient.getDeviceList();
-        } catch (e) {
-            throw new ErrorWithNotification(
-                `Could not connect to ADB server. Please make sure the ADB server is running and at least one device or emulator is connected.`,
-            );
-        }
-        if (!device) {
-            if (deviceSerials.length === 1) {
-                return deviceSerials[0];
-            }
-            if (deviceSerials.length > 1) {
-                throw new ErrorWithNotification(
-                    `Multiple connected Android devices found, please specify a device name or serial number in your launch configuration, property "androidDevice".`,
-                    new ConfigureButton(),
-                );
-            }
-            throw new ErrorWithNotification(
-                `No Android devices found. Please verify that at least one device or emulator is connected to the ADB server.`,
-            );
-        }
-        for (const deviceSerial of deviceSerials) {
-            if (deviceSerial === device) {
-                return deviceSerial;
-            }
-        }
-        for (const deviceSerial of deviceSerials) {
-            const adbClient = new AdbClient();
-            adbClient.setDeviceSerial(deviceSerial);
-            const name = await adbClient.getDeviceName();
-            if (name === device) {
-                return deviceSerial;
-            }
-        }
+  static async resolveDeviceSerial(device?: string): Promise<string> {
+    const adbClient = new AdbClient();
+    let deviceSerials: string[];
+    try {
+      deviceSerials = await adbClient.getDeviceList();
+    } catch (e) {
+      throw new ErrorWithNotification(
+        `Could not connect to ADB server. Please make sure the ADB server is running and at least one device or emulator is connected.`,
+      );
+    }
+    if (!device) {
+      if (deviceSerials.length === 1) {
+        return deviceSerials[0];
+      }
+      if (deviceSerials.length > 1) {
         throw new ErrorWithNotification(
-            `Android devices "${device}" not found. Please connect this device or emulator to the ADB server.`,
+          `Multiple connected Android devices found, please specify a device name or serial number in your launch configuration, property "androidDevice".`,
+          new ConfigureButton(),
         );
+      }
+      throw new ErrorWithNotification(
+        `No Android devices found. Please verify that at least one device or emulator is connected to the ADB server.`,
+      );
     }
-
-    /**
-     * Returned arch can be: aarch64, riscv64, arm, x86_64, i386
-     */
-    static async getTargetArch(deviceSerial: string): Promise<string> {
-        const adbClient = new AdbClient();
-        adbClient.setDeviceSerial(deviceSerial);
-        const arch = await adbClient.shellCommandToString("uname -m");
-        return arch.trim();
+    for (const deviceSerial of deviceSerials) {
+      if (deviceSerial === device) {
+        return deviceSerial;
+      }
     }
-
-    static async getLldbServerPath(ndkPath: string, targetArch: string): Promise<string | undefined> {
-        const path = await Ndk.getLldbServerPath(ndkPath, targetArch);
-        if (!path) {
-            throw new ErrorWithNotification(
-                `Could not find lldb-server in the NDK at path "${ndkPath}" for target architecture "${targetArch}". Please verify that the NDK path is correct and that the NDK includes support for this architecture.`,
-            );
-        }
-        return path;
+    for (const deviceSerial of deviceSerials) {
+      const adbClient = new AdbClient();
+      adbClient.setDeviceSerial(deviceSerial);
+      const name = await adbClient.getDeviceName();
+      if (name === device) {
+        return deviceSerial;
+      }
     }
+    throw new ErrorWithNotification(
+      `Android devices "${device}" not found. Please connect this device or emulator to the ADB server.`,
+    );
+  }
+
+  /**
+   * Returned arch can be: aarch64, riscv64, arm, x86_64, i386
+   */
+  static async getTargetArch(deviceSerial: string): Promise<string> {
+    const adbClient = new AdbClient();
+    adbClient.setDeviceSerial(deviceSerial);
+    const arch = await adbClient.shellCommandToString("uname -m");
+    return arch.trim();
+  }
 
-    static getLldbLaunchCommands(deviceSerial: string | undefined, componentName: string): string[] {
-        // We create a temporary ApkDebugSession instance just to get the launch commands.
-        const apkDebugSession = new ApkDebugSession(undefined, deviceSerial, componentName);
-        return apkDebugSession.getLldbLaunchCommands();
+  static async getLldbServerPath(
+    ndkPath: string,
+    targetArch: string,
+  ): Promise<string | undefined> {
+    const path = await Ndk.getLldbServerPath(ndkPath, targetArch);
+    if (!path) {
+      throw new ErrorWithNotification(
+        `Could not find lldb-server in the NDK at path "${ndkPath}" for target architecture "${targetArch}". Please verify that the NDK path is correct and that the NDK includes support for this architecture.`,
+      );
     }
+    return path;
+  }
+
+  static getLldbLaunchCommands(
+    deviceSerial: string | undefined,
+    componentName: string,
+  ): string[] {
+    // We create a temporary ApkDebugSession instance just to get the launch commands.
+    const apkDebugSession = new ApkDebugSession(
+      undefined,
+      deviceSerial,
+      componentName,
+    );
+    return apkDebugSession.getLldbLaunchCommands();
+  }
 }
diff --git a/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts b/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
index af0539506cafe..20805eea403c5 100644
--- a/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
@@ -1,5 +1,5 @@
 import * as vscode from "vscode";
-import { ApkDebugEnv, ApkDebugSession } from "./core/apk-debug-session";
+import { ApkDebugSession } from "./core/apk-debug-session";
 
 /**
  * This class is for tracking the Android APK debug session associated with the
@@ -11,32 +11,40 @@ import { ApkDebugEnv, ApkDebugSession } from "./core/apk-debug-session";
  * the APK is installed, and the ADB daemon is running.
  */
 export class AndroidSessionTracker {
-
-    private static catalog = new WeakMap<vscode.DebugSession, AndroidSessionTracker>();
-
-    static getFromSession(session: vscode.DebugSession): AndroidSessionTracker | undefined {
-        return AndroidSessionTracker.catalog.get(session);
-    }
-
-    private apkDebugSession: ApkDebugSession;
-
-    constructor(session: vscode.DebugSession) {
-        const env = { lldbServerPath: session.configuration.androidLldbServerPath };
-        const deviceSerial = session.configuration.androidDeviceSerial;
-        const componentName = session.configuration.androidComponent;
-        this.apkDebugSession = new ApkDebugSession(env, deviceSerial, componentName);
-        AndroidSessionTracker.catalog.set(session, this);
-    }
-
-    async startDebugSession() {
-        await this.apkDebugSession.start(true);
-    }
-
-    async stopDebugSession() {
-        await this.apkDebugSession.stop();
-    }
-
-    async dismissWaitingForDebuggerDialog() {
-        await this.apkDebugSession.dismissWaitingForDebuggerDialog();
-    }
+  private static catalog = new WeakMap<
+    vscode.DebugSession,
+    AndroidSessionTracker
+  >();
+
+  static getFromSession(
+    session: vscode.DebugSession,
+  ): AndroidSessionTracker | undefined {
+    return AndroidSessionTracker.catalog.get(session);
+  }
+
+  private apkDebugSession: ApkDebugSession;
+
+  constructor(session: vscode.DebugSession) {
+    const env = { lldbServerPath: session.configuration.androidLldbServerPath };
+    const deviceSerial = session.configuration.androidDeviceSerial;
+    const componentName = session.configuration.androidComponent;
+    this.apkDebugSession = new ApkDebugSession(
+      env,
+      deviceSerial,
+      componentName,
+    );
+    AndroidSessionTracker.catalog.set(session, this);
+  }
+
+  async startDebugSession() {
+    await this.apkDebugSession.start(true);
+  }
+
+  async stopDebugSession() {
+    await this.apkDebugSession.stop();
+  }
+
+  async dismissWaitingForDebuggerDialog() {
+    await this.apkDebugSession.dismissWaitingForDebuggerDialog();
+  }
 }
diff --git a/lldb/tools/lldb-dap/extension/src/android/core/adb-client.ts b/lldb/tools/lldb-dap/extension/src/android/core/adb-client.ts
index 723e4adac779b..b65656c0aec06 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/adb-client.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/adb-client.ts
@@ -1,7 +1,7 @@
-import * as process from 'node:process';
-import { AdbConnection } from './adb-connection';
-import { Connection } from './connection';
-import Jdwp from './jdwp';
+import * as process from "node:process";
+import { AdbConnection } from "./adb-connection";
+import { Connection } from "./connection";
+import Jdwp from "./jdwp";
 
 /**
  * ADB (Android Debug Bridge) client.
@@ -12,193 +12,209 @@ import Jdwp from './jdwp';
  * This client expects the ADB daemon to be running on the local machine.
  */
 export class AdbClient {
-
-    private deviceSerial: string | undefined = undefined;
-
-    async getDeviceList(): Promise<string[]> {
-        const connection = await this.createAdbConnection();
-        try {
-            const devices = await connection.getDeviceList();
-            return devices;
-        } finally {
-            connection.close();
-        }
-    }
-
-    async autoDetectDeviceSerial(): Promise<void> {
-        const connection = await this.createAdbConnection();
-        try {
-            const devices = await connection.getDeviceList();
-            if (devices.length === 1) {
-                this.deviceSerial = devices[0];
-                return;
-            }
-            if (devices.length === 0) {
-                throw new Error('No connected Android devices found');
-            }
-            throw new Error('Multiple connected Android devices found, please specify a device SN');
-        } finally {
-            connection.close();
-        }
-    }
-
-    setDeviceSerial(deviceSerial: string) {
-        this.deviceSerial = deviceSerial;
-    }
-
-    getDeviceSerial(): string {
-        if (this.deviceSerial === undefined) {
-            throw new Error('Device SN is not set');
-        }
-        return this.deviceSerial;
-    }
-
-    async getDeviceName(): Promise<string> {
-        let emulatorName = await this.shellCommandToString("getprop ro.boot.qemu.avd_name");
-        emulatorName = emulatorName.trim();
-        if (emulatorName)
-            return emulatorName;
-        let deviceName = await this.shellCommandToString("settings get global device_name");
-        deviceName = deviceName.trim();
-        if (deviceName)
-            return deviceName;
-        return this.getDeviceSerial();
-    }
-
-    async shellCommand(command: string): Promise<void> {
-        const deviceSerial = this.getDeviceSerial();
-        const connection = await this.createAdbConnection();
-        try {
-            await connection.setTargetDevice(deviceSerial);
-            await connection.shellCommand(command);
-        } finally {
-            connection.close();
-        }
-    }
-
-    async shellCommandToString(command: string): Promise<string> {
-        const deviceSerial = this.getDeviceSerial();
-        const connection = await this.createAdbConnection();
-        try {
-            await connection.setTargetDevice(deviceSerial);
-            const output = await connection.shellCommandToString(command);
-            return output;
-        } finally {
-            connection.close();
-        }
-    }
-
-    async shellCommandToStream(command: string, writer: (data: Uint8Array) => Promise<void>, abort: AbortSignal): Promise<void> {
-        const deviceSerial = this.getDeviceSerial();
-        const connection = await this.createAdbConnection();
-        abort.addEventListener('abort', () => {
-            connection.close();
-        });
-        try {
-            await connection.setTargetDevice(deviceSerial);
-            await connection.shellCommandToStream(command, writer);
-        } finally {
-            connection.close();
-        }
-    }
-
-    async getPid(packageName: string): Promise<number> {
-        const deviceSerial = this.getDeviceSerial();
-        const connection = await this.createAdbConnection();
-        try {
-            await connection.setTargetDevice(deviceSerial);
-            const pid = await connection.getPid(packageName);
-            return pid;
-        } finally {
-            connection.close();
-        }
-    }
-
-    async addPortForwarding(remotePort: number | string, localPort: number = 0): Promise<number> {
-        const deviceSerial = this.getDeviceSerial();
-        const connection = await this.createAdbConnection();
-        try {
-            const port = await connection.addPortForwarding(deviceSerial, remotePort, localPort);
-            return port;
-        } finally {
-            connection.close();
-        }
-    }
-
-    async removePortForwarding(localPort: number): Promise<void> {
-        const deviceSerial = this.getDeviceSerial();
-        const connection = await this.createAdbConnection();
-        try {
-            await connection.removePortForwarding(deviceSerial, localPort);
-        } finally {
-            connection.close();
-        }
-    }
-
-    async getPortForwardingList(): Promise<{ device: string, localPort: string, remotePort: string }[]> {
-        const connection = await this.createAdbConnection();
-        try {
-            return await connection.getPortForwardingList();
-        } finally {
-            connection.close();
-        }
-    }
-
-    async dismissWaitingForDebuggerDialog(pid: number): Promise<void> {
-        const port = await this.addPortForwarding(`jdwp:${pid}`);
-        try {
-            const connection = new Connection();
-            await connection.connect('127.0.0.1', port);
-            try {
-                await Jdwp.handshake(connection);
-                // Dalvik is able to reply to handshake and DDM commands (command set 199)
-                // without loading the JDWP agent.
-                // By sending a version command, we force it to load the JDWP agent, which
-                // causes the "waiting for debugger" popup to be dismissed.
-                const version = await Jdwp.getVersion(connection);
-                console.log("JDWP Version:", JSON.stringify(version));
-                // TODO: understand why we need to keep the connection active for a while
-                await new Promise(resolve => setTimeout(resolve, 200));
-            } finally {
-                connection.close();
-            }
-        } finally {
-            await this.removePortForwarding(port);
-        }
-    }
-
-    async pushData(data: Uint8Array, remoteFilePath: string): Promise<void> {
-        const deviceSerial = this.getDeviceSerial();
-        const connection = await this.createAdbConnection();
-        try {
-            await connection.setTargetDevice(deviceSerial);
-            await connection.enterSyncMode();
-            await connection.pushData(data, remoteFilePath);
-        } finally {
-            connection.close();
-        }
-    }
-
-    async pushFile(localFilePath: string, remoteFilePath: string): Promise<void> {
-        const deviceSerial = this.getDeviceSerial();
-        const connection = await this.createAdbConnection();
-        try {
-            await connection.setTargetDevice(deviceSerial);
-            await connection.enterSyncMode();
-            await connection.pushFile(localFilePath, remoteFilePath);
-        } finally {
-            connection.close();
-        }
-    }
-
-    private getAdbPort(): number {
-        const envPort = process.env.ANDROID_ADB_SERVER_PORT;
-        return envPort ? parseInt(envPort, 10) : 5037;
-    }
-
-    private async createAdbConnection(): Promise<AdbConnection> {
-        const connection = new AdbConnection();
-        await connection.connect('127.0.0.1', this.getAdbPort());
-        return connection;
-    }
+  private deviceSerial: string | undefined = undefined;
+
+  async getDeviceList(): Promise<string[]> {
+    const connection = await this.createAdbConnection();
+    try {
+      const devices = await connection.getDeviceList();
+      return devices;
+    } finally {
+      connection.close();
+    }
+  }
+
+  async autoDetectDeviceSerial(): Promise<void> {
+    const connection = await this.createAdbConnection();
+    try {
+      const devices = await connection.getDeviceList();
+      if (devices.length === 1) {
+        this.deviceSerial = devices[0];
+        return;
+      }
+      if (devices.length === 0) {
+        throw new Error("No connected Android devices found");
+      }
+      throw new Error(
+        "Multiple connected Android devices found, please specify a device SN",
+      );
+    } finally {
+      connection.close();
+    }
+  }
+
+  setDeviceSerial(deviceSerial: string) {
+    this.deviceSerial = deviceSerial;
+  }
+
+  getDeviceSerial(): string {
+    if (this.deviceSerial === undefined) {
+      throw new Error("Device SN is not set");
+    }
+    return this.deviceSerial;
+  }
+
+  async getDeviceName(): Promise<string> {
+    let emulatorName = await this.shellCommandToString(
+      "getprop ro.boot.qemu.avd_name",
+    );
+    emulatorName = emulatorName.trim();
+    if (emulatorName) return emulatorName;
+    let deviceName = await this.shellCommandToString(
+      "settings get global device_name",
+    );
+    deviceName = deviceName.trim();
+    if (deviceName) return deviceName;
+    return this.getDeviceSerial();
+  }
+
+  async shellCommand(command: string): Promise<void> {
+    const deviceSerial = this.getDeviceSerial();
+    const connection = await this.createAdbConnection();
+    try {
+      await connection.setTargetDevice(deviceSerial);
+      await connection.shellCommand(command);
+    } finally {
+      connection.close();
+    }
+  }
+
+  async shellCommandToString(command: string): Promise<string> {
+    const deviceSerial = this.getDeviceSerial();
+    const connection = await this.createAdbConnection();
+    try {
+      await connection.setTargetDevice(deviceSerial);
+      const output = await connection.shellCommandToString(command);
+      return output;
+    } finally {
+      connection.close();
+    }
+  }
+
+  async shellCommandToStream(
+    command: string,
+    writer: (data: Uint8Array) => Promise<void>,
+    abort: AbortSignal,
+  ): Promise<void> {
+    const deviceSerial = this.getDeviceSerial();
+    const connection = await this.createAdbConnection();
+    abort.addEventListener("abort", () => {
+      connection.close();
+    });
+    try {
+      await connection.setTargetDevice(deviceSerial);
+      await connection.shellCommandToStream(command, writer);
+    } finally {
+      connection.close();
+    }
+  }
+
+  async getPid(packageName: string): Promise<number> {
+    const deviceSerial = this.getDeviceSerial();
+    const connection = await this.createAdbConnection();
+    try {
+      await connection.setTargetDevice(deviceSerial);
+      const pid = await connection.getPid(packageName);
+      return pid;
+    } finally {
+      connection.close();
+    }
+  }
+
+  async addPortForwarding(
+    remotePort: number | string,
+    localPort: number = 0,
+  ): Promise<number> {
+    const deviceSerial = this.getDeviceSerial();
+    const connection = await this.createAdbConnection();
+    try {
+      const port = await connection.addPortForwarding(
+        deviceSerial,
+        remotePort,
+        localPort,
+      );
+      return port;
+    } finally {
+      connection.close();
+    }
+  }
+
+  async removePortForwarding(localPort: number): Promise<void> {
+    const deviceSerial = this.getDeviceSerial();
+    const connection = await this.createAdbConnection();
+    try {
+      await connection.removePortForwarding(deviceSerial, localPort);
+    } finally {
+      connection.close();
+    }
+  }
+
+  async getPortForwardingList(): Promise<
+    { device: string; localPort: string; remotePort: string }[]
+  > {
+    const connection = await this.createAdbConnection();
+    try {
+      return await connection.getPortForwardingList();
+    } finally {
+      connection.close();
+    }
+  }
+
+  async dismissWaitingForDebuggerDialog(pid: number): Promise<void> {
+    const port = await this.addPortForwarding(`jdwp:${pid}`);
+    try {
+      const connection = new Connection();
+      await connection.connect("127.0.0.1", port);
+      try {
+        await Jdwp.handshake(connection);
+        // Dalvik is able to reply to handshake and DDM commands (command set 199)
+        // without loading the JDWP agent.
+        // By sending a version command, we force it to load the JDWP agent, which
+        // causes the "waiting for debugger" popup to be dismissed.
+        const version = await Jdwp.getVersion(connection);
+        console.log("JDWP Version:", JSON.stringify(version));
+        // TODO: understand why we need to keep the connection active for a while
+        await new Promise((resolve) => setTimeout(resolve, 200));
+      } finally {
+        connection.close();
+      }
+    } finally {
+      await this.removePortForwarding(port);
+    }
+  }
+
+  async pushData(data: Uint8Array, remoteFilePath: string): Promise<void> {
+    const deviceSerial = this.getDeviceSerial();
+    const connection = await this.createAdbConnection();
+    try {
+      await connection.setTargetDevice(deviceSerial);
+      await connection.enterSyncMode();
+      await connection.pushData(data, remoteFilePath);
+    } finally {
+      connection.close();
+    }
+  }
+
+  async pushFile(localFilePath: string, remoteFilePath: string): Promise<void> {
+    const deviceSerial = this.getDeviceSerial();
+    const connection = await this.createAdbConnection();
+    try {
+      await connection.setTargetDevice(deviceSerial);
+      await connection.enterSyncMode();
+      await connection.pushFile(localFilePath, remoteFilePath);
+    } finally {
+      connection.close();
+    }
+  }
+
+  private getAdbPort(): number {
+    const envPort = process.env.ANDROID_ADB_SERVER_PORT;
+    return envPort ? parseInt(envPort, 10) : 5037;
+  }
+
+  private async createAdbConnection(): Promise<AdbConnection> {
+    const connection = new AdbConnection();
+    await connection.connect("127.0.0.1", this.getAdbPort());
+    return connection;
+  }
 }
diff --git a/lldb/tools/lldb-dap/extension/src/android/core/adb-connection.ts b/lldb/tools/lldb-dap/extension/src/android/core/adb-connection.ts
index 1c1b1b64bc18e..5aeb515062e68 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/adb-connection.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/adb-connection.ts
@@ -8,274 +8,304 @@ import { Connection } from "./connection";
  * For a complete ADB client, see AdbClient.
  */
 export class AdbConnection extends Connection {
-
-    async sendAdbMessage(packet: string): Promise<void> {
-        const enc = new TextEncoder();
-        const data = enc.encode(packet);
-        if (data.length > 0xFFFF) {
-            throw new Error('Packet size exceeds maximum allowed length of 65535 bytes');
-        }
-        const head = enc.encode(data.length.toString(16).padStart(4, '0'));
-        await this.write(head);
-        await this.write(data);
+  async sendAdbMessage(packet: string): Promise<void> {
+    const enc = new TextEncoder();
+    const data = enc.encode(packet);
+    if (data.length > 0xffff) {
+      throw new Error(
+        "Packet size exceeds maximum allowed length of 65535 bytes",
+      );
     }
+    const head = enc.encode(data.length.toString(16).padStart(4, "0"));
+    await this.write(head);
+    await this.write(data);
+  }
 
-    async sendDeviceMessage(deviceSerial: string, packet: string): Promise<void> {
-        const msg = `host-serial:${deviceSerial}:${packet}`;
-        await this.sendAdbMessage(msg);
-    }
+  async sendDeviceMessage(deviceSerial: string, packet: string): Promise<void> {
+    const msg = `host-serial:${deviceSerial}:${packet}`;
+    await this.sendAdbMessage(msg);
+  }
 
-    async readAdbMessage(): Promise<Uint8Array> {
-        const head = await this.read(4);
-        if (head.length !== 4) {
-            throw new Error('Incomplete ADB message head received');
-        }
-        const dec = new TextDecoder();
-        const size = parseInt(dec.decode(head), 16);
-        const message = await this.read(size);
-        if (message.length !== size) {
-            throw new Error('Incomplete ADB message received');
-        }
-        return message;
+  async readAdbMessage(): Promise<Uint8Array> {
+    const head = await this.read(4);
+    if (head.length !== 4) {
+      throw new Error("Incomplete ADB message head received");
+    }
+    const dec = new TextDecoder();
+    const size = parseInt(dec.decode(head), 16);
+    const message = await this.read(size);
+    if (message.length !== size) {
+      throw new Error("Incomplete ADB message received");
     }
+    return message;
+  }
 
-    async readResponseStatus(): Promise<void> {
-        const data = await this.read(4);
-        if (data.length !== 4) {
-            throw new Error('Incomplete ADB message head received');
-        }
-        const dec = new TextDecoder();
-        const status = dec.decode(data);
-        switch (status) {
-            case 'OKAY':
-                return;
-            case 'FAIL':
-                const errorMsg = await this.readAdbMessage();
-                throw new AdbCommandFailed(`ADB command failed: ${dec.decode(errorMsg)}`);
-            default:
-                throw new Error(`Unknown ADB response status: ${status}`);
-        }
+  async readResponseStatus(): Promise<void> {
+    const data = await this.read(4);
+    if (data.length !== 4) {
+      throw new Error("Incomplete ADB message head received");
     }
+    const dec = new TextDecoder();
+    const status = dec.decode(data);
+    switch (status) {
+      case "OKAY":
+        return;
+      case "FAIL":
+        const errorMsg = await this.readAdbMessage();
+        throw new AdbCommandFailed(
+          `ADB command failed: ${dec.decode(errorMsg)}`,
+        );
+      default:
+        throw new Error(`Unknown ADB response status: ${status}`);
+    }
+  }
 
-    /**
-     * Return a list of device serial numbers connected to the ADB server.
-     * The ADB server closes the connection after executing this command.
-     */
-    async getDeviceList(): Promise<string[]> {
-        await this.sendAdbMessage('host:devices');
-        await this.readResponseStatus();
-        const data = await this.readAdbMessage();
+  /**
+   * Return a list of device serial numbers connected to the ADB server.
+   * The ADB server closes the connection after executing this command.
+   */
+  async getDeviceList(): Promise<string[]> {
+    await this.sendAdbMessage("host:devices");
+    await this.readResponseStatus();
+    const data = await this.readAdbMessage();
 
-        const dec = new TextDecoder();
-        const response = dec.decode(data);
-        const deviceList: string[] = [];
-        const lines = response.split('\n');
-        for (const line of lines) {
-            const [device] = line.split('\t');
-            if (device) {
-                deviceList.push(device);
-            }
-        }
-        return deviceList;
+    const dec = new TextDecoder();
+    const response = dec.decode(data);
+    const deviceList: string[] = [];
+    const lines = response.split("\n");
+    for (const line of lines) {
+      const [device] = line.split("\t");
+      if (device) {
+        deviceList.push(device);
+      }
     }
+    return deviceList;
+  }
 
-    async setTargetDevice(deviceSerial: string): Promise<void> {
-        await this.sendAdbMessage(`host:transport:${deviceSerial}`);
-        await this.readResponseStatus();
-    }
+  async setTargetDevice(deviceSerial: string): Promise<void> {
+    await this.sendAdbMessage(`host:transport:${deviceSerial}`);
+    await this.readResponseStatus();
+  }
 
-    /**
-     * The ADB server closes the connection after executing this command.
-     */
-    async shellCommandToStream(command: string, writer: (data: Uint8Array) => Promise<void>): Promise<void> {
-        if (command === "") {
-            throw new Error('Shell command cannot be empty');
-        }
-        const message = `shell:${command}`;
-        await this.sendAdbMessage(message);
-        await this.readResponseStatus();
-        for (;;) {
-            const data = await this.read();
-            if (data.length === 0) {
-                break;
-            }
-            await writer(data);
-        }
+  /**
+   * The ADB server closes the connection after executing this command.
+   */
+  async shellCommandToStream(
+    command: string,
+    writer: (data: Uint8Array) => Promise<void>,
+  ): Promise<void> {
+    if (command === "") {
+      throw new Error("Shell command cannot be empty");
     }
-
-    async shellCommandToString(command: string): Promise<string> {
-        const output: Uint8Array[] = [];
-        const writer = async (data: Uint8Array) => {
-            output.push(data);
-        };
-        await this.shellCommandToStream(command, writer);
-        const totalLength = output.reduce((sum, buf) => sum + buf.length, 0);
-        const combined = new Uint8Array(totalLength);
-        let offset = 0;
-        for (const buf of output) {
-            combined.set(buf, offset);
-            offset += buf.length;
-        }
-        const dec = new TextDecoder();
-        return dec.decode(combined);
+    const message = `shell:${command}`;
+    await this.sendAdbMessage(message);
+    await this.readResponseStatus();
+    for (;;) {
+      const data = await this.read();
+      if (data.length === 0) {
+        break;
+      }
+      await writer(data);
     }
+  }
 
-    async shellCommand(command: string): Promise<void> {
-        const writer = async (data: Uint8Array) => {};
-        await this.shellCommandToStream(command, writer);
+  async shellCommandToString(command: string): Promise<string> {
+    const output: Uint8Array[] = [];
+    const writer = async (data: Uint8Array) => {
+      output.push(data);
+    };
+    await this.shellCommandToStream(command, writer);
+    const totalLength = output.reduce((sum, buf) => sum + buf.length, 0);
+    const combined = new Uint8Array(totalLength);
+    let offset = 0;
+    for (const buf of output) {
+      combined.set(buf, offset);
+      offset += buf.length;
     }
+    const dec = new TextDecoder();
+    return dec.decode(combined);
+  }
 
-    /**
-     * The ADB server closes the connection after executing this command.
-     */
-    async getPid(packageName: string): Promise<number> {
-        const output = await this.shellCommandToString(`pidof ${packageName}`);
-        const pid = parseInt(output.trim(), 10);
-        if (isNaN(pid)) {
-            throw new AdbCommandFailed(`Failed to get PID for package: ${packageName}`);
-        }
-        return pid;
-    }
+  async shellCommand(command: string): Promise<void> {
+    const writer = async (data: Uint8Array) => {};
+    await this.shellCommandToStream(command, writer);
+  }
 
-    async addPortForwarding(deviceSerial: string, remotePort: number | string, localPort: number = 0): Promise<number> {
-        if (typeof remotePort === 'number') {
-            remotePort = `tcp:${remotePort}`;
-        }
-        const message = `forward:tcp:${localPort};${remotePort}`;
-        await this.sendDeviceMessage(deviceSerial, message);
-        await this.readResponseStatus();
-        await this.readResponseStatus();
-        const result = await this.readAdbMessage();
-        const dec = new TextDecoder();
-        const port = parseInt(dec.decode(result), 10);
-        if (isNaN(port)) {
-            throw new Error('Failed to add port forwarding');
-        }
-        return port;
+  /**
+   * The ADB server closes the connection after executing this command.
+   */
+  async getPid(packageName: string): Promise<number> {
+    const output = await this.shellCommandToString(`pidof ${packageName}`);
+    const pid = parseInt(output.trim(), 10);
+    if (isNaN(pid)) {
+      throw new AdbCommandFailed(
+        `Failed to get PID for package: ${packageName}`,
+      );
     }
+    return pid;
+  }
 
-    async removePortForwarding(deviceSerial: string, localPort: number): Promise<void> {
-        const message = `killforward:tcp:${localPort}`;
-        await this.sendDeviceMessage(deviceSerial, message);
-        await this.readResponseStatus();
+  async addPortForwarding(
+    deviceSerial: string,
+    remotePort: number | string,
+    localPort: number = 0,
+  ): Promise<number> {
+    if (typeof remotePort === "number") {
+      remotePort = `tcp:${remotePort}`;
     }
-
-    async getPortForwardingList(): Promise<{ device: string, localPort: string, remotePort: string }[]> {
-        const message = `host:list-forward`;
-        await this.sendAdbMessage(message);
-        await this.readResponseStatus();
-        const result = await this.readAdbMessage();
-        const dec = new TextDecoder();
-        const list = dec.decode(result);
-        const ret: { device: string, localPort: string, remotePort: string }[] = [];
-        for (const line of list.split('\n')) {
-            const elems = line.split(' ');
-            if (elems.length === 3) {
-                ret.push({ device: elems[0], localPort: elems[1], remotePort: elems[2] });
-            }
-        }
-        return ret;
+    const message = `forward:tcp:${localPort};${remotePort}`;
+    await this.sendDeviceMessage(deviceSerial, message);
+    await this.readResponseStatus();
+    await this.readResponseStatus();
+    const result = await this.readAdbMessage();
+    const dec = new TextDecoder();
+    const port = parseInt(dec.decode(result), 10);
+    if (isNaN(port)) {
+      throw new Error("Failed to add port forwarding");
     }
+    return port;
+  }
 
-    async enterSyncMode(): Promise<void> {
-        await this.sendAdbMessage('sync:');
-        await this.readResponseStatus();
-    }
+  async removePortForwarding(
+    deviceSerial: string,
+    localPort: number,
+  ): Promise<void> {
+    const message = `killforward:tcp:${localPort}`;
+    await this.sendDeviceMessage(deviceSerial, message);
+    await this.readResponseStatus();
+  }
 
-    async writeSyncHeader(requestId: string, dataLen: number): Promise<void> {
-        const enc = new TextEncoder();
-        const requestIdData = enc.encode(requestId);
-        if (requestIdData.length !== 4) {
-            throw new Error('Sync request ID must be 4 characters long');
-        }
-        const buf = new Uint8Array(8);
-        const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
-        buf.set(requestIdData, 0);
-        view.setUint32(4, dataLen, true);
-        await this.write(buf);
+  async getPortForwardingList(): Promise<
+    { device: string; localPort: string; remotePort: string }[]
+  > {
+    const message = `host:list-forward`;
+    await this.sendAdbMessage(message);
+    await this.readResponseStatus();
+    const result = await this.readAdbMessage();
+    const dec = new TextDecoder();
+    const list = dec.decode(result);
+    const ret: { device: string; localPort: string; remotePort: string }[] = [];
+    for (const line of list.split("\n")) {
+      const elems = line.split(" ");
+      if (elems.length === 3) {
+        ret.push({
+          device: elems[0],
+          localPort: elems[1],
+          remotePort: elems[2],
+        });
+      }
     }
+    return ret;
+  }
 
-    async writeSyncData(requestId: string, data: Uint8Array): Promise<void> {
-        await this.writeSyncHeader(requestId, data.length);
-        await this.write(data);
-    }
+  async enterSyncMode(): Promise<void> {
+    await this.sendAdbMessage("sync:");
+    await this.readResponseStatus();
+  }
 
-    async readSyncHeader(): Promise<{ responseId: string; dataLen: number }> {
-        const header = await this.read(8);
-        if (header.length !== 8) {
-            throw new Error('Incomplete sync header received');
-        }
-        const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
-        const dec = new TextDecoder();
-        const responseId = dec.decode(header.subarray(0, 4));
-        const dataLen = view.getUint32(4, true);
-        return { responseId, dataLen };
+  async writeSyncHeader(requestId: string, dataLen: number): Promise<void> {
+    const enc = new TextEncoder();
+    const requestIdData = enc.encode(requestId);
+    if (requestIdData.length !== 4) {
+      throw new Error("Sync request ID must be 4 characters long");
     }
+    const buf = new Uint8Array(8);
+    const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
+    buf.set(requestIdData, 0);
+    view.setUint32(4, dataLen, true);
+    await this.write(buf);
+  }
 
-    async pushStream(reader: (maxSize: number) => Promise<Uint8Array>, remoteFilePath: string): Promise<void> {
-        const defaultMode = 0o100770;
-        const maxChunkSize = 2 * 1024;
+  async writeSyncData(requestId: string, data: Uint8Array): Promise<void> {
+    await this.writeSyncHeader(requestId, data.length);
+    await this.write(data);
+  }
 
-        if (remoteFilePath.indexOf(',') !== -1) {
-            throw new Error('Remote file path cannot contain commas');
-        }
+  async readSyncHeader(): Promise<{ responseId: string; dataLen: number }> {
+    const header = await this.read(8);
+    if (header.length !== 8) {
+      throw new Error("Incomplete sync header received");
+    }
+    const view = new DataView(
+      header.buffer,
+      header.byteOffset,
+      header.byteLength,
+    );
+    const dec = new TextDecoder();
+    const responseId = dec.decode(header.subarray(0, 4));
+    const dataLen = view.getUint32(4, true);
+    return { responseId, dataLen };
+  }
 
-        const enc = new TextEncoder();
-        const body = `${remoteFilePath},0${defaultMode.toString(8)}`;
-        await this.writeSyncData('SEND', enc.encode(body));
+  async pushStream(
+    reader: (maxSize: number) => Promise<Uint8Array>,
+    remoteFilePath: string,
+  ): Promise<void> {
+    const defaultMode = 0o100770;
+    const maxChunkSize = 2 * 1024;
 
-        for (;;) {
-            const chunk = await reader(maxChunkSize);
-            if (chunk.length === 0) {
-                break;
-            }
-            await this.writeSyncData('DATA', chunk);
-        }
+    if (remoteFilePath.indexOf(",") !== -1) {
+      throw new Error("Remote file path cannot contain commas");
+    }
 
-        const mtime = Math.floor(Date.now() / 1000);
-        await this.writeSyncHeader('DONE', mtime); // TODO: year 2038 bug
+    const enc = new TextEncoder();
+    const body = `${remoteFilePath},0${defaultMode.toString(8)}`;
+    await this.writeSyncData("SEND", enc.encode(body));
 
-        const { responseId, dataLen } = await this.readSyncHeader();
-        if (responseId === 'FAIL') {
-            const errorMessageData = await this.read(dataLen);
-            const dec = new TextDecoder();
-            const errorMessage = dec.decode(errorMessageData);
-            throw new AdbCommandFailed(`Failed to push file "${remoteFilePath}": ${errorMessage}`);
-        } else if (responseId !== 'OKAY') {
-            throw new Error(`Unexpected sync response: ${responseId}`);
-        }
-        if (dataLen !== 0) {
-            throw new Error('Unexpected data in sync OKAY response');
-        }
+    for (;;) {
+      const chunk = await reader(maxChunkSize);
+      if (chunk.length === 0) {
+        break;
+      }
+      await this.writeSyncData("DATA", chunk);
     }
 
-    async pushData(data: Uint8Array, remoteFilePath: string): Promise<void> {
-        let offset = 0;
-        const reader = async (maxSize: number): Promise<Uint8Array> => {
-            const chunk = data.subarray(offset, offset + maxSize);
-            offset += chunk.length;
-            return chunk;
-        };
-        await this.pushStream(reader, remoteFilePath);
-    }
+    const mtime = Math.floor(Date.now() / 1000);
+    await this.writeSyncHeader("DONE", mtime); // TODO: year 2038 bug
 
-    async pushFile(localFilePath: string, remoteFilePath: string): Promise<void> {
-        const handle = await fs.open(localFilePath, "r");
-        let buf = new Uint8Array(0);
-        const reader = async (maxSize: number): Promise<Uint8Array> => {
-            if (buf.length < maxSize) {
-                buf = new Uint8Array(maxSize);
-            }
-            const { bytesRead } = await handle.read(buf, 0, maxSize, null);
-            return buf.subarray(0, bytesRead);
-        };
-        await this.pushStream(reader, remoteFilePath);
+    const { responseId, dataLen } = await this.readSyncHeader();
+    if (responseId === "FAIL") {
+      const errorMessageData = await this.read(dataLen);
+      const dec = new TextDecoder();
+      const errorMessage = dec.decode(errorMessageData);
+      throw new AdbCommandFailed(
+        `Failed to push file "${remoteFilePath}": ${errorMessage}`,
+      );
+    } else if (responseId !== "OKAY") {
+      throw new Error(`Unexpected sync response: ${responseId}`);
     }
+    if (dataLen !== 0) {
+      throw new Error("Unexpected data in sync OKAY response");
+    }
+  }
+
+  async pushData(data: Uint8Array, remoteFilePath: string): Promise<void> {
+    let offset = 0;
+    const reader = async (maxSize: number): Promise<Uint8Array> => {
+      const chunk = data.subarray(offset, offset + maxSize);
+      offset += chunk.length;
+      return chunk;
+    };
+    await this.pushStream(reader, remoteFilePath);
+  }
+
+  async pushFile(localFilePath: string, remoteFilePath: string): Promise<void> {
+    const handle = await fs.open(localFilePath, "r");
+    let buf = new Uint8Array(0);
+    const reader = async (maxSize: number): Promise<Uint8Array> => {
+      if (buf.length < maxSize) {
+        buf = new Uint8Array(maxSize);
+      }
+      const { bytesRead } = await handle.read(buf, 0, maxSize, null);
+      return buf.subarray(0, bytesRead);
+    };
+    await this.pushStream(reader, remoteFilePath);
+  }
 }
 
 export class AdbCommandFailed extends Error {
-    constructor(message: string) {
-        super(message);
-        this.name = 'AdbCommandFailed';
-    }
+  constructor(message: string) {
+    super(message);
+    this.name = "AdbCommandFailed";
+  }
 }
diff --git a/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts b/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
index 7b648f43f0a36..92c1613d84f19 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
@@ -9,228 +9,262 @@ import { Ndk } from "./ndk";
  * APK is installed, and the ADB daemon is running.
  */
 export class ApkDebugSession {
+  private runningSession: SessionInfo | undefined;
+  readonly env: ApkDebugEnv | undefined;
+  readonly deviceSerial: string | undefined;
+  readonly componentName: string;
 
-    private runningSession: SessionInfo | undefined;
-    readonly env: ApkDebugEnv | undefined;
-    readonly deviceSerial: string | undefined;
-    readonly componentName: string;
-
-    /**
-     * Component name is in the form "com.example.app/.MainActivity".
-     * If the env is undefined, a default one will be created when starting.
-     * If the deviceSerial is undefined, we expect that only one device is connected.
-     */
-    constructor(env: ApkDebugEnv | undefined, deviceSerial: string | undefined, componentName: string) {
-        this.env = env;
-        this.deviceSerial = deviceSerial;
-        this.componentName = componentName;
-    }
+  /**
+   * Component name is in the form "com.example.app/.MainActivity".
+   * If the env is undefined, a default one will be created when starting.
+   * If the deviceSerial is undefined, we expect that only one device is connected.
+   */
+  constructor(
+    env: ApkDebugEnv | undefined,
+    deviceSerial: string | undefined,
+    componentName: string,
+  ) {
+    this.env = env;
+    this.deviceSerial = deviceSerial;
+    this.componentName = componentName;
+  }
 
-    getLldbLaunchCommands(): string[] {
-        let deviceSerial = this.deviceSerial;
-        if (deviceSerial === undefined) {
-            deviceSerial = "";
-        }
-        const appId = this.componentName.split("/")[0];
-        return [
-            `platform select remote-android`,
-            `platform connect unix-abstract-connect://${deviceSerial}/${appId}/lldb-platform.sock`,
-            `process attach --name ${appId}`,
-            `process handle SIGSEGV -n false -p true -s false`,
-            `process handle SIGBUS -n false -p true -s false`,
-            `process handle SIGCHLD -n false -p true -s false`,
-        ];
+  getLldbLaunchCommands(): string[] {
+    let deviceSerial = this.deviceSerial;
+    if (deviceSerial === undefined) {
+      deviceSerial = "";
     }
+    const appId = this.componentName.split("/")[0];
+    return [
+      `platform select remote-android`,
+      `platform connect unix-abstract-connect://${deviceSerial}/${appId}/lldb-platform.sock`,
+      `process attach --name ${appId}`,
+      `process handle SIGSEGV -n false -p true -s false`,
+      `process handle SIGBUS -n false -p true -s false`,
+      `process handle SIGCHLD -n false -p true -s false`,
+    ];
+  }
 
-    /**
-     * Start the debug session.
-     * `wfd` stays for "waiting for debugger".
-     */
-    async start(wfd: boolean) {
-        const addId = this.componentName.split('/')[0];
-        const adb = new AdbClient();
-        if (this.deviceSerial !== undefined) {
-            adb.setDeviceSerial(this.deviceSerial);
-        } else {
-            await adb.autoDetectDeviceSerial();
-        }
-        let env = this.env;
-        if (!env) {
-            const targetArch = (await adb.shellCommandToString("uname -m")).trim();
-            env = await this.createDefaultEnv(targetArch);
-        }
-        await this.stop();
-        await this.cleanUpEarlierDebugSessions(adb, addId);
-        await this.installLldbServer(adb, addId, env.lldbServerPath);
-        await this.startApk(adb, this.componentName, wfd);
-
-        const abortController = new AbortController();
-        const endPromise = this.startLldbServer(adb, addId, abortController.signal);
-        this.runningSession = {
-            adb,
-            addId,
-            endPromise,
-            abortController,
-        };
-
-        await this.waitLldbServerReachable(adb, addId);
+  /**
+   * Start the debug session.
+   * `wfd` stays for "waiting for debugger".
+   */
+  async start(wfd: boolean) {
+    const addId = this.componentName.split("/")[0];
+    const adb = new AdbClient();
+    if (this.deviceSerial !== undefined) {
+      adb.setDeviceSerial(this.deviceSerial);
+    } else {
+      await adb.autoDetectDeviceSerial();
     }
-
-    async stop() {
-        if (this.runningSession !== undefined) {
-            try {
-                await this.runningSession.adb.shellCommand(`am force-stop ${this.runningSession.addId}`);
-            } catch {}
-            this.runningSession.abortController.abort();
-            try {
-                await this.runningSession.endPromise;
-            } catch {}
-            this.runningSession = undefined;
-        }
+    let env = this.env;
+    if (!env) {
+      const targetArch = (await adb.shellCommandToString("uname -m")).trim();
+      env = await this.createDefaultEnv(targetArch);
     }
+    await this.stop();
+    await this.cleanUpEarlierDebugSessions(adb, addId);
+    await this.installLldbServer(adb, addId, env.lldbServerPath);
+    await this.startApk(adb, this.componentName, wfd);
 
-    async dismissWaitingForDebuggerDialog() {
-        if (this.runningSession !== undefined) {
-            const pid = await this.runningSession.adb.getPid(this.runningSession.addId);
-            await this.runningSession.adb.dismissWaitingForDebuggerDialog(pid);
-        }
-    }
+    const abortController = new AbortController();
+    const endPromise = this.startLldbServer(adb, addId, abortController.signal);
+    this.runningSession = {
+      adb,
+      addId,
+      endPromise,
+      abortController,
+    };
+
+    await this.waitLldbServerReachable(adb, addId);
+  }
 
-    /**
-     * This function tries to clean up anything left behind by earlier debug
-     * sessions.
-     * The current implementation is a bit aggressive, and could impact debug
-     * sessions running in parallel on other apps.
-     * However, the current debugging solution is not super stable and a deep
-     * clean-up phase is really needed. At the same time, cases where two debug
-     * sessions run in parallel are rare.
-     */
-    private async cleanUpEarlierDebugSessions(adb: AdbClient, addId: string) {
-        const deviceSerial = adb.getDeviceSerial();
-
-        // stop the app
-        await adb.shellCommand(`am force-stop ${addId}`);
-
-        // kill existing gdbserver processes
-        await adb.shellCommand(`run-as ${addId} killall lldb-server`);
-
-        // clean up port forwarding
-        await this.cleanUpPortForwarding(adb, addId);
-
-        // clean up unix-domain socket files in the app data folder
-        await adb.shellCommand(`run-as ${addId} find /data/data/${addId} -name 'gdbserver.*' -exec rm {} \\;`);
+  async stop() {
+    if (this.runningSession !== undefined) {
+      try {
+        await this.runningSession.adb.shellCommand(
+          `am force-stop ${this.runningSession.addId}`,
+        );
+      } catch {}
+      this.runningSession.abortController.abort();
+      try {
+        await this.runningSession.endPromise;
+      } catch {}
+      this.runningSession = undefined;
     }
+  }
 
-    private async cleanUpPortForwarding(adb: AdbClient, addId: string) {
-        const deviceSerial = adb.getDeviceSerial();
-        const list = await adb.getPortForwardingList();
-        const filteredList = list.filter(item => {
-            if (item.device !== deviceSerial) {
-                return false;
-            }
-            const pattern1 = `localabstract:/${addId}`;
-            const regex1 = new RegExp(pattern1);
-            if (regex1.test(item.remotePort)) {
-                return true;
-            }
-            const pattern2 = `localabstract:gdbserver.`;
-            const regex2 = new RegExp(pattern2);
-            if (regex2.test(item.remotePort)) {
-                return true;
-            }
-            return false;
-        });
-        for (const item of filteredList) {
-            const localPort = parseInt(item.localPort.replace(/^tcp:/, ""), 10);
-            console.log(`Removing port forwarding for local port ${localPort} (remote: ${item.remotePort})`);
-            await adb.removePortForwarding(localPort);
-        }
+  async dismissWaitingForDebuggerDialog() {
+    if (this.runningSession !== undefined) {
+      const pid = await this.runningSession.adb.getPid(
+        this.runningSession.addId,
+      );
+      await this.runningSession.adb.dismissWaitingForDebuggerDialog(pid);
     }
+  }
+
+  /**
+   * This function tries to clean up anything left behind by earlier debug
+   * sessions.
+   * The current implementation is a bit aggressive, and could impact debug
+   * sessions running in parallel on other apps.
+   * However, the current debugging solution is not super stable and a deep
+   * clean-up phase is really needed. At the same time, cases where two debug
+   * sessions run in parallel are rare.
+   */
+  private async cleanUpEarlierDebugSessions(adb: AdbClient, addId: string) {
+    const deviceSerial = adb.getDeviceSerial();
+
+    // stop the app
+    await adb.shellCommand(`am force-stop ${addId}`);
+
+    // kill existing gdbserver processes
+    await adb.shellCommand(`run-as ${addId} killall lldb-server`);
 
-    private async installLldbServer(adb: AdbClient, addId: string, lldbServerPath: string) {
-        await adb.shellCommand(`mkdir -p /data/local/tmp/lldb-stuff`);
-        await adb.pushFile(lldbServerPath, `/data/local/tmp/lldb-stuff/lldb-server`);
-        await adb.shellCommand(`run-as ${addId} mkdir -p /data/data/${addId}/lldb-stuff`);
-        await adb.shellCommand(`cat /data/local/tmp/lldb-stuff/lldb-server | run-as ${addId} sh -c 'cat > /data/data/${addId}/lldb-stuff/lldb-server'`);
-        await adb.shellCommand(`run-as ${addId} chmod 700 /data/data/${addId}/lldb-stuff/lldb-server`);
+    // clean up port forwarding
+    await this.cleanUpPortForwarding(adb, addId);
+
+    // clean up unix-domain socket files in the app data folder
+    await adb.shellCommand(
+      `run-as ${addId} find /data/data/${addId} -name 'gdbserver.*' -exec rm {} \\;`,
+    );
+  }
+
+  private async cleanUpPortForwarding(adb: AdbClient, addId: string) {
+    const deviceSerial = adb.getDeviceSerial();
+    const list = await adb.getPortForwardingList();
+    const filteredList = list.filter((item) => {
+      if (item.device !== deviceSerial) {
+        return false;
+      }
+      const pattern1 = `localabstract:/${addId}`;
+      const regex1 = new RegExp(pattern1);
+      if (regex1.test(item.remotePort)) {
+        return true;
+      }
+      const pattern2 = `localabstract:gdbserver.`;
+      const regex2 = new RegExp(pattern2);
+      if (regex2.test(item.remotePort)) {
+        return true;
+      }
+      return false;
+    });
+    for (const item of filteredList) {
+      const localPort = parseInt(item.localPort.replace(/^tcp:/, ""), 10);
+      console.log(
+        `Removing port forwarding for local port ${localPort} (remote: ${item.remotePort})`,
+      );
+      await adb.removePortForwarding(localPort);
     }
+  }
+
+  private async installLldbServer(
+    adb: AdbClient,
+    addId: string,
+    lldbServerPath: string,
+  ) {
+    await adb.shellCommand(`mkdir -p /data/local/tmp/lldb-stuff`);
+    await adb.pushFile(
+      lldbServerPath,
+      `/data/local/tmp/lldb-stuff/lldb-server`,
+    );
+    await adb.shellCommand(
+      `run-as ${addId} mkdir -p /data/data/${addId}/lldb-stuff`,
+    );
+    await adb.shellCommand(
+      `cat /data/local/tmp/lldb-stuff/lldb-server | run-as ${addId} sh -c 'cat > /data/data/${addId}/lldb-stuff/lldb-server'`,
+    );
+    await adb.shellCommand(
+      `run-as ${addId} chmod 700 /data/data/${addId}/lldb-stuff/lldb-server`,
+    );
+  }
+
+  private startLldbServer(adb: AdbClient, addId: string, abort: AbortSignal) {
+    const command =
+      `run-as ${addId} /data/data/${addId}/lldb-stuff/lldb-server` +
+      ` platform --server --listen unix-abstract:///${addId}/lldb-platform.sock` +
+      ` --log-channels "lldb process:gdb-remote packets"`;
+    // TODO: open log file
+    const writer = async () => {};
+    return adb.shellCommandToStream(command, writer, abort).then(() => {
+      // TODO: close log file
+    });
+  }
 
-    private startLldbServer(adb: AdbClient, addId: string, abort: AbortSignal) {
-        const command = `run-as ${addId} /data/data/${addId}/lldb-stuff/lldb-server`
-            + ` platform --server --listen unix-abstract:///${addId}/lldb-platform.sock`
-            + ` --log-channels "lldb process:gdb-remote packets"`;
-        // TODO: open log file
-        const writer = async () => {};
-        return adb.shellCommandToStream(command, writer, abort)
-            .then(() => {
-                // TODO: close log file
-            });
+  private async waitLldbServerReachable(adb: AdbClient, addId: string) {
+    const t1 = Date.now();
+    for (;;) {
+      const t2 = Date.now();
+      if (t2 - t1 > 10000) {
+        throw new Error("Timeout waiting for lldb-server to start");
+      }
+      const result = await adb.shellCommandToString(
+        `cat /proc/net/unix | grep ${addId}/lldb-platform.sock`,
+      );
+      if (result.trim().length > 0) {
+        return;
+      }
+      await new Promise((resolve) => setTimeout(resolve, 100));
     }
+  }
 
-    private async waitLldbServerReachable(adb: AdbClient, addId: string) {
-        const t1 = Date.now();
-        for (;;) {
-            const t2 = Date.now();
-            if (t2 - t1 > 10000) {
-                throw new Error("Timeout waiting for lldb-server to start");
-            }
-            const result = await adb.shellCommandToString(`cat /proc/net/unix | grep ${addId}/lldb-platform.sock`);
-            if (result.trim().length > 0) {
-                return;
-            }
-            await new Promise(resolve => setTimeout(resolve, 100));
-        }
+  private async startApk(
+    adb: AdbClient,
+    componentName: string,
+    wfd: boolean,
+  ): Promise<number> {
+    const parts = componentName.split("/");
+    const addId = parts[0];
+    if (parts.length === 1) {
+      componentName = parts[0] + "/.MainActivity";
     }
 
-    private async startApk(adb: AdbClient, componentName: string, wfd: boolean): Promise<number> {
-        const parts = componentName.split('/');
-        const addId = parts[0];
-        if (parts.length === 1) {
-            componentName = parts[0] + "/.MainActivity";
-        }
-
-        await adb.shellCommand(`am start -n ${componentName} -a android.intent.action.MAIN -c android.intent.category.LAUNCHER ${wfd ? '-D' : ''}`);
-
-        const t1 = Date.now();
-        for (;;) {
-            const t2 = Date.now();
-            if (t2 - t1 > 10000) {
-                throw new Error("Timeout waiting for app to start");
-            }
-            try {
-                const pid = await adb.getPid(addId);
-                return pid;
-            } catch {}
-            await new Promise(resolve => setTimeout(resolve, 100));
-        }
+    await adb.shellCommand(
+      `am start -n ${componentName} -a android.intent.action.MAIN -c android.intent.category.LAUNCHER ${wfd ? "-D" : ""}`,
+    );
+
+    const t1 = Date.now();
+    for (;;) {
+      const t2 = Date.now();
+      if (t2 - t1 > 10000) {
+        throw new Error("Timeout waiting for app to start");
+      }
+      try {
+        const pid = await adb.getPid(addId);
+        return pid;
+      } catch {}
+      await new Promise((resolve) => setTimeout(resolve, 100));
     }
+  }
 
-    private async createDefaultEnv(targetArch: string): Promise<ApkDebugEnv> {
-        const ndkPath = await Ndk.getDefaultPath();
-        if (!ndkPath) {
-            throw new Error("NDK not found");
-        }
-        const ndkVersion = await Ndk.getVersion(ndkPath);
-        if (!ndkVersion) {
-            throw new Error(`Invalid NDK at path "${ndkPath}"`);
-        }
-        const lldbServerPath = await Ndk.getLldbServerPath(ndkPath, targetArch);
-        if (!lldbServerPath) {
-            throw new Error(`Could not find lldb-server in the NDK at path "${ndkPath}" for target architecture "${targetArch}"`);
-        }
-        return {
-            lldbServerPath,
-        };
+  private async createDefaultEnv(targetArch: string): Promise<ApkDebugEnv> {
+    const ndkPath = await Ndk.getDefaultPath();
+    if (!ndkPath) {
+      throw new Error("NDK not found");
+    }
+    const ndkVersion = await Ndk.getVersion(ndkPath);
+    if (!ndkVersion) {
+      throw new Error(`Invalid NDK at path "${ndkPath}"`);
+    }
+    const lldbServerPath = await Ndk.getLldbServerPath(ndkPath, targetArch);
+    if (!lldbServerPath) {
+      throw new Error(
+        `Could not find lldb-server in the NDK at path "${ndkPath}" for target architecture "${targetArch}"`,
+      );
     }
+    return {
+      lldbServerPath,
+    };
+  }
 }
 
 export interface ApkDebugEnv {
-    lldbServerPath: string; // path to the lldb-server executable on the host machine
+  lldbServerPath: string; // path to the lldb-server executable on the host machine
 }
 
 interface SessionInfo {
-    adb: AdbClient;
-    addId: string;
-    endPromise: Promise<void>;
-    abortController: AbortController;
+  adb: AdbClient;
+  addId: string;
+  endPromise: Promise<void>;
+  abortController: AbortController;
 }
diff --git a/lldb/tools/lldb-dap/extension/src/android/core/connection.ts b/lldb/tools/lldb-dap/extension/src/android/core/connection.ts
index 810e31687483b..75e0d4a5e8a59 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/connection.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/connection.ts
@@ -1,233 +1,232 @@
-import * as net from 'node:net';
+import * as net from "node:net";
 
 /**
  * This class is a TCP client.
  */
 export class Connection {
+  private socket: net.Socket | null = null;
+  private rxBuffer: Buffer[] = [];
+  private rxSignal: Signal | null = null;
+  private rxEndOfStream = false;
+  private inError: Error | null = null;
+
+  async connect(host: string, port: number) {
+    if (this.socket) {
+      throw new Error("Connection is already established");
+    }
+    const socket = await connectSocket(host, port);
+    this.socket = socket;
+
+    socket.on("data", (data: Buffer) => {
+      this.rxBuffer.push(data);
+      if (this.rxSignal) {
+        this.rxSignal.fire();
+        this.rxSignal = null;
+      }
+    });
+    socket.on("end", () => {
+      // the peer has performed a tx shutdown or closed the connection
+      this.rxEndOfStream = true;
+      if (this.rxSignal) {
+        this.rxSignal.fire();
+        this.rxSignal = null;
+      }
+    });
+    socket.on("error", (err: Error) => {
+      console.error(`Socket error: ${err.message}`);
+      this.inError = err;
+      this.socket?.destroy();
+      this.socket = null;
+      this.rxBuffer = [];
+      if (this.rxSignal) {
+        this.rxSignal.fire();
+        this.rxSignal = null;
+      }
+      this.rxEndOfStream = false;
+    });
+  }
+
+  /**
+   * Signal the end of data transmission. Reception of data may still continue.
+   */
+  async end(): Promise<void> {
+    if (this.inError) {
+      const err = this.inError;
+      this.inError = null;
+      throw err;
+    }
+    const socket = this.socket;
+    if (!socket) {
+      throw new Error("Connection is closed");
+    }
+    return new Promise((resolve, reject) => {
+      socket.end(() => {
+        resolve();
+      });
+    });
+  }
+
+  /**
+   * Close the connection immediately.
+   * This discards buffered data.
+   */
+  close(): void {
+    const socket = this.socket;
+    if (socket) {
+      socket.destroy();
+      this.socket = null;
+      this.rxBuffer = [];
+      if (this.rxSignal) {
+        this.rxSignal.fire();
+        this.rxSignal = null;
+      }
+      this.rxEndOfStream = false;
+    }
+  }
 
-    private socket: net.Socket | null = null;
-    private rxBuffer: Buffer[] = [];
-    private rxSignal: Signal | null = null;
-    private rxEndOfStream = false;
-    private inError: Error | null = null;
+  get isConnected(): boolean {
+    return this.socket !== null;
+  }
 
-    async connect(host: string, port: number) {
-        if (this.socket) {
-            throw new Error('Connection is already established');
-        }
-        const socket = await connectSocket(host, port);
-        this.socket = socket;
-
-        socket.on('data', (data: Buffer) => {
-            this.rxBuffer.push(data);
-            if (this.rxSignal) {
-                this.rxSignal.fire();
-                this.rxSignal = null;
-            }
-        });
-        socket.on('end', () => {
-            // the peer has performed a tx shutdown or closed the connection
-            this.rxEndOfStream = true;
-            if (this.rxSignal) {
-                this.rxSignal.fire();
-                this.rxSignal = null;
-            }
-        });
-        socket.on('error', (err: Error) => {
-            console.error(`Socket error: ${err.message}`);
-            this.inError = err;
-            this.socket?.destroy();
-            this.socket = null;
-            this.rxBuffer = [];
-            if (this.rxSignal) {
-                this.rxSignal.fire();
-                this.rxSignal = null;
-            }
-            this.rxEndOfStream = false;
-        });
+  async write(data: Uint8Array): Promise<void> {
+    if (this.inError) {
+      const err = this.inError;
+      this.inError = null;
+      throw err;
     }
-
-    /**
-     * Signal the end of data transmission. Reception of data may still continue.
-     */
-    async end(): Promise<void> {
-        if (this.inError) {
-            const err = this.inError;
-            this.inError = null;
-            throw err;
-        }
-        const socket = this.socket;
-        if (!socket) {
-            throw new Error('Connection is closed');
-        }
-        return new Promise((resolve, reject) => {
-            socket.end(() => {
-                resolve();
-            });
-        });
+    const socket = this.socket;
+    if (!socket) {
+      throw new Error("Connection is closed");
     }
-
-    /**
-     * Close the connection immediately.
-     * This discards buffered data.
-     */
-    close(): void {
-        const socket = this.socket;
-        if (socket) {
-            socket.destroy();
-            this.socket = null;
-            this.rxBuffer = [];
-            if (this.rxSignal) {
-                this.rxSignal.fire();
-                this.rxSignal = null;
-            }
-            this.rxEndOfStream = false;
-        }
+    if (data.length === 0) {
+      return;
     }
-
-    get isConnected(): boolean {
-        return this.socket !== null;
+    return new Promise((resolve, reject) => {
+      socket.write(data, (err) => {
+        if (err) {
+          return reject(err);
+        }
+        resolve();
+      });
+    });
+  }
+
+  /**
+   * Get the number of bytes currently available in the receive buffer.
+   */
+  get availableData(): number {
+    let totalLength = 0;
+    for (const buf of this.rxBuffer) {
+      totalLength += buf.length;
     }
-
-    async write(data: Uint8Array): Promise<void> {
+    return totalLength;
+  }
+
+  /**
+   * Read `size` bytes from the receive buffer. If `size` is undefined, read
+   * all available data, but at least one byte.
+   * If the requested data is not yet available, wait until it is received.
+   * If the end of the stream is reached before the requested data is
+   * available, returns whatever is available (may be zero bytes).
+   */
+  async read(size?: number): Promise<Uint8Array> {
+    return new Promise(async (resolve, reject) => {
+      for (;;) {
         if (this.inError) {
-            const err = this.inError;
-            this.inError = null;
-            throw err;
+          const err = this.inError;
+          this.inError = null;
+          reject(err);
+          return;
         }
-        const socket = this.socket;
-        if (!socket) {
-            throw new Error('Connection is closed');
+        if (!this.socket) {
+          reject(new Error("Connection is closed"));
+          return;
         }
-        if (data.length === 0) {
-            return;
+        if (this.rxSignal) {
+          reject(new Error("Concurrent read operations are not supported"));
+          return;
         }
-        return new Promise((resolve, reject) => {
-            socket.write(data, (err) => {
-                if (err) {
-                    return reject(err);
-                }
-                resolve();
-            });
-        });
-    }
-
-    /**
-     * Get the number of bytes currently available in the receive buffer.
-     */
-    get availableData(): number {
-        let totalLength = 0;
-        for (const buf of this.rxBuffer) {
-            totalLength += buf.length;
+        const available = this.availableData;
+        if (available >= (size ?? 1)) {
+          const buffer = this.readFromRxBuffer(size ?? available);
+          resolve(buffer);
+          return;
         }
-        return totalLength;
-    }
-
-    /**
-     * Read `size` bytes from the receive buffer. If `size` is undefined, read
-     * all available data, but at least one byte.
-     * If the requested data is not yet available, wait until it is received.
-     * If the end of the stream is reached before the requested data is
-     * available, returns whatever is available (may be zero bytes).
-     */
-    async read(size?: number): Promise<Uint8Array> {
-        return new Promise(async (resolve, reject) => {
-            for (;;) {
-                if (this.inError) {
-                    const err = this.inError;
-                    this.inError = null;
-                    reject(err);
-                    return;
-                }
-                if (!this.socket) {
-                    reject(new Error('Connection is closed'));
-                    return;
-                }
-                if (this.rxSignal) {
-                    reject(new Error('Concurrent read operations are not supported'));
-                    return;
-                }
-                const available = this.availableData;
-                if (available >= (size ?? 1)) {
-                    const buffer = this.readFromRxBuffer(size ?? available);
-                    resolve(buffer);
-                    return;
-                }
-                if (this.rxEndOfStream) {
-                    const buffer = this.readFromRxBuffer(available);
-                    resolve(buffer);
-                    return;
-                }
-                this.rxSignal = new Signal();
-                await this.rxSignal.promise;
-            }
-        });
-    }
-
-    private readFromRxBuffer(size: number): Uint8Array {
-        const buffer = new Uint8Array(size);
-        let offset = 0;
-        while (offset < size) {
-            if (this.rxBuffer.length === 0) {
-                throw new Error('Not enough data in rxBuffer');
-            }
-            const chunk = this.rxBuffer[0];
-            const toCopy = Math.min(size - offset, chunk.length);
-            buffer.set(chunk.subarray(0, toCopy), offset);
-            offset += toCopy;
-            if (toCopy < chunk.length) {
-                this.rxBuffer[0] = chunk.subarray(toCopy);
-            } else {
-                this.rxBuffer.shift();
-            }
+        if (this.rxEndOfStream) {
+          const buffer = this.readFromRxBuffer(available);
+          resolve(buffer);
+          return;
         }
-        return buffer;
+        this.rxSignal = new Signal();
+        await this.rxSignal.promise;
+      }
+    });
+  }
+
+  private readFromRxBuffer(size: number): Uint8Array {
+    const buffer = new Uint8Array(size);
+    let offset = 0;
+    while (offset < size) {
+      if (this.rxBuffer.length === 0) {
+        throw new Error("Not enough data in rxBuffer");
+      }
+      const chunk = this.rxBuffer[0];
+      const toCopy = Math.min(size - offset, chunk.length);
+      buffer.set(chunk.subarray(0, toCopy), offset);
+      offset += toCopy;
+      if (toCopy < chunk.length) {
+        this.rxBuffer[0] = chunk.subarray(toCopy);
+      } else {
+        this.rxBuffer.shift();
+      }
     }
+    return buffer;
+  }
 }
 
 class Signal {
-    private _resolve!: () => void;
-    private _reject!: (error: Error) => void;
+  private _resolve!: () => void;
+  private _reject!: (error: Error) => void;
 
-    readonly promise: Promise<void>;
+  readonly promise: Promise<void>;
 
-    constructor() {
-        this.promise = new Promise<void>((res, rej) => {
-            this._resolve = res;
-            this._reject = rej;
-        });
-    }
+  constructor() {
+    this.promise = new Promise<void>((res, rej) => {
+      this._resolve = res;
+      this._reject = rej;
+    });
+  }
 
-    fire() {
-        this._resolve();
-    }
+  fire() {
+    this._resolve();
+  }
 
-    fireError(error: Error) {
-        this._reject(error);
-    }
+  fireError(error: Error) {
+    this._reject(error);
+  }
 }
 
 async function connectSocket(host: string, port: number): Promise<net.Socket> {
-    return new Promise((resolve, reject) => {
-        let socket: net.Socket;
-
-        const errorHandler = (err: Error) => {
-            console.error(`Connection error: ${err.message}`);
-            reject(err);
-        };
-
-        const endHandler = () => {
-            // can this happen?
-            errorHandler(new Error('Connection ended unexpectedly'));
-        };
-
-        socket = net.createConnection({ host, port }, () => {
-            socket.off('error', errorHandler);
-            socket.off('end', endHandler);
-            resolve(socket);
-        });
-
-        socket.on('error', errorHandler);
-        socket.on('end', endHandler);
+  return new Promise((resolve, reject) => {
+    let socket: net.Socket;
+
+    const errorHandler = (err: Error) => {
+      console.error(`Connection error: ${err.message}`);
+      reject(err);
+    };
+
+    const endHandler = () => {
+      // can this happen?
+      errorHandler(new Error("Connection ended unexpectedly"));
+    };
+
+    socket = net.createConnection({ host, port }, () => {
+      socket.off("error", errorHandler);
+      socket.off("end", endHandler);
+      resolve(socket);
     });
+
+    socket.on("error", errorHandler);
+    socket.on("end", endHandler);
+  });
 }
diff --git a/lldb/tools/lldb-dap/extension/src/android/core/env.ts b/lldb/tools/lldb-dap/extension/src/android/core/env.ts
index 2c406dd62f811..d07b8f173b404 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/env.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/env.ts
@@ -1,22 +1,22 @@
-import * as path from "node:path";
 import * as fs from "node:fs/promises";
 import * as os from "node:os";
+import * as path from "node:path";
 
 /**
  * TODO: probably not useful anymore
  * @deprecated
  */
 namespace Env {
-    async function getDataFolder(): Promise<string | undefined> {
-        const home = os.homedir();
-        try {
-            await fs.access(home, fs.constants.R_OK | fs.constants.W_OK);
-            const dataFolder = path.join(home, ".lldb", "android");
-            await fs.mkdir(dataFolder, { recursive: true });
-            return dataFolder;
-        } catch {}
-        return undefined;
-    }
+  async function getDataFolder(): Promise<string | undefined> {
+    const home = os.homedir();
+    try {
+      await fs.access(home, fs.constants.R_OK | fs.constants.W_OK);
+      const dataFolder = path.join(home, ".lldb", "android");
+      await fs.mkdir(dataFolder, { recursive: true });
+      return dataFolder;
+    } catch {}
+    return undefined;
+  }
 }
 
 export default Env;
diff --git a/lldb/tools/lldb-dap/extension/src/android/core/jdwp.ts b/lldb/tools/lldb-dap/extension/src/android/core/jdwp.ts
index 1132da40ea6cc..ef1360cf0d8f3 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/jdwp.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/jdwp.ts
@@ -5,146 +5,161 @@ import { Connection } from "./connection";
  */
 
 namespace Jdwp {
+  enum CommandFlag {
+    REPLY = 0x80,
+  }
 
-    enum CommandFlag {
-        REPLY = 0x80,
-    }
+  enum CommandSet {
+    VM = 1,
+    CLASS = 3,
+    OBJECT = 9,
+  }
 
-    enum CommandSet {
-        VM = 1,
-        CLASS = 3,
-        OBJECT = 9,
-    }
+  enum VmCommand {
+    VERSION = 1,
+  }
 
-    enum VmCommand {
-        VERSION = 1,
-    }
+  type CommandPacket = {
+    id: number;
+    flags: number;
+    commandSet: CommandSet;
+    command: number;
+    data: Uint8Array;
+  };
 
-    type CommandPacket = {
-        id: number;
-        flags: number;
-        commandSet: CommandSet;
-        command: number;
-        data: Uint8Array;
-    }
+  type ReplayPacket = {
+    id: number;
+    flags: number;
+    errorCode: number;
+    data: Uint8Array;
+  };
 
-    type ReplayPacket = {
-        id: number;
-        flags: number;
-        errorCode: number;
-        data: Uint8Array;
-    }
+  function encodePacket(command: CommandPacket): Uint8Array {
+    const packet = new Uint8Array(11 + command.data.length);
+    const view = new DataView(
+      packet.buffer,
+      packet.byteOffset,
+      packet.byteLength,
+    );
+    view.setUint32(0, 11 + command.data.length, false); // length
+    view.setUint32(4, command.id, false); // id
+    view.setUint8(8, command.flags); // flags
+    view.setUint8(9, command.commandSet); // commandSet
+    view.setUint8(10, command.command); // command
+    packet.set(command.data, 11);
+    return packet;
+  }
 
-    function encodePacket(command: CommandPacket): Uint8Array {
-        const packet = new Uint8Array(11 + command.data.length);
-        const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength);
-        view.setUint32(0, 11 + command.data.length, false); // length
-        view.setUint32(4, command.id, false); // id
-        view.setUint8(8, command.flags); // flags
-        view.setUint8(9, command.commandSet); // commandSet
-        view.setUint8(10, command.command); // command
-        packet.set(command.data, 11);
-        return packet;
-    }
+  function decodePacket(buffer: Uint8Array): ReplayPacket {
+    const view = new DataView(
+      buffer.buffer,
+      buffer.byteOffset,
+      buffer.byteLength,
+    );
+    const length = view.getUint32(0, false);
+    const id = view.getUint32(4, false);
+    const flags = view.getUint8(8);
+    const errorCode = view.getUint16(9, false);
+    const data = buffer.subarray(11, length);
+    return {
+      id,
+      flags,
+      errorCode,
+      data,
+    };
+  }
 
-    function decodePacket(buffer: Uint8Array): ReplayPacket {
-        const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
-        const length = view.getUint32(0, false);
-        const id = view.getUint32(4, false);
-        const flags = view.getUint8(8);
-        const errorCode = view.getUint16(9, false);
-        const data = buffer.subarray(11, length);
-        return {
-            id,
-            flags,
-            errorCode,
-            data,
-        };
-    }
+  function encodeVersionCommand() {
+    const command: CommandPacket = {
+      id: 1,
+      flags: 0,
+      commandSet: CommandSet.VM,
+      command: VmCommand.VERSION,
+      data: new Uint8Array(0),
+    };
+    return encodePacket(command);
+  }
 
-    function encodeVersionCommand() {
-        const command: CommandPacket = {
-            id: 1,
-            flags: 0,
-            commandSet: CommandSet.VM,
-            command: VmCommand.VERSION,
-            data: new Uint8Array(0),
-        };
-        return encodePacket(command);
+  function decodeVersionReply(buffer: Uint8Array) {
+    const packet = decodePacket(buffer);
+    if ((packet.flags & CommandFlag.REPLY) === 0) {
+      throw new Error("Not a reply packet");
     }
-
-    function decodeVersionReply(buffer: Uint8Array) {
-        const packet = decodePacket(buffer);
-        if ((packet.flags & CommandFlag.REPLY) === 0) {
-            throw new Error('Not a reply packet');
-        }
-        if (packet.errorCode !== 0) {
-            throw new Error(`JDWP error code: ${packet.errorCode}`);
-        }
-        const view = new DataView(packet.data.buffer, packet.data.byteOffset, packet.data.byteLength);
-        const dec = new TextDecoder();
-        let offset = 0;
-        const descLength = view.getUint32(offset, false);
-        offset += 4;
-        const desc = dec.decode(packet.data.subarray(offset, offset + descLength));
-        offset += descLength;
-        const jdwpMajor = view.getUint32(offset, false);
-        offset += 4;
-        const jdwpMinor = view.getUint32(offset, false);
-        offset += 4;
-        const vmVersionLength = view.getUint32(offset, false);
-        offset += 4;
-        const vmVersion = dec.decode(packet.data.subarray(offset, offset + vmVersionLength));
-        offset += vmVersionLength;
-        const vmNameLength = view.getUint32(offset, false);
-        offset += 4;
-        const vmName = dec.decode(packet.data.subarray(offset, offset + vmNameLength));
-        offset += vmNameLength;
-        if (offset !== packet.data.length) {
-            throw new Error('Unexpected data in version reply packet');
-        }
-        return {
-            description: desc,
-            jdwpMajor,
-            jdwpMinor,
-            vmVersion,
-            vmName,
-        };
+    if (packet.errorCode !== 0) {
+      throw new Error(`JDWP error code: ${packet.errorCode}`);
     }
-
-    async function readPacket(c: Connection): Promise<Uint8Array> {
-        const head = await c.read(11);
-        if (head.length !== 11) {
-            throw new Error('Incomplete JDWP packet header received');
-        }
-        const view = new DataView(head.buffer);
-        const length = view.getUint32(0, false);
-        const body = await c.read(length - 11);
-        if (body.length !== length - 11) {
-            throw new Error('Incomplete JDWP packet body received');
-        }
-        const full = new Uint8Array(length);
-        full.set(head, 0);
-        full.set(body, 11);
-        return full;
+    const view = new DataView(
+      packet.data.buffer,
+      packet.data.byteOffset,
+      packet.data.byteLength,
+    );
+    const dec = new TextDecoder();
+    let offset = 0;
+    const descLength = view.getUint32(offset, false);
+    offset += 4;
+    const desc = dec.decode(packet.data.subarray(offset, offset + descLength));
+    offset += descLength;
+    const jdwpMajor = view.getUint32(offset, false);
+    offset += 4;
+    const jdwpMinor = view.getUint32(offset, false);
+    offset += 4;
+    const vmVersionLength = view.getUint32(offset, false);
+    offset += 4;
+    const vmVersion = dec.decode(
+      packet.data.subarray(offset, offset + vmVersionLength),
+    );
+    offset += vmVersionLength;
+    const vmNameLength = view.getUint32(offset, false);
+    offset += 4;
+    const vmName = dec.decode(
+      packet.data.subarray(offset, offset + vmNameLength),
+    );
+    offset += vmNameLength;
+    if (offset !== packet.data.length) {
+      throw new Error("Unexpected data in version reply packet");
     }
+    return {
+      description: desc,
+      jdwpMajor,
+      jdwpMinor,
+      vmVersion,
+      vmName,
+    };
+  }
 
-    export async function handshake(c: Connection) {
-        c.write(Buffer.from("JDWP-Handshake"));
-        const reply = await c.read(14);
-        const dec = new TextDecoder();
-        if (dec.decode(reply) !== "JDWP-Handshake") {
-            throw new Error('Invalid JDWP handshake reply');
-        }
+  async function readPacket(c: Connection): Promise<Uint8Array> {
+    const head = await c.read(11);
+    if (head.length !== 11) {
+      throw new Error("Incomplete JDWP packet header received");
     }
+    const view = new DataView(head.buffer);
+    const length = view.getUint32(0, false);
+    const body = await c.read(length - 11);
+    if (body.length !== length - 11) {
+      throw new Error("Incomplete JDWP packet body received");
+    }
+    const full = new Uint8Array(length);
+    full.set(head, 0);
+    full.set(body, 11);
+    return full;
+  }
 
-    export async function getVersion(c: Connection) {
-        const versionCommand = encodeVersionCommand();
-        await c.write(versionCommand);
-        const replyBuffer = await readPacket(c);
-        const versionReply = decodeVersionReply(replyBuffer);
-        return versionReply;
+  export async function handshake(c: Connection) {
+    c.write(Buffer.from("JDWP-Handshake"));
+    const reply = await c.read(14);
+    const dec = new TextDecoder();
+    if (dec.decode(reply) !== "JDWP-Handshake") {
+      throw new Error("Invalid JDWP handshake reply");
     }
+  }
+
+  export async function getVersion(c: Connection) {
+    const versionCommand = encodeVersionCommand();
+    await c.write(versionCommand);
+    const replyBuffer = await readPacket(c);
+    const versionReply = decodeVersionReply(replyBuffer);
+    return versionReply;
+  }
 }
 
 export default Jdwp;
diff --git a/lldb/tools/lldb-dap/extension/src/android/core/ndk.ts b/lldb/tools/lldb-dap/extension/src/android/core/ndk.ts
index a7508f834aac6..d508a39d8278c 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/ndk.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/ndk.ts
@@ -1,67 +1,69 @@
-import * as path from "node:path";
 import * as fs from "node:fs/promises";
 import * as os from "node:os";
+import * as path from "node:path";
 
 /**
  * Gives access to elements of the Android NDK.
  */
 export class Ndk {
-
-    static async getDefaultPath(): Promise<string | undefined> {
-        const home = os.homedir();
-        const ndk = path.join(home, "Library", "Android", "sdk", "ndk");
-        let entries: string[] = [];
-        try {
-            entries = await fs.readdir(ndk);
-        } catch {}
-        if (entries.length === 0) {
-            return undefined;
-        }
-        entries.sort((a, b) => b.localeCompare(a, 'en-US', { numeric: true }));
-        return path.join(ndk, entries[0]);
+  static async getDefaultPath(): Promise<string | undefined> {
+    const home = os.homedir();
+    const ndk = path.join(home, "Library", "Android", "sdk", "ndk");
+    let entries: string[] = [];
+    try {
+      entries = await fs.readdir(ndk);
+    } catch {}
+    if (entries.length === 0) {
+      return undefined;
     }
+    entries.sort((a, b) => b.localeCompare(a, "en-US", { numeric: true }));
+    return path.join(ndk, entries[0]);
+  }
 
-    static async getVersion(ndkPath: string): Promise<string | undefined> {
-        const sourcePropsPath = path.join(ndkPath, "source.properties");
-        try {
-            const content = await fs.readFile(sourcePropsPath, { encoding: "utf-8" });
-            const lines = content.split("\n");
-            for (const line of lines) {
-                const match = line.match(/^Pkg.Revision\s*=\s*(.+)$/);
-                if (match) {
-                    return match[1].trim();
-                }
-            }
-        } catch {}
-    }
+  static async getVersion(ndkPath: string): Promise<string | undefined> {
+    const sourcePropsPath = path.join(ndkPath, "source.properties");
+    try {
+      const content = await fs.readFile(sourcePropsPath, { encoding: "utf-8" });
+      const lines = content.split("\n");
+      for (const line of lines) {
+        const match = line.match(/^Pkg.Revision\s*=\s*(.+)$/);
+        if (match) {
+          return match[1].trim();
+        }
+      }
+    } catch {}
+  }
 
-    static async getLldbServerPath(ndkPath: string, targetArch: string): Promise<string | undefined> {
-        const root1 = path.join(ndkPath, "toolchains", "llvm", "prebuilt");
-        try {
-            const entries1 = await fs.readdir(root1);
-            for (const entry1 of entries1) {
-                if (entry1.startsWith("darwin-")) {
-                    const root2 = path.join(root1, entry1, "lib", "clang");
+  static async getLldbServerPath(
+    ndkPath: string,
+    targetArch: string,
+  ): Promise<string | undefined> {
+    const root1 = path.join(ndkPath, "toolchains", "llvm", "prebuilt");
+    try {
+      const entries1 = await fs.readdir(root1);
+      for (const entry1 of entries1) {
+        if (entry1.startsWith("darwin-")) {
+          const root2 = path.join(root1, entry1, "lib", "clang");
+          try {
+            const entries2 = await fs.readdir(root2);
+            for (const entry2 of entries2) {
+              const root3 = path.join(root2, entry2, "lib", "linux");
+              try {
+                const entries3 = await fs.readdir(root3);
+                for (const entry3 of entries3) {
+                  if (entry3 === targetArch) {
+                    const candidate = path.join(root3, entry3, "lldb-server");
                     try {
-                        const entries2 = await fs.readdir(root2);
-                        for (const entry2 of entries2) {
-                            const root3 = path.join(root2, entry2, "lib", "linux");
-                            try {
-                                const entries3 = await fs.readdir(root3);
-                                for (const entry3 of entries3) {
-                                    if (entry3 === targetArch) {
-                                        const candidate = path.join(root3, entry3, "lldb-server");
-                                        try {
-                                            await fs.access(candidate, fs.constants.R_OK);
-                                            return candidate;
-                                        } catch {}
-                                    }
-                                }
-                            } catch {}
-                        }
+                      await fs.access(candidate, fs.constants.R_OK);
+                      return candidate;
                     } catch {}
+                  }
                 }
+              } catch {}
             }
-        } catch {}
-    }
+          } catch {}
+        }
+      }
+    } catch {}
+  }
 }
diff --git a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
index be9abed44229a..352e8aaf62d80 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
@@ -3,11 +3,11 @@ import * as fs from "node:fs/promises";
 import * as path from "path";
 import * as util from "util";
 import * as vscode from "vscode";
+import { AndroidSessionTracker } from "./android/android-session-tracker";
 import { LogFilePathProvider, LogType } from "./logging";
 import { ErrorWithNotification } from "./ui/error-with-notification";
 import { ConfigureButton, OpenSettingsButton } from "./ui/show-error-message";
 import { expandUser } from "./utils";
-import { AndroidSessionTracker } from "./android/android-session-tracker";
 
 const exec = util.promisify(child_process.execFile);
 
@@ -329,7 +329,10 @@ export class LLDBDapDescriptorFactory
       throw error;
     }
 
-    if (session.configuration.androidComponent && session.configuration.request === "launch") {
+    if (
+      session.configuration.androidComponent &&
+      session.configuration.request === "launch"
+    ) {
       this.logger.info(
         `Session "${session.name}" is an Android debug session for component ${session.configuration.androidComponent}.`,
       );
diff --git a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
index 8d25b03b27c3e..0b674b32032a3 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
@@ -1,12 +1,12 @@
 import * as child_process from "child_process";
 import * as util from "util";
 import * as vscode from "vscode";
+import { AndroidConfigurationBuilder } from "./android/android-configuration-builder";
 import { createDebugAdapterExecutable } from "./debug-adapter-factory";
 import { LLDBDapServer } from "./lldb-dap-server";
 import { LogFilePathProvider } from "./logging";
 import { ErrorWithNotification } from "./ui/error-with-notification";
 import { ConfigureButton } from "./ui/show-error-message";
-import { AndroidConfigurationBuilder } from "./android/android-configuration-builder";
 
 const exec = util.promisify(child_process.execFile);
 
@@ -237,7 +237,10 @@ export class LLDBDapConfigurationProvider
         }
       }
 
-      if (debugConfiguration.androidComponent && debugConfiguration.request === "launch") {
+      if (
+        debugConfiguration.androidComponent &&
+        debugConfiguration.request === "launch"
+      ) {
         if (
           !debugConfiguration.launchCommands ||
           debugConfiguration.launchCommands.length === 0
@@ -245,37 +248,44 @@ export class LLDBDapConfigurationProvider
           if (!debugConfiguration.androidDeviceSerial) {
             debugConfiguration.androidDeviceSerial =
               await AndroidConfigurationBuilder.resolveDeviceSerial(
-                debugConfiguration.androidDevice
+                debugConfiguration.androidDevice,
               );
           }
-          this.logger.info(`Android device serial number: ${debugConfiguration.androidDeviceSerial}`);
+          this.logger.info(
+            `Android device serial number: ${debugConfiguration.androidDeviceSerial}`,
+          );
           if (!debugConfiguration.androidTargetArch) {
             debugConfiguration.androidTargetArch =
               await AndroidConfigurationBuilder.getTargetArch(
-                debugConfiguration.androidDeviceSerial
+                debugConfiguration.androidDeviceSerial,
               );
           }
-          this.logger.info(`Android target architecture: ${debugConfiguration.androidTargetArch}`);
+          this.logger.info(
+            `Android target architecture: ${debugConfiguration.androidTargetArch}`,
+          );
           if (!debugConfiguration.androidLldbServerPath) {
             if (!debugConfiguration.androidNDKPath) {
               debugConfiguration.androidNDKPath =
-                  await AndroidConfigurationBuilder.getDefaultNdkPath();
+                await AndroidConfigurationBuilder.getDefaultNdkPath();
             }
-            const ndkVersion = await AndroidConfigurationBuilder.checkNdkAndRetrieveVersion(
-              debugConfiguration.androidNDKPath
+            const ndkVersion =
+              await AndroidConfigurationBuilder.checkNdkAndRetrieveVersion(
+                debugConfiguration.androidNDKPath,
+              );
+            this.logger.info(
+              `Android NDK path: ${debugConfiguration.androidNDKPath}`,
             );
-            this.logger.info(`Android NDK path: ${debugConfiguration.androidNDKPath}`);
             this.logger.info(`Android NDK version: ${ndkVersion}`);
             debugConfiguration.androidLldbServerPath =
               await AndroidConfigurationBuilder.getLldbServerPath(
                 debugConfiguration.androidNDKPath,
-                debugConfiguration.androidTargetArch
+                debugConfiguration.androidTargetArch,
               );
           }
           debugConfiguration.launchCommands =
             AndroidConfigurationBuilder.getLldbLaunchCommands(
               debugConfiguration.androidDeviceSerial,
-              debugConfiguration.androidComponent
+              debugConfiguration.androidComponent,
             );
         }
       }
diff --git a/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts b/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
index b56f0dbc133ce..c862e2db3fe1a 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
@@ -87,7 +87,8 @@ export class DebugSessionTracker
     let stopping = false;
     return {
       onError: (error) => !stopping && this.logger.error(error), // Can throw benign read errors when shutting down.
-      onWillReceiveMessage: (message) => this.onWillReceiveMessage(session, message),
+      onWillReceiveMessage: (message) =>
+        this.onWillReceiveMessage(session, message),
       onDidSendMessage: (message) => this.onDidSendMessage(session, message),
       onWillStopSession: () => (stopping = true),
       onExit: () => this.onExit(session),
@@ -105,9 +106,12 @@ export class DebugSessionTracker
 
   /** Clear information from the active session. */
   private onExit(session: vscode.DebugSession) {
-    const androidComponentTracker = AndroidSessionTracker.getFromSession(session);
+    const androidComponentTracker =
+      AndroidSessionTracker.getFromSession(session);
     if (androidComponentTracker) {
-      this.logger.info(`Stopping android APK "${session.configuration.androidComponent}"`);
+      this.logger.info(
+        `Stopping android APK "${session.configuration.androidComponent}"`,
+      );
       androidComponentTracker.stopDebugSession().catch();
     }
     this.modules.delete(session);
@@ -134,13 +138,17 @@ export class DebugSessionTracker
     }
   }
 
-  private onWillReceiveMessage(session: vscode.DebugSession, message: DebugProtocol.Request) {
+  private onWillReceiveMessage(
+    session: vscode.DebugSession,
+    message: DebugProtocol.Request,
+  ) {
     this.logger.info(`Received message: ${JSON.stringify(message)}`);
     if (message.command === "configurationDone") {
-      const androidComponentTracker = AndroidSessionTracker.getFromSession(session);
+      const androidComponentTracker =
+        AndroidSessionTracker.getFromSession(session);
       if (androidComponentTracker) {
         this.logger.info(
-          `Dismissing Waiting-For-Debugger dialog on Android APK "${session.configuration.androidComponent}"`
+          `Dismissing Waiting-For-Debugger dialog on Android APK "${session.configuration.androidComponent}"`,
         );
         androidComponentTracker.dismissWaitingForDebuggerDialog().catch();
       }

>From 194c3e0ff2ba866c20867a87811fc3fa2fcf524d Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Fri, 6 Feb 2026 13:24:11 +0100
Subject: [PATCH 13/18] [lldb-dap] remove noisy log

---
 lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts b/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
index c862e2db3fe1a..c9bbd71645e84 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
@@ -142,7 +142,6 @@ export class DebugSessionTracker
     session: vscode.DebugSession,
     message: DebugProtocol.Request,
   ) {
-    this.logger.info(`Received message: ${JSON.stringify(message)}`);
     if (message.command === "configurationDone") {
       const androidComponentTracker =
         AndroidSessionTracker.getFromSession(session);

>From 92312163b2017e6cb202713e0a6d012f48dadd68 Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Fri, 6 Feb 2026 14:35:45 +0100
Subject: [PATCH 14/18] [lldb-dap] keep JDWP connection open for at least 1
 second

---
 .../extension/src/android/core/adb-client.ts  | 46 ++++++++++++-------
 1 file changed, 30 insertions(+), 16 deletions(-)

diff --git a/lldb/tools/lldb-dap/extension/src/android/core/adb-client.ts b/lldb/tools/lldb-dap/extension/src/android/core/adb-client.ts
index b65656c0aec06..176cc4c4abb76 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/adb-client.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/adb-client.ts
@@ -162,25 +162,39 @@ export class AdbClient {
 
   async dismissWaitingForDebuggerDialog(pid: number): Promise<void> {
     const port = await this.addPortForwarding(`jdwp:${pid}`);
+    let connection: Connection | undefined = undefined;
+    const cleanup = async () => {
+      try {
+        connection?.close();
+      } catch {}
+      try {
+        await this.removePortForwarding(port);
+      } catch {}
+    };
     try {
-      const connection = new Connection();
+      connection = new Connection();
       await connection.connect("127.0.0.1", port);
-      try {
-        await Jdwp.handshake(connection);
-        // Dalvik is able to reply to handshake and DDM commands (command set 199)
-        // without loading the JDWP agent.
-        // By sending a version command, we force it to load the JDWP agent, which
-        // causes the "waiting for debugger" popup to be dismissed.
-        const version = await Jdwp.getVersion(connection);
-        console.log("JDWP Version:", JSON.stringify(version));
-        // TODO: understand why we need to keep the connection active for a while
-        await new Promise((resolve) => setTimeout(resolve, 200));
-      } finally {
-        connection.close();
-      }
-    } finally {
-      await this.removePortForwarding(port);
+      await Jdwp.handshake(connection);
+
+      // ART is able to reply to handshake and DDM commands (command set 199)
+      // without loading the JDWP agent.
+      // By sending a version command, we force it to load the JDWP agent, which
+      // causes the "waiting for debugger" popup to be dismissed.
+      const version = await Jdwp.getVersion(connection);
+      console.log("JDWP Version:", JSON.stringify(version));
+    } catch (error) {
+      await cleanup();
+      throw error;
     }
+
+    // When ART is in waiting-for-debugger mode, it actually runs in a loop,
+    // polling for a debugger agent every 100ms. Therefore, the agent must run
+    // for at least 100ms in order for the polling loop to detect it.
+    // Once detected, the loop exits, the dialog is dismissed and the app
+    // finally starts.
+    // To keep the agent running, we have to keep the connection open. We do
+    // that for 1 second, in background, i.e. without suspending this method.
+    setTimeout(cleanup, 1000);
   }
 
   async pushData(data: Uint8Array, remoteFilePath: string): Promise<void> {

>From bd18c3a5d1bdeea54e1c9e00bd2c960f8c270735 Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Fri, 6 Feb 2026 14:53:06 +0100
Subject: [PATCH 15/18] [lldb-dap] cleanup

---
 .../src/android/android-session-tracker.ts    |  1 +
 .../src/android/core/adb-connection.ts        |  2 +-
 .../src/android/core/apk-debug-session.ts     |  9 +++-----
 .../extension/src/android/core/env.ts         | 22 -------------------
 .../extension/src/debug-adapter-factory.ts    |  1 -
 5 files changed, 5 insertions(+), 30 deletions(-)
 delete mode 100644 lldb/tools/lldb-dap/extension/src/android/core/env.ts

diff --git a/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts b/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
index 20805eea403c5..e36746cf64740 100644
--- a/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
@@ -37,6 +37,7 @@ export class AndroidSessionTracker {
   }
 
   async startDebugSession() {
+    // TODO: Do we want some exceptions to be reported as ErrorWithNotification?
     await this.apkDebugSession.start(true);
   }
 
diff --git a/lldb/tools/lldb-dap/extension/src/android/core/adb-connection.ts b/lldb/tools/lldb-dap/extension/src/android/core/adb-connection.ts
index 5aeb515062e68..d300a0761d906 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/adb-connection.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/adb-connection.ts
@@ -261,7 +261,7 @@ export class AdbConnection extends Connection {
     }
 
     const mtime = Math.floor(Date.now() / 1000);
-    await this.writeSyncHeader("DONE", mtime); // TODO: year 2038 bug
+    await this.writeSyncHeader("DONE", mtime); // TODO: Year 2038 bug?
 
     const { responseId, dataLen } = await this.readSyncHeader();
     if (responseId === "FAIL") {
diff --git a/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts b/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
index 92c1613d84f19..1ba957fdb1203 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
@@ -68,7 +68,7 @@ export class ApkDebugSession {
     await this.startApk(adb, this.componentName, wfd);
 
     const abortController = new AbortController();
-    const endPromise = this.startLldbServer(adb, addId, abortController.signal);
+    const endPromise = this.runLldbServer(adb, addId, abortController.signal);
     this.runningSession = {
       adb,
       addId,
@@ -179,16 +179,13 @@ export class ApkDebugSession {
     );
   }
 
-  private startLldbServer(adb: AdbClient, addId: string, abort: AbortSignal) {
+  private runLldbServer(adb: AdbClient, addId: string, abort: AbortSignal) {
     const command =
       `run-as ${addId} /data/data/${addId}/lldb-stuff/lldb-server` +
       ` platform --server --listen unix-abstract:///${addId}/lldb-platform.sock` +
       ` --log-channels "lldb process:gdb-remote packets"`;
-    // TODO: open log file
     const writer = async () => {};
-    return adb.shellCommandToStream(command, writer, abort).then(() => {
-      // TODO: close log file
-    });
+    return adb.shellCommandToStream(command, writer, abort);
   }
 
   private async waitLldbServerReachable(adb: AdbClient, addId: string) {
diff --git a/lldb/tools/lldb-dap/extension/src/android/core/env.ts b/lldb/tools/lldb-dap/extension/src/android/core/env.ts
deleted file mode 100644
index d07b8f173b404..0000000000000
--- a/lldb/tools/lldb-dap/extension/src/android/core/env.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import * as fs from "node:fs/promises";
-import * as os from "node:os";
-import * as path from "node:path";
-
-/**
- * TODO: probably not useful anymore
- * @deprecated
- */
-namespace Env {
-  async function getDataFolder(): Promise<string | undefined> {
-    const home = os.homedir();
-    try {
-      await fs.access(home, fs.constants.R_OK | fs.constants.W_OK);
-      const dataFolder = path.join(home, ".lldb", "android");
-      await fs.mkdir(dataFolder, { recursive: true });
-      return dataFolder;
-    } catch {}
-    return undefined;
-  }
-}
-
-export default Env;
diff --git a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
index 352e8aaf62d80..472e84ab16d4a 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
@@ -337,7 +337,6 @@ export class LLDBDapDescriptorFactory
         `Session "${session.name}" is an Android debug session for component ${session.configuration.androidComponent}.`,
       );
       const tracker = new AndroidSessionTracker(session);
-      // TODO: handled exceptions
       await tracker.startDebugSession();
     }
 

>From b8b7eafdc8fc18603fbfeca98f97d0683c0576d8 Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Fri, 20 Feb 2026 11:24:47 +0100
Subject: [PATCH 16/18] [lldb-dap] group all Android-specific handing in the
 AndroidPlatform class

---
 .../android/android-configuration-builder.ts  |  3 +
 ...on-tracker.ts => android-debug-session.ts} | 23 ++---
 .../extension/src/android/android-platform.ts | 91 +++++++++++++++++++
 .../extension/src/debug-adapter-factory.ts    | 12 +--
 .../src/debug-configuration-provider.ts       | 58 +-----------
 .../extension/src/debug-session-tracker.ts    | 16 ++--
 6 files changed, 117 insertions(+), 86 deletions(-)
 rename lldb/tools/lldb-dap/extension/src/android/{android-session-tracker.ts => android-debug-session.ts} (68%)
 create mode 100644 lldb/tools/lldb-dap/extension/src/android/android-platform.ts

diff --git a/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts b/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
index d25ae86884f91..388a618dbf188 100644
--- a/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
@@ -4,6 +4,9 @@ import { AdbClient } from "./core/adb-client";
 import { ApkDebugSession } from "./core/apk-debug-session";
 import { Ndk } from "./core/ndk";
 
+/**
+ * This class provides utility functions for building the Android debug configuration.
+ */
 export class AndroidConfigurationBuilder {
   static async getDefaultNdkPath(): Promise<string> {
     const path = await Ndk.getDefaultPath();
diff --git a/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts b/lldb/tools/lldb-dap/extension/src/android/android-debug-session.ts
similarity index 68%
rename from lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
rename to lldb/tools/lldb-dap/extension/src/android/android-debug-session.ts
index e36746cf64740..8af879291ce18 100644
--- a/lldb/tools/lldb-dap/extension/src/android/android-session-tracker.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/android-debug-session.ts
@@ -2,26 +2,16 @@ import * as vscode from "vscode";
 import { ApkDebugSession } from "./core/apk-debug-session";
 
 /**
- * This class is for tracking the Android APK debug session associated with the
- * VS Code debug session.
+ * This class represents an Android remote debug session.
  * It includes everything needed to start and stop the Android activity
  * and lldb-server on the target device, and dismiss the "waiting for debugger"
  * dialog.
  * It assumes that the target device is connected (or the emulator is running),
  * the APK is installed, and the ADB daemon is running.
+ * An AndroidDebugSession is always associated with a VS Code debug session.
+ * It's created and tracked by the AndroidPlatform class.
  */
-export class AndroidSessionTracker {
-  private static catalog = new WeakMap<
-    vscode.DebugSession,
-    AndroidSessionTracker
-  >();
-
-  static getFromSession(
-    session: vscode.DebugSession,
-  ): AndroidSessionTracker | undefined {
-    return AndroidSessionTracker.catalog.get(session);
-  }
-
+export class AndroidDebugSession {
   private apkDebugSession: ApkDebugSession;
 
   constructor(session: vscode.DebugSession) {
@@ -33,15 +23,14 @@ export class AndroidSessionTracker {
       deviceSerial,
       componentName,
     );
-    AndroidSessionTracker.catalog.set(session, this);
   }
 
-  async startDebugSession() {
+  async start() {
     // TODO: Do we want some exceptions to be reported as ErrorWithNotification?
     await this.apkDebugSession.start(true);
   }
 
-  async stopDebugSession() {
+  async stop() {
     await this.apkDebugSession.stop();
   }
 
diff --git a/lldb/tools/lldb-dap/extension/src/android/android-platform.ts b/lldb/tools/lldb-dap/extension/src/android/android-platform.ts
new file mode 100644
index 0000000000000..86b97625cd05c
--- /dev/null
+++ b/lldb/tools/lldb-dap/extension/src/android/android-platform.ts
@@ -0,0 +1,91 @@
+import * as vscode from "vscode";
+import { AndroidConfigurationBuilder } from "./android-configuration-builder";
+import { AndroidDebugSession } from "./android-debug-session";
+
+/**
+ * This class manages Android APK debugging.
+ * It detects and resolves Android-specific debug configuration, and creates and
+ * keeps track of any AndroidDebugSession associated with a VS Code debug session.
+ */
+export class AndroidPlatform {
+  private static sessions = new WeakMap<
+    vscode.DebugSession,
+    AndroidDebugSession
+  >();
+
+  static async resolveDebugConfiguration(
+    debugConfiguration: vscode.DebugConfiguration,
+    logger: vscode.LogOutputChannel,
+  ) {
+    if (
+      debugConfiguration.androidComponent &&
+      debugConfiguration.request === "launch"
+    ) {
+      if (
+        !debugConfiguration.launchCommands ||
+        debugConfiguration.launchCommands.length === 0
+      ) {
+        if (!debugConfiguration.androidDeviceSerial) {
+          debugConfiguration.androidDeviceSerial =
+            await AndroidConfigurationBuilder.resolveDeviceSerial(
+              debugConfiguration.androidDevice,
+            );
+        }
+        logger.info(
+          `Android device serial number: ${debugConfiguration.androidDeviceSerial}`,
+        );
+        if (!debugConfiguration.androidTargetArch) {
+          debugConfiguration.androidTargetArch =
+            await AndroidConfigurationBuilder.getTargetArch(
+              debugConfiguration.androidDeviceSerial,
+            );
+        }
+        logger.info(
+          `Android target architecture: ${debugConfiguration.androidTargetArch}`,
+        );
+        if (!debugConfiguration.androidLldbServerPath) {
+          if (!debugConfiguration.androidNDKPath) {
+            debugConfiguration.androidNDKPath =
+              await AndroidConfigurationBuilder.getDefaultNdkPath();
+          }
+          const ndkVersion =
+            await AndroidConfigurationBuilder.checkNdkAndRetrieveVersion(
+              debugConfiguration.androidNDKPath,
+            );
+          logger.info(`Android NDK path: ${debugConfiguration.androidNDKPath}`);
+          logger.info(`Android NDK version: ${ndkVersion}`);
+          debugConfiguration.androidLldbServerPath =
+            await AndroidConfigurationBuilder.getLldbServerPath(
+              debugConfiguration.androidNDKPath,
+              debugConfiguration.androidTargetArch,
+            );
+        }
+        debugConfiguration.launchCommands =
+          AndroidConfigurationBuilder.getLldbLaunchCommands(
+            debugConfiguration.androidDeviceSerial,
+            debugConfiguration.androidComponent,
+          );
+      }
+    }
+  }
+
+  static async createDebugSession(
+    session: vscode.DebugSession,
+  ): Promise<AndroidDebugSession | undefined> {
+    if (
+      session.configuration.androidComponent &&
+      session.configuration.request === "launch"
+    ) {
+      const androidDebugSession = new AndroidDebugSession(session);
+      AndroidPlatform.sessions.set(session, androidDebugSession);
+      return androidDebugSession;
+    }
+    return undefined;
+  }
+
+  static getDebugSession(
+    session: vscode.DebugSession,
+  ): AndroidDebugSession | undefined {
+    return AndroidPlatform.sessions.get(session);
+  }
+}
diff --git a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
index 472e84ab16d4a..ea9092d8d1333 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-adapter-factory.ts
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
 import * as path from "path";
 import * as util from "util";
 import * as vscode from "vscode";
-import { AndroidSessionTracker } from "./android/android-session-tracker";
+import { AndroidPlatform } from "./android/android-platform";
 import { LogFilePathProvider, LogType } from "./logging";
 import { ErrorWithNotification } from "./ui/error-with-notification";
 import { ConfigureButton, OpenSettingsButton } from "./ui/show-error-message";
@@ -329,15 +329,13 @@ export class LLDBDapDescriptorFactory
       throw error;
     }
 
-    if (
-      session.configuration.androidComponent &&
-      session.configuration.request === "launch"
-    ) {
+    const androidDebugSession =
+      await AndroidPlatform.createDebugSession(session);
+    if (androidDebugSession) {
       this.logger.info(
         `Session "${session.name}" is an Android debug session for component ${session.configuration.androidComponent}.`,
       );
-      const tracker = new AndroidSessionTracker(session);
-      await tracker.startDebugSession();
+      await androidDebugSession.start();
     }
 
     // Use a server connection if the debugAdapterPort is provided
diff --git a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
index 0b674b32032a3..78ab61ad4f3ae 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-configuration-provider.ts
@@ -1,7 +1,7 @@
 import * as child_process from "child_process";
 import * as util from "util";
 import * as vscode from "vscode";
-import { AndroidConfigurationBuilder } from "./android/android-configuration-builder";
+import { AndroidPlatform } from "./android/android-platform";
 import { createDebugAdapterExecutable } from "./debug-adapter-factory";
 import { LLDBDapServer } from "./lldb-dap-server";
 import { LogFilePathProvider } from "./logging";
@@ -237,58 +237,10 @@ export class LLDBDapConfigurationProvider
         }
       }
 
-      if (
-        debugConfiguration.androidComponent &&
-        debugConfiguration.request === "launch"
-      ) {
-        if (
-          !debugConfiguration.launchCommands ||
-          debugConfiguration.launchCommands.length === 0
-        ) {
-          if (!debugConfiguration.androidDeviceSerial) {
-            debugConfiguration.androidDeviceSerial =
-              await AndroidConfigurationBuilder.resolveDeviceSerial(
-                debugConfiguration.androidDevice,
-              );
-          }
-          this.logger.info(
-            `Android device serial number: ${debugConfiguration.androidDeviceSerial}`,
-          );
-          if (!debugConfiguration.androidTargetArch) {
-            debugConfiguration.androidTargetArch =
-              await AndroidConfigurationBuilder.getTargetArch(
-                debugConfiguration.androidDeviceSerial,
-              );
-          }
-          this.logger.info(
-            `Android target architecture: ${debugConfiguration.androidTargetArch}`,
-          );
-          if (!debugConfiguration.androidLldbServerPath) {
-            if (!debugConfiguration.androidNDKPath) {
-              debugConfiguration.androidNDKPath =
-                await AndroidConfigurationBuilder.getDefaultNdkPath();
-            }
-            const ndkVersion =
-              await AndroidConfigurationBuilder.checkNdkAndRetrieveVersion(
-                debugConfiguration.androidNDKPath,
-              );
-            this.logger.info(
-              `Android NDK path: ${debugConfiguration.androidNDKPath}`,
-            );
-            this.logger.info(`Android NDK version: ${ndkVersion}`);
-            debugConfiguration.androidLldbServerPath =
-              await AndroidConfigurationBuilder.getLldbServerPath(
-                debugConfiguration.androidNDKPath,
-                debugConfiguration.androidTargetArch,
-              );
-          }
-          debugConfiguration.launchCommands =
-            AndroidConfigurationBuilder.getLldbLaunchCommands(
-              debugConfiguration.androidDeviceSerial,
-              debugConfiguration.androidComponent,
-            );
-        }
-      }
+      await AndroidPlatform.resolveDebugConfiguration(
+        debugConfiguration,
+        this.logger,
+      );
 
       this.logger.info(
         "Resolved debug configuration:\n" +
diff --git a/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts b/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
index c9bbd71645e84..a76f6a0cbbe18 100644
--- a/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
+++ b/lldb/tools/lldb-dap/extension/src/debug-session-tracker.ts
@@ -1,6 +1,6 @@
 import { DebugProtocol } from "@vscode/debugprotocol";
 import * as vscode from "vscode";
-import { AndroidSessionTracker } from "./android/android-session-tracker";
+import { AndroidPlatform } from "./android/android-platform";
 
 export interface LLDBDapCapabilities extends DebugProtocol.Capabilities {
   /** The debug adapter supports the `moduleSymbols` request. */
@@ -106,13 +106,12 @@ export class DebugSessionTracker
 
   /** Clear information from the active session. */
   private onExit(session: vscode.DebugSession) {
-    const androidComponentTracker =
-      AndroidSessionTracker.getFromSession(session);
-    if (androidComponentTracker) {
+    const androidDebugSession = AndroidPlatform.getDebugSession(session);
+    if (androidDebugSession) {
       this.logger.info(
         `Stopping android APK "${session.configuration.androidComponent}"`,
       );
-      androidComponentTracker.stopDebugSession().catch();
+      androidDebugSession.stop().catch();
     }
     this.modules.delete(session);
     this.modulesChanged.fire(undefined);
@@ -143,13 +142,12 @@ export class DebugSessionTracker
     message: DebugProtocol.Request,
   ) {
     if (message.command === "configurationDone") {
-      const androidComponentTracker =
-        AndroidSessionTracker.getFromSession(session);
-      if (androidComponentTracker) {
+      const androidDebugSession = AndroidPlatform.getDebugSession(session);
+      if (androidDebugSession) {
         this.logger.info(
           `Dismissing Waiting-For-Debugger dialog on Android APK "${session.configuration.androidComponent}"`,
         );
-        androidComponentTracker.dismissWaitingForDebuggerDialog().catch();
+        androidDebugSession.dismissWaitingForDebuggerDialog().catch();
       }
     }
   }

>From 3ce49fd9a7383dc6a5394349e4967dfadba3e3ae Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Fri, 20 Feb 2026 12:32:21 +0100
Subject: [PATCH 17/18] [lldb-dap] report errors occurring when starting the
 Android app

---
 .../extension/src/android/core/apk-debug-session.ts       | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts b/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
index 1ba957fdb1203..58f3672b30cbf 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
@@ -216,9 +216,15 @@ export class ApkDebugSession {
       componentName = parts[0] + "/.MainActivity";
     }
 
-    await adb.shellCommand(
+    let result = await adb.shellCommandToString(
       `am start -n ${componentName} -a android.intent.action.MAIN -c android.intent.category.LAUNCHER ${wfd ? "-D" : ""}`,
     );
+    if (result.includes("Error")) {
+      const errorMessage = result
+        .split("\n")
+        .find((line) => line.startsWith("Error:"));
+      throw new Error(`Failed to start the app.\n${errorMessage ?? result}`);
+    }
 
     const t1 = Date.now();
     for (;;) {

>From ff0209330181dd42bdd3435241684282903c9c36 Mon Sep 17 00:00:00 2001
From: Gabriele Mondada <gab at ijk.ch>
Date: Thu, 12 Mar 2026 17:53:51 +0100
Subject: [PATCH 18/18] [lldb-dap] add default NDK path for Windows and Linux

---
 .../android/android-configuration-builder.ts  |  2 +-
 .../src/android/core/apk-debug-session.ts     |  2 +-
 .../extension/src/android/core/ndk.ts         | 31 +++++++++++++++++--
 3 files changed, 31 insertions(+), 4 deletions(-)

diff --git a/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts b/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
index 388a618dbf188..185933310f685 100644
--- a/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/android-configuration-builder.ts
@@ -9,7 +9,7 @@ import { Ndk } from "./core/ndk";
  */
 export class AndroidConfigurationBuilder {
   static async getDefaultNdkPath(): Promise<string> {
-    const path = await Ndk.getDefaultPath();
+    const path = await Ndk.getDefaultNdkPath();
     if (!path) {
       throw new ErrorWithNotification(
         `Unable to find the Android NDK. Please install it in its default location or define its path in the settings.`,
diff --git a/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts b/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
index 58f3672b30cbf..1144b747076f4 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/apk-debug-session.ts
@@ -241,7 +241,7 @@ export class ApkDebugSession {
   }
 
   private async createDefaultEnv(targetArch: string): Promise<ApkDebugEnv> {
-    const ndkPath = await Ndk.getDefaultPath();
+    const ndkPath = await Ndk.getDefaultNdkPath();
     if (!ndkPath) {
       throw new Error("NDK not found");
     }
diff --git a/lldb/tools/lldb-dap/extension/src/android/core/ndk.ts b/lldb/tools/lldb-dap/extension/src/android/core/ndk.ts
index d508a39d8278c..0383ac7f06881 100644
--- a/lldb/tools/lldb-dap/extension/src/android/core/ndk.ts
+++ b/lldb/tools/lldb-dap/extension/src/android/core/ndk.ts
@@ -6,9 +6,36 @@ import * as path from "node:path";
  * Gives access to elements of the Android NDK.
  */
 export class Ndk {
-  static async getDefaultPath(): Promise<string | undefined> {
+  static async getDefaultSdkPath(): Promise<string | undefined> {
     const home = os.homedir();
-    const ndk = path.join(home, "Library", "Android", "sdk", "ndk");
+    let pathComponents: string[];
+    switch (process.platform) {
+      case "darwin":
+        pathComponents = [home, "Library", "Android", "sdk"];
+        break;
+      case "win32":
+        pathComponents = [home, "AppData", "Local", "Android", "Sdk"];
+        break;
+      case "linux":
+        pathComponents = [home, "Android", "Sdk"];
+        break;
+      default:
+        return undefined;
+    }
+    const sdk = path.join(...pathComponents);
+    try {
+      await fs.access(sdk);
+      return sdk;
+    } catch {}
+    return undefined;
+  }
+
+  static async getDefaultNdkPath(): Promise<string | undefined> {
+    const sdk = await this.getDefaultSdkPath();
+    if (sdk === undefined) {
+      return undefined;
+    }
+    const ndk = path.join(sdk, "ndk");
     let entries: string[] = [];
     try {
       entries = await fs.readdir(ndk);



More information about the lldb-commits mailing list