* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {
CancellationToken,
DebugConfiguration,
ProviderResult,
WorkspaceFolder,
} from 'vscode';
import * as vscode from 'vscode';
import { exec, execSync, spawn } from 'child_process';
import path from 'path';
import * as fs from 'fs';
import { execNativeCommand } from './NewWebviewProvider';
import { config } from 'process';
export class KeelsDebugAdapterConfigProvider {
private static deviceSerialNum: string;
private static bundleName: string;
private static remotePlatform: string = '';
private static plateformSocketName: string;
private static readonly NDK_REQUIRED_VERSION = '26.3.11579264';
private static readonly CLANG_VERSION = "17";
private static readonly MAX_PID_ATTEMPTS = 10;
private static readonly PID_RETRY_DELAY = 300;
private static generatePlateformSocketName(): void {
let socketNum = Date.now().toString();
let socketDir = this.bundleName;
if (socketDir !== '') {
socketDir = `/${socketDir}`;
}
if (this.remotePlatform === 'remote-ohos') {
this.plateformSocketName = `${socketDir}/platform-${socketNum}.sock`;
} else {
this.plateformSocketName = `${socketDir}/lldb_debug-${socketNum}.sock`;
}
}
private static resetConfigInfo(config: DebugConfiguration) {
this.deviceSerialNum = config.deviceSerialNum;
this.bundleName = config.bundleName;
this.generatePlateformSocketName();
this.remotePlatform = config.remotePlatform;
config.processId = '';
if (config.afterConnectCommands === undefined) {
config.afterConnectCommands = [];
}
for (let element of config.lib_path) {
config.afterConnectCommands.push(`settings append target.exec-search-paths \"${element}\"`);
}
if (config.postAttachCommands === undefined) {
config.postAttachCommands = ["process handle -s false SIGSEGV"];
for (let element of config.source_map) {
config.postAttachCommands.push(
`settings set target.source-map ${element[0]} ${element[1].toLowerCase()}`
);
}
} else {
config.postAttachCommands.push("process handle -s false SIGSEGV");
for (let element of config.source_map) {
config.postAttachCommands.push(
`settings set target.source-map ${element[0]} ${element[1].toLowerCase()}`
);
}
}
if (config.remotePlatform === 'remote-ohos') {
config.remoteAddress = `unix-abstract-connect://[${this.deviceSerialNum}]${this.plateformSocketName}`;
} else {
config.remoteAddress = `unix-abstract-connect://[${this.deviceSerialNum}]/data/user/0${this.plateformSocketName}`;
}
if (config.startupCommands === undefined) {
config.startupCommands = ["settings set auto-confirm true"];
} else {
config.startupCommands.push("settings set auto-confirm true");
}
delete config.lib_path;
delete config.source_map;
}
private static getLLDBForOhosPath(): string {
const platform = process.platform;
const sdkRoot = process.env.DEVECO_SDK_HOME;
if (!sdkRoot) {
throw new Error('DEVECO_SDK_HOME environment variable is not set.');
}
if (platform === 'win32' || platform === 'darwin') {
const lldbPath = path.join(sdkRoot, 'default', 'hms', 'native', 'lldb', 'aarch64-linux-ohos', 'lldb-server');
if (fs.existsSync(lldbPath)) {
return lldbPath;
}
throw new Error(
`lldb-server not found at ${lldbPath}. Please ensure:\n` +
`DEVECO_SDK_HOME is set correctly (current: ${sdkRoot})`
);
} else {
throw new Error(`Unsupported platform: ${platform}`);
}
}
private static getLLDBForAndroidPath(): string {
const platform = process.platform;
const sdkRoot = process.env.ANDROID_SDK_ROOT;
if (!sdkRoot) {
throw new Error('ANDROID_SDK_ROOT environment variable is not set.');
}
const ndkPath = path.join(sdkRoot, 'ndk', this.NDK_REQUIRED_VERSION);
if (!fs.existsSync(ndkPath)) {
throw new Error(
`NDK version ${this.NDK_REQUIRED_VERSION} not found at ${ndkPath}. ` +
`Please install the required NDK version via SDK Manager or set ANDROID_SDK_ROOT correctly.`
);
}
let systemDir: string;
if (platform === 'win32') {
systemDir = 'windows-x86_64';
} else if (platform === 'darwin') {
systemDir = 'darwin-x86_64';
} else {
throw new Error(`Unsupported platform: ${platform}`);
}
const lldbPath = path.join(
ndkPath,
'toolchains',
'llvm',
'prebuilt',
systemDir,
'lib',
'clang',
this.CLANG_VERSION,
'lib',
'linux',
'aarch64',
'lldb-server'
);
if (fs.existsSync(lldbPath)) {
return lldbPath;
}
throw new Error(
`lldb-server not found at ${lldbPath}. Please ensure:\n` +
`ANDROID_SDK_ROOT is set correctly (current: ${sdkRoot})`
);
}
private static getAdbPath(): string {
const isWindows = process.platform === 'win32';
const adbExe = isWindows ? 'adb.exe' : 'adb';
if (!this.deviceSerialNum) {
throw new Error('deviceSerialNum is not set before calling getAdbPath');
}
try {
const cmd = isWindows ? `where ${adbExe}` : `which ${adbExe}`;
const pathResult = execSync(cmd, { encoding: 'utf-8' }).split('\n')[0];
if (pathResult?.trim()) {
const escapedPath = isWindows ? `"${pathResult.trim()}"` : pathResult.trim().replace(/ /g, '\\ ');
return `${escapedPath} -s ${this.deviceSerialNum}`;
}
} catch {
}
const sdkRoot = process.env.ANDROID_SDK_ROOT;
if (sdkRoot) {
const sdkPath = path.join(sdkRoot, 'platform-tools', adbExe);
if (fs.existsSync(sdkPath)) {
const escapedSdkPath = isWindows ? `"${sdkPath}"` : sdkPath.replace(/ /g, '\\ ');
return `${escapedSdkPath} -s ${this.deviceSerialNum}`;
}
}
throw new Error(
`Could not find ${adbExe} executable. Please try either;\n` +
`1. Add adb to system PATH environment variable.\n` +
`2. Set ANDROID_SDK_ROOT enviroment variable to your SDK root directory.`,
);
}
private static getHdcPath(): string {
const isWindows = process.platform === 'win32';
const hdcExe = isWindows ? 'hdc.exe' : 'hdc';
try {
const cmd = isWindows ? `where ${hdcExe}` : `which ${hdcExe}`;
const pathResult = execSync(cmd, { encoding: 'utf-8' }).split('\n')[0];
if (pathResult?.trim()) {
return pathResult.trim() + ` -t ${this.deviceSerialNum}`;
}
} catch {
}
const sdkRoot = process.env.DEVECO_SDK_HOME;
if (sdkRoot) {
const pathsToCheck = [
path.join(sdkRoot, 'toolchains', hdcExe),
path.join(sdkRoot, 'default', 'openharmony', 'toolchains', hdcExe),
];
for (const p of pathsToCheck) {
if (fs.existsSync(p)) {
return p + ` -t ${this.deviceSerialNum}`;
}
}
}
throw new Error(
`Could not find ${hdcExe} executable. Please try either;\n` +
`1. Add hdc to system PATH enviroment variable.\n` +
`2. Set DEVECO_SDK_HOME enviroment variable to your SDK root directory.`,
);
}
private static async pushLldbserverInAndroid() {
const adbCmd = this.getAdbPath();
const lldbServer = this.getLLDBForAndroidPath().replace(/\\/g, '/');
const lldbServerDir = path.join('/data/local/tmp').replace(/\\/g, '/');
const lldbServerInDevice = path.join(lldbServerDir, 'lldb-server').replace(/\\/g, '/');
const lldbServerPushCmd = `${adbCmd} push ${lldbServer} ${lldbServerInDevice}`;
const chmodInDevice = `${adbCmd} shell "run-as ${this.bundleName} sh -c 'chmod +x lldb-server'"`;
const stopLldbserverIfRunning = `${adbCmd} shell "run-as ${this.bundleName} sh -c 'pkill -9 lldb-server || true'"`;
const removeLldbserverIfExists = `${adbCmd} shell "run-as ${this.bundleName} sh -c 'if [ -e lldb-server ]; then rm -f lldb-server; fi'"`;
const cpLldbserver = `${adbCmd} shell "run-as ${this.bundleName} sh -c 'cp ${lldbServerInDevice} .'"`;
await execNativeCommand(lldbServerPushCmd).catch((err) =>
console.error('push lldb-server to device failed:', err)
);
await execNativeCommand(stopLldbserverIfRunning).catch((err) =>
console.error(`stop running lldb-server in ${this.bundleName} failed:`, err)
);
await execNativeCommand(removeLldbserverIfExists).catch((err) =>
console.error(`remove existing lldb-server in ${this.bundleName} failed:`, err)
);
await execNativeCommand(cpLldbserver).catch((err) =>
console.error(`cp lldb-server to ${this.bundleName} failed:`, err)
);
await execNativeCommand(chmodInDevice).catch((err) =>
console.error('chmod lldb-server to device failed:', err)
);
}
private static async pushLldbserverInOhos() {
const hdcCmd = this.getHdcPath();
const lldbServer = this.getLLDBForOhosPath();
const lldbServerDir = path
.join('/data/local/tmp/debugserver', this.bundleName)
.replace(/\\/g, '/');
const lldbServerPathInDevice = path.join(lldbServerDir, 'lldb-server').replace(/\\/g, '/');
const makeDir = `${hdcCmd} shell mkdir -p ${lldbServerDir}`;
const chmodInPC = `${hdcCmd} shell chmod 757 ${lldbServerDir}`;
const lldbServerPushCmd = `${hdcCmd} file send "${lldbServer}" "${lldbServerPathInDevice}"`;
const chmodInDevice = `${hdcCmd} shell chmod 755 ${lldbServerPathInDevice}`;
await execNativeCommand(makeDir).catch((err) =>
console.error('make dir in device failed:', err),
);
await execNativeCommand(chmodInPC).catch((err) =>
console.error('chmod dir in device failed:', err),
);
await execNativeCommand(lldbServerPushCmd).catch((err) =>
console.error('push lldb-server to device failed:', err),
);
await execNativeCommand(chmodInDevice).catch((err) =>
console.error('chmod lldb-server to device failed:', err),
);
}
static async sleep(milliseconds: number): Promise<void> {
return new Promise((f) => setTimeout(f, milliseconds));
}
private static async startLldbServer(name: string) {
const hdcCmd = this.getHdcPath();
const isWindows = process.platform === 'win32';
let startDebug = `${hdcCmd} shell aa process -a EntryAbility -b ${name} -D "/data/local/tmp/debugserver/${name}/lldb-server platform --listen unix-abstract://${this.plateformSocketName} --log-channels 'lldb process:gdb-remote packets' --log-file '/data/local/tmp/debugserver/${name}/platform.log'"`;
execSync(startDebug, { shell: isWindows ? 'powershell.exe' : '/bin/bash' });
}
private static async checkAndroidLLDBServerStartUp(config: DebugConfiguration): Promise<string> {
const adbCmd = this.getAdbPath();
const isWindows = process.platform === 'win32';
let exePath = isWindows
? `${adbCmd} shell ps -ef | findstr ${config.bundleName}`
: `${adbCmd} shell ps -ef | grep ${config.bundleName}`;
let cmdResultInfo = await execNativeCommand(exePath);
if (cmdResultInfo.includes(this.plateformSocketName)) {
return 'success';
}
return 'failed';
}
private static async startAndroidLldbServer(config: DebugConfiguration): Promise<string> {
const adbCmd = this.getAdbPath();
let startCmd = `${adbCmd} shell "run-as ${config.bundleName} sh -c './lldb-server platform --server --listen unix-abstract:///data/user/0${this.plateformSocketName} </dev/null >/dev/null 2>&1 &'"`;
execNativeCommand(startCmd).catch((err) => console.error('First command failed:', err));
await this.sleep(3000);
return this.checkAndroidLLDBServerStartUp(config);
}
private static parsePidofResult(output: string): string {
const pidMatch = output
.trim()
.split(/\s+/)
.find((part) => /^\d+$/.test(part));
return pidMatch ?? '';
}
private static async initAndroidPid(config: DebugConfiguration) {
await this.startAndroidLldbServer(config);
const adbCmd = this.getAdbPath();
let attemptCount = 0;
let result: string = '';
while (attemptCount < this.MAX_PID_ATTEMPTS) {
try {
const pidofResult = await execNativeCommand(`${adbCmd} shell pidof ${config.bundleName}`);
result = this.parsePidofResult(pidofResult);
} catch (err) {
console.info(`pidof lookup failed for ${config.bundleName}:`, err);
}
if (result !== '') {
config.processId = result;
break;
}
if (attemptCount < this.MAX_PID_ATTEMPTS) {
console.info(
`Try to attach application. Attempt ${attemptCount} times failed, retrying...`,
);
await this.sleep(this.PID_RETRY_DELAY);
}
attemptCount++;
}
if (result.length > 0) {
vscode.window.showInformationMessage('Start a process successfully!');
} else {
vscode.window.showInformationMessage('Failed to start process.');
}
}
private static async initOHPid(config: DebugConfiguration) {
await this.startLldbServer(config.bundleName);
const hdcCmd = this.getHdcPath();
try {
const attachCmd = `${hdcCmd} shell aa attach -b ${config.bundleName}`;
await execNativeCommand(attachCmd);
console.log(`successfully attached to bundle: ${config.bundleName}`);
await this.sleep(this.PID_RETRY_DELAY);
} catch (err) {
console.error('Failed to attach to application:', err);
vscode.window.showErrorMessage(`Failed to attach to ${config.bundleName}: ${err}`);
return;
}
let attemptCount = 0;
let result: string = '';
while (attemptCount < this.MAX_PID_ATTEMPTS) {
const isWindows = process.platform === 'win32';
let cmdResultInfo = isWindows
? await execNativeCommand(`${hdcCmd} shell ps -ef | findstr ${config.bundleName}`)
: await execNativeCommand(`${hdcCmd} shell ps -ef | grep ${config.bundleName}`);
const lines = cmdResultInfo.split('\n');
if (cmdResultInfo.includes('process:gdb-remote')) {
lines.forEach((line) => {
const parts = line.trim().split(/\s+/);
if (parts.length >= 4 && parts[7].toLowerCase() === config.bundleName) {
result = parts[1];
}
});
}
if (result !== '') {
config.processId = result.toString();
break;
}
if (attemptCount < this.MAX_PID_ATTEMPTS) {
console.info(
`Try to attach application. Attempt ${attemptCount} times failed, retrying...`,
);
await this.sleep(this.PID_RETRY_DELAY);
}
attemptCount++;
}
if (result.length > 0) {
vscode.window.showInformationMessage('Start a process successfully!');
} else {
vscode.window.showInformationMessage('Failed to start process.');
}
}
public static async resolveDebugConfigAsync(
configuration: DebugConfiguration,
): Promise<DebugConfiguration> {
const config = configuration;
this.resetConfigInfo(config);
if (config.remotePlatform === 'remote-ohos') {
await this.pushLldbserverInOhos();
await this.initOHPid(config);
} else if (config.remotePlatform === 'remote-android') {
await this.pushLldbserverInAndroid();
await this.initAndroidPid(config);
}
return config;
}
}