* 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 * as vscode from 'vscode';
import { exec, execSync, spawn } from 'child_process';
import { debounce } from 'lodash';
import path from 'path';
import * as fs from 'fs';
import * as ini from 'ini';
export type PlatformType = 'android' | 'ohos' | string;
export type Category = 'mobile' | string;
export const TimeDiff = 180000;
const PLATFORM_MAPPING: Record<string, { category: Category; platform: string }> = {
android: { category: 'mobile', platform: 'Android' },
ohos: { category: 'mobile', platform: 'HarmonyOS' },
};
* Set the path of CJMP SDK
*/
export async function setCjmpSdkPath(sdkPath: string): Promise<void> {
try {
if (!sdkPath) {
vscode.window.showErrorMessage('CJMP SDK path not set. Operation cancelled.');
return;
}
const config = vscode.workspace.getConfiguration();
await config.update('CJMP.sdkPath', sdkPath, vscode.ConfigurationTarget.Global);
} catch (error) {
vscode.window.showErrorMessage(
`Failed to set CJMP SDK path: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
* Get the path of CJMP SDK
* @returns the path of CJMP SDK or undefined
*/
export async function getCjmpSdkPath(): Promise<string | undefined> {
const config = vscode.workspace.getConfiguration();
const cjmpSdkPath = config.get<string>('CJMP.sdkPath');
if (!cjmpSdkPath) {
vscode.window.showErrorMessage(
`CJMP SDK path is not set. Please configure "CJMP.sdkPath" in your settings.`,
);
return undefined;
}
return cjmpSdkPath;
}
* Validate if the CJMP SDK path is valid
* @param cjmpSdkPath The SDK path to validate
* @returns Returns true if the SDK path is valid, otherwise false
*/
export function validateCjmpSdkPath(cjmpSdkPath?: string): boolean {
const sdkPath = cjmpSdkPath || vscode.workspace.getConfiguration().get<string>('CJMP.sdkPath');
if (!sdkPath) {
vscode.window.showErrorMessage('CJMP SDK path is not configured in settings. Please set "CJMP.sdkPath".');
return false;
}
if (!sdkPath.endsWith('bin')) {
vscode.window.showErrorMessage(`CJMP SDK validation failed for path: ${sdkPath}`);
return false;
}
const cjmpToolsDir = path.dirname(sdkPath);
const thirdPartyPath = path.join(cjmpToolsDir, 'third_party');
if (!fs.existsSync(thirdPartyPath)) {
vscode.window.showErrorMessage(`CJMP SDK validation failed: The "third_party" folder was not found in ${cjmpToolsDir}.`);
return false;
}
const toolsPath = path.join(cjmpToolsDir, 'tools')
if (!fs.existsSync(toolsPath)) {
vscode.window.showErrorMessage(`CJMP SDK validation failed: The "tools" folder not found in ${cjmpToolsDir}.`);
return false;
}
return true;
}
export interface Device {
name: string;
id: string;
isSupported: boolean;
targetPlatform: string;
emulator: boolean;
sdk?: string;
category: Category;
platform: string;
}
* Execute "keels devices --machine" to obtain the device list in JSON format.
*/
export class KeelsDeviceManager {
private statusBarItem: vscode.StatusBarItem;
public currentDevice: Device | null = null;
private context: vscode.ExtensionContext;
constructor(context: vscode.ExtensionContext) {
this.context = context;
this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
this.statusBarItem.command = 'cjmp.selectDevice';
this.statusBarItem.tooltip = 'Select CJMP Device';
this.loadPersistedDevice();
this.startDeviceMonitoring();
}
private startDeviceMonitoring() {
this.startPolling();
}
private startPolling(intervalMs: number = 10000) {
setInterval(() => this.debouncedRefreshDevices(), intervalMs);
}
private debouncedRefreshDevices = debounce(() => this.refreshDevices(), 500);
public async getKeelsDevices(): Promise<Device[]> {
return new Promise((resolve) => {
const cjmpSdkPath = vscode.workspace.getConfiguration().get<string>('CJMP.sdkPath');
if (!cjmpSdkPath) {
return;
}
const isWindows = process.platform === 'win32';
const command = isWindows
? `${path.join(cjmpSdkPath, 'keels')} devices --machine`
: `${path.join(cjmpSdkPath, 'keels')} devices --machine`.replace(/\\/g, '/');
exec(command, { shell: isWindows ? 'cmd.exe' : '/bin/bash' }, (error, stdout) => {
if (error) {
resolve([]);
} else {
try {
const rawData: any[] = JSON.parse(stdout);
const devices = rawData.map((item) => {
const { category, platformName } = this.parsePlatform(item.targetPlatform || '');
return {
name: item.name || 'Unknown Device',
id: item.id || '',
isSupported: !!item.isSupported,
targetPlatform: item.targetPlatform || 'unknown',
emulator: !!item.emulator,
sdk: item.sdk,
category,
platform: platformName,
} as Device;
});
resolve(devices);
} catch (e) {
resolve([]);
}
}
});
});
}
private parsePlatform(targetPlatform: string) {
const [basePlatform, ...architectureParts] = targetPlatform.split(/[-_]/);
const mapping = PLATFORM_MAPPING[basePlatform.toLowerCase()] || {
category: 'unknown' as Category,
platform: basePlatform,
};
return {
category: mapping.category,
platformName: mapping.platform,
};
}
private async refreshDevices() {
const devices = await this.getKeelsDevices();
if (devices.length === 0) {
this.statusBarItem.text = '$(alert) No Devices';
this.statusBarItem.show();
return;
}
const currentDeviceExists =
this.currentDevice && devices.some((d) => d.id === this.currentDevice?.id);
if (!currentDeviceExists) {
const savedDeviceId = this.context.workspaceState.get<string>('keels.selectedDevice');
const savedDevice = devices.find((d) => d.id === savedDeviceId);
this.setCurrentDevice(savedDevice || devices[0]);
}
}
private loadPersistedDevice() {
const savedDeviceId = this.context.workspaceState.get<string>('keels.selectedDevice');
if (savedDeviceId) {
this.statusBarItem.text = '$(loading~spin) Loading...';
this.statusBarItem.show();
}
}
private saveCurrentDevice(device: Device) {
this.context.workspaceState.update('keels.selectedDevice', device.id);
}
private setCurrentDevice(device: Device) {
this.currentDevice = device;
this.statusBarItem.text = `$(device-mobile) ${device.name} (${device.targetPlatform})`;
this.statusBarItem.show();
this.saveCurrentDevice(device);
}
public async showDevicePicker(): Promise<Device | undefined> {
const devices = await this.getKeelsDevices();
if (devices.length === 0) {
vscode.window.showInformationMessage('No CJMP devices found.');
return;
}
const picked = await vscode.window.showQuickPick(
devices.map((d) => ({
label: this.labelForDevice(d, { withIcon: true }),
description: `${d.id} - ${d.category || d.platform || ''}`,
detail: d === this.currentDevice ? 'Current Device' : undefined,
device: d,
})),
{ placeHolder: 'Select a CJMP device' },
);
if (picked) {
this.setCurrentDevice(picked.device);
}
return picked?.device;
}
public labelForDevice(device: Device, { withIcon = false }: { withIcon?: boolean } = {}): any {
let icon;
switch (device.category) {
case 'mobile':
icon = '$(device-mobile) ';
break;
default:
icon = undefined;
}
const name = device.name;
return withIcon ? `${icon ?? ''}${name}` : name;
}
public dispose() {
this.statusBarItem.dispose();
}
}
export function getIDEType(): string {
return vscode.env.appName;
}
export function getWorkSpacePath() {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showErrorMessage('No workspace folder found.');
return;
}
return workspaceFolder.uri;
}
export async function getBundleName() {
const projectName = await getProjectName();
const orgName = await getOrgName();
if (!orgName) {
return '';
}
if (!projectName) {
return '';
}
return `${orgName}.${projectName}`;
}
export async function getProjectName() {
let projectConf = getProjectConf();
if (!projectConf) {
return;
}
const fileContent = fs.readFileSync(projectConf, 'utf-8');
const config = ini.parse(fileContent);
return config.project.name;
}
export async function getOrgName() {
let projectConf = getProjectConf();
if (!projectConf) {
return;
}
const fileContent = fs.readFileSync(projectConf, 'utf-8');
const config = ini.parse(fileContent);
return config.project.organization;
}
export async function getPackageName() {
let projectConf = getProjectConf();
if (!projectConf) {
return;
}
const fileContent = fs.readFileSync(projectConf, 'utf-8');
const config = ini.parse(fileContent);
return config.project.packageName;
}
export async function getLibPath(remotePlatform: string | undefined) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showErrorMessage('No workspace folder found.');
return;
}
let hosPath = vscode.Uri.joinPath(
workspaceFolder.uri,
process.platform === 'win32' ? 'hos\\entry\\build\\default\\intermediates\\libs\\default\\arm64-v8a' : 'hos/entry/build/default/intermediates/libs/default/arm64-v8a',
);
const toWorkspacePath = (relativePath: string) =>
vscode.Uri.joinPath(
workspaceFolder.uri,
process.platform === 'win32' ? relativePath : relativePath.replace(/\\/g, '/')
).fsPath;
const androidCandidates = [
'android\\app\\build\\intermediates\\merged_native_libs\\debug\\out\\lib\\arm64-v8a',
'android\\app\\build\\intermediates\\merged_native_libs\\debug\\mergeDebugNativeLibs\\out\\lib\\arm64-v8a',
];
const hosPathExists = fs.existsSync(hosPath.fsPath);
let androidLib = androidCandidates
.map((candidate) => toWorkspacePath(candidate))
.filter((candidatePath, index, paths) => fs.existsSync(candidatePath) && paths.indexOf(candidatePath) === index);
let hosLib = [hosPath.fsPath];
if (getProjectType() === 'logic-module') {
const projectName = await getPackageName();
const logicModuleCandidates = [
`android\\${projectName}\\build\\intermediates\\merged_native_libs\\debug\\out\\lib\\arm64-v8a`,
`android\\${projectName}\\build\\intermediates\\merged_native_libs\\debug\\mergeDebugNativeLibs\\out\\lib\\arm64-v8a`,
];
androidLib.push(
...logicModuleCandidates
.map((candidate) => toWorkspacePath(candidate))
.filter((candidatePath, index, paths) => fs.existsSync(candidatePath) && paths.indexOf(candidatePath) === index)
);
androidLib = androidLib.filter((candidatePath, index, paths) => paths.indexOf(candidatePath) === index);
}
switch (remotePlatform) {
case 'remote-ohos':
return hosPathExists ? hosLib : [];
case 'remote-android':
return androidLib;
default:
return [];
}
}
export const cjmpDoctorWithSpawn = async (): Promise<void> => {
return new Promise((resolve, reject) => {
try {
getCjmpSdkPath().then((cjmpSdkPath) => {
if (!cjmpSdkPath) {
vscode.window.showErrorMessage(
'CJMP SDK path is not configured. Please set it up first.',
);
reject(new Error('CJMP SDK path is not configured.'));
return;
}
const command = process.platform === 'win32' ? 'keels doctor -v' : './keels doctor -v';
const outputChannel = vscode.window.createOutputChannel('Keels Doctor');
outputChannel.show(true);
outputChannel.append('Running keels doctor -v ...\n');
const doctorProcess =
process.platform === 'win32'
? spawn('cmd', ['/c', command], { cwd: cjmpSdkPath })
: spawn(getDefaultShell(), ['-c', command], { cwd: cjmpSdkPath.replace(/\\/g, '/') });
doctorProcess.stdout.on('data', (data) => {
const message = data.toString();
outputChannel.append(message);
console.log(message);
});
doctorProcess.stderr.on('data', (data) => {
const errorMessage = data.toString();
outputChannel.append(errorMessage);
console.log(errorMessage);
});
doctorProcess.on('close', async (code) => {
if (code === 0) {
vscode.window.showInformationMessage('Keels doctor cammand executed successfully!');
resolve();
} else {
vscode.window.showErrorMessage(
`Failed to execute "keels doctor -v". Exit code: ${code}`,
);
reject(new Error(`Failed to execute "keels doctor -v". Exit code: ${code}`));
}
});
doctorProcess.on('error', (error) => {
vscode.window.showErrorMessage(`Failed to excute command: ${error.message}`);
outputChannel.appendLine(`Error: ${error.message}`);
reject(error);
});
});
} catch (error) {
vscode.window.showErrorMessage(
`Error running keels doctor: ${error instanceof Error ? error.message : String(error)}`,
);
reject(error);
}
});
};
export function getAdbPath(): string {
const isWindows = process.platform === 'win32';
const adbExe = isWindows ? 'adb.exe' : 'adb';
try {
const cmd = isWindows ? `where.exe ${adbExe}` : `which ${adbExe}`;
const pathResult = execSync(cmd, { encoding: 'utf-8' }).split('\n')[0];
if (pathResult?.trim()) {
return pathResult.trim();
}
} catch {
vscode.window.showErrorMessage('find env var for adb faild');
}
const sdkRoot = process.env.ANDROID_SDK_ROOT;
if (sdkRoot) {
const sdkPath = path.join(sdkRoot, 'platform-tools', adbExe);
if (fs.existsSync(sdkPath)) {
return sdkPath;
}
}
vscode.window.showErrorMessage(
`Could not find ${adbExe} executable. Please try either:\n` +
`1. Add adb to system Path env varible:\n` +
`2. Set ANDROID_SDK_ROOT environment variable to your SDK root directory.`,
);
return '';
}
export function getDefaultShell(): string {
const defaultShell = process.platform === "darwin" ? "/bin/zsh" : "/bin/bash";
return process.env.SHELL ?? defaultShell;
}
export function getProjectConf(): string | undefined {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
return '';
}
let projectConf = vscode.Uri.joinPath(workspaceFolder.uri, 'project.conf');
if (!fs.existsSync(projectConf.fsPath)) {
return '';
}
return projectConf.fsPath;
}
export function getProjectType(): string | undefined {
let projectConf = getProjectConf();
if (!projectConf) {
return;
}
const fileContent = fs.readFileSync(projectConf, 'utf-8');
const config = ini.parse(fileContent);
return config.project.type;
}
let stripAnsi: (str: string) => string;
export async function activateStripAnsi() {
stripAnsi = (await import('strip-ansi')).default;
}
const _orig = (vscode.window.createOutputChannel) as any;
vscode.window.createOutputChannel = function (
name: string,
options?: string | { log: true }
) {
const ch = _orig(name, options);
if (!('append' in ch)) return ch;
const _append = ch.append.bind(ch);
const _appendLine = ch.appendLine.bind(ch);
ch.append = (val: string) => _append(stripAnsi(val));
ch.appendLine = (val: string) => _appendLine(stripAnsi(val));
return ch;
};