* 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 { DebugConfiguration } from 'vscode';
import * as vscode from 'vscode';
import { execSync } from 'child_process';
import path from 'path';
import * as fs from 'fs';
import { execNativeCommand } from './commandExecutor';
import { RemotePlatform } from './types';
import { TunnelManager } from './apple/tunnel/tunnelManager';
import { TunnelInfo } from './apple/tunnel/tunnelInfo';
import { PyMobileDevice3 } from './apple/pyMobileDevice3CommandExecutor';
import { DeviceUtil } from './apple/deviceUtil';
import { checkIsValid } from './utils';
import { resolveCjmpSdkRoot } from './cjmpSdk';
export class KeelsDebugAdapterConfigProvider {
private static deviceSerialNum: string;
private static bundleName: string;
private static remotePlatform: string = '';
private static platformSocketName: string;
private static execNative = execNativeCommand;
private static execSyncCommand = execSync;
private static tunnelManager = TunnelManager.getInstance();
private static startDebugServer = (tunnel: TunnelInfo) => PyMobileDevice3.startDebugServer(tunnel);
private static getPidByBundleId = (deviceId: string, bundleId: string) =>
DeviceUtil.getPidByBundleId(deviceId, bundleId);
private static readonly NDK_REQUIRED_VERSION = '27.2.12479018';
private static readonly CLANG_VERSION = "18";
private static readonly MAX_PID_ATTEMPTS = 10;
private static readonly PID_RETRY_DELAY = 300;
private static generatePlatformSocketName(): void {
let socketNum = Date.now().toString();
let socketDir = this.bundleName;
if (socketDir !== '') {
socketDir = `/${socketDir}`;
}
if (this.remotePlatform === RemotePlatform.OHOS) {
this.platformSocketName = `${socketDir}/platform-${socketNum}.sock`;
} else if (this.remotePlatform === RemotePlatform.ANDROID) {
this.platformSocketName = `${socketDir}/lldb_debug-${socketNum}.sock`;
}
}
private static resetConfigInfo(config: DebugConfiguration) {
this.deviceSerialNum = config.deviceSerialNum;
this.bundleName = config.bundleName;
this.remotePlatform = config.remotePlatform;
this.generatePlatformSocketName();
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 === RemotePlatform.OHOS) {
config.remoteAddress = `unix-abstract-connect://[${this.deviceSerialNum}]${this.platformSocketName}`;
} else if (config.remotePlatform === RemotePlatform.ANDROID) {
config.remoteAddress = `unix-abstract-connect://[${this.deviceSerialNum}]/data/user/0${this.platformSocketName}`;
}
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 = this.execSyncCommand(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}. Add adb to PATH or set ANDROID_SDK_ROOT to your Android SDK 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 = this.execSyncCommand(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} required for HarmonyOS debugging. ` +
`Add hdc to PATH or set DEVECO_SDK_HOME to your HarmonyOS SDK 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 this.execNative(lldbServerPushCmd).catch((err) =>
console.error('push lldb-server to device failed:', err)
);
await this.execNative(stopLldbserverIfRunning).catch((err) =>
console.error(`stop running lldb-server in ${this.bundleName} failed:`, err)
);
await this.execNative(removeLldbserverIfExists).catch((err) =>
console.error(`remove existing lldb-server in ${this.bundleName} failed:`, err)
);
await this.execNative(cpLldbserver).catch((err) =>
console.error(`cp lldb-server to ${this.bundleName} failed:`, err)
);
await this.execNative(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 this.execNative(makeDir).catch((err) =>
console.error('make dir in device failed:', err),
);
await this.execNative(chmodInPC).catch((err) =>
console.error('chmod dir in device failed:', err),
);
await this.execNative(lldbServerPushCmd).catch((err) =>
console.error('push lldb-server to device failed:', err),
);
await this.execNative(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.platformSocketName} --log-channels 'lldb process:gdb-remote packets' --log-file '/data/local/tmp/debugserver/${name}/platform.log'"`;
this.execSyncCommand(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 this.execNative(exePath);
if (cmdResultInfo.includes(this.platformSocketName)) {
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.platformSocketName} </dev/null >/dev/null 2>&1 &'"`;
this.execNative(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 this.execNative(`${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(
`Unable to find the application process. Retrying (${attemptCount + 1}/${this.MAX_PID_ATTEMPTS})...`,
);
await this.sleep(this.PID_RETRY_DELAY);
}
attemptCount++;
}
if (result.length > 0) {
vscode.window.showInformationMessage('The application process was found and is ready for debugging.');
} else {
vscode.window.showErrorMessage(
'Unable to find the application process after multiple attempts. ' +
'Please confirm that the application is running on the selected device.',
);
}
}
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 this.execNative(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 this.execNative(`${hdcCmd} shell ps -ef | findstr ${config.bundleName}`)
: await this.execNative(`${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('The application process was found and is ready for debugging.');
} else {
vscode.window.showErrorMessage(
'Unable to find the application process after multiple attempts. ' +
'Please confirm that the application is running on the selected device.',
);
}
}
private static async initIosPid(config: DebugConfiguration, context: vscode.ExtensionContext) {
let tunnel = await this.tunnelManager.getTunnel(this.deviceSerialNum);
context.subscriptions.push(this.tunnelManager);
let debugServerConnectInfo = await this.startDebugServer(tunnel);
config.remoteAddress = debugServerConnectInfo.connectCommand;
let attemptCount = 0;
while (attemptCount < this.MAX_PID_ATTEMPTS) {
let pid = await this.getPidByBundleId(this.deviceSerialNum, config.bundleName);
if (checkIsValid(pid)) {
config.processId = pid;
break;
}
if (attemptCount < this.MAX_PID_ATTEMPTS) {
console.info(
`Unable to find the application process. Retrying (${attemptCount + 1}/${this.MAX_PID_ATTEMPTS})...`,
);
await this.sleep(this.PID_RETRY_DELAY);
}
attemptCount++;
}
if (checkIsValid(config.processId)) {
vscode.window.showInformationMessage('The application process was found and is ready for debugging.');
} else {
vscode.window.showErrorMessage(
'Unable to find the application process after multiple attempts. ' +
'Please confirm that the application is running on the selected device.',
);
}
}
private static async buildCjdbScriptCommand() {
const sdkRoot = resolveCjmpSdkRoot();
if (!sdkRoot) {
return '';
}
const cjdbScriptPath = path.join(
sdkRoot,
'cjmp-tools',
'third_party',
'cangjie-ios',
'tools',
'script',
'cangjie_cjdb.py',
);
if (fs.existsSync(cjdbScriptPath)) {
return `command script import ${cjdbScriptPath}`;
}
return '';
}
public static async resolveDebugConfigAsync(
configuration: DebugConfiguration,
context: vscode.ExtensionContext
): Promise<DebugConfiguration> {
const config = configuration;
this.resetConfigInfo(config);
if (config.remotePlatform === RemotePlatform.OHOS) {
await this.pushLldbserverInOhos();
await this.initOHPid(config);
} else if (config.remotePlatform === RemotePlatform.ANDROID) {
await this.pushLldbserverInAndroid();
await this.initAndroidPid(config);
} else if (config.remotePlatform === RemotePlatform.IOS) {
await this.initIosPid(config, context);
let cjdbScriptCommand = await this.buildCjdbScriptCommand();
checkIsValid(cjdbScriptCommand) && config.startupCommands.push(cjdbScriptCommand);
}
return config;
}
}