应用执行Shell指令及其基本原理
应用如果需要执行系统的Shell指令,可以通过@ohos.process的runCmd(command: string, options?: ConditionType): ChildProcess函数来执行。 需要说明的是,该方法局限性比较大,通常不建议使用,仅做参考 使用方法 下载Full SDK 由于该api是系统应用权限,且不对普通应用开放,因此需要下载Full SDK,如果已经是Full
应用如果需要执行系统的Shell指令,可以通过@ohos.process
的runCmd(command: string, options?: ConditionType): ChildProcess
函数来执行。
需要说明的是,该方法局限性比较大,通常不建议使用,仅做参考
使用方法
下载Full SDK
由于该api是系统应用权限,且不对普通应用开放,因此需要下载Full SDK,如果已经是Full SDK可以忽略这一步。Full SDK可以在OpenHarmony ci的每日构建上下载对应的11或12的版本,链接:https://ci.openharmony.cn/workbench/cicd/dailybuild/dailylist
通过选择OpenHarmony-4.1-Release分支下载api 11 sdk、OpenHarmony-5.0-Release分支下载api 12 sdk。
下载解压后通过DevEco Studio的设置将OpenHarmony SDK指向该目录
关闭selinux
由于selinux的限制,普通应用访问/bin下的可执行文件时,会报权限错误,因此需要关闭selinux。可以通过如下指令关闭:
hdc shell setenforce 0
系统应用权限
由于该api有@systemapi Hide this for inner system use
标志,因此需要应用为系统应用。可以通过预置该应用到系统中,或者通过APL的方式获取系统应用权限,方法就不在此赘述,有需要可以搜索相关资料。
相关代码
import process from '@ohos.process';
import buffer from '@ohos.buffer';
async function execCmd(cmd: string): Promise<string> {
let childProcess: process.ChildProcess = process.runCmd(cmd);
let outputString: string = '执行失败';
let errorOutputString: string = '';
try {
// 等待子进程结束
const exitCode = await childProcess.wait();
// 获取子进程的标准输出
const output = await childProcess.getOutput();
outputString = buffer.from(output).toString('UTF-8');
// 获取子进程的标准错误输出
if (outputString === '\u0000') {
const errorOutput = await childProcess.getErrorOutput();
errorOutputString = buffer.from(errorOutput).toString('UTF-8');
}
} catch (error) {
// 处理可能出现的错误
Logger.error(TAG, JSON.stringify(error) ?? '');
throw error as Error;
}
if (errorOutputString) {
throw new Error(errorOutputString);
}
return outputString;
}
上述代码定义了execCmd函数,主要流程如下:
- 调用
runCmd
后会返回childProcess
对象,该对象表示子进程 - 调用
childProcess.wait()
来等待子进程执行结束 - 子进程执行结束后,通过
childProcess.getOutput()
来获取指令执行后的结果,通过childProcess.getErrorOutput()
来获取指令执行后的错误 - 如果有错误,则抛出异常
- 由于Shell指令会在子进程中执行,因此无需担心阻塞主进程的主线程
execCmd函数使用起来也比较简单,如执行一个简单的ls执行:
execCmd('ls')
.then(result => {})
.catch(e => {});
入参可以替换为需要执行的指令
4.1与5.0使用差异
在4.1的系统上,runCmd
函数仅需要传入可执行文件的名字即可,如ls、reboot。但是在5.0上,需要传入可执行文件的全路径,如runCmd('/bin/ls')
注意事项
runCmd
函数执行后,会fork当前应用进程出一个子进程,并通过childProcess
对象表示。待执行的Shell指令会在子进程中执行,这意味着子进程的权限与应用一致,并且没有root权限。通常应用本身无法访问沙箱目录外的文件,那么子进程也同样如此。- 经过测试,
runCmd
仅支持执行部分/bin下的可执行程序 - 如果执行的二进制内部,访问了应用不可访问的目录,会出现不可预料的错误。同理,如果执行了应用没有权限访问的api或操作,也会出现错误
- 如果是自己编写的二进制程序需要使用应用来执行,可以考虑将该程序放在应用的assets中,在运行时将该文件拷贝到应用的沙箱目录中,再通过沙箱目录+文件名的方式来执行该程序,可以参考:https://gitee.com/kunyuan-hongke/openharmonyevaluate/blob/master/entry/src/main/ets/test/CpuMapTest.ets
基本原理
runCmd的代码实现位于commonlibrary\ets_utils\js_sys_module\process\native_module_process.cpp
下:
static napi_value ChildProcessConstructor(napi_env env, napi_callback_info info)
{
napi_value thisVar = nullptr;
void* data = nullptr;
size_t argc = 2; // 2:The number of parameters is 2
napi_value args[2] = { nullptr }; // 2:The number of parameters is 2
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, args, &thisVar, &data));
DealType(env, args, argc);
auto objectInfo = new ChildProcess();
objectInfo->InitOptionsInfo(env, args[1]);
objectInfo->Spawn(env, args[0]);
NAPI_CALL(env, napi_wrap(
env, thisVar, objectInfo,
[](napi_env env, void* data, void* hint) {
auto objectResult = reinterpret_cast<ChildProcess*>(data);
if (objectResult != nullptr) {
delete objectResult;
objectResult = nullptr;
}
},
nullptr, nullptr));
return thisVar;
}
实际上就是实例化了一个ChildProcess对象,并调用了其Spawn
函数:
void ChildProcess::Spawn(napi_env env, napi_value command)
{
int ret = pipe(stdOutFd_);
if (ret < 0) {
HILOG_ERROR("ChildProcess:: pipe1 failed %{public}d", errno);
return;
}
ret = pipe(stdErrFd_);
if (ret < 0) {
HILOG_ERROR("ChildProcess:: pipe2 failed %{public}d", errno);
return;
}
std::string strCommnd = RequireStrValue(env, command);
pid_t pid = fork();
if (!pid) {
close(stdErrFd_[0]);
close(stdOutFd_[0]);
dup2(stdOutFd_[1], 1);
dup2(stdErrFd_[1], 2); // 2:The value of parameter
if (execl("/bin/sh", "sh", "-c", strCommnd.c_str(), nullptr) == -1) {
HILOG_ERROR("ChildProcess:: execl command failed");
_exit(127); // 127:The parameter value
}
} else if (pid > 0) {
if (optionsInfo_ == nullptr) {
HILOG_ERROR("ChildProcess:: optionsInfo_ is nullptr");
return;
}
optionsInfo_->pid = pid;
ppid_ = getpid();
CreateWorker(env);
napi_value resourceName = nullptr;
napi_create_string_utf8(env, "TimeoutListener", strlen("TimeoutListener"), &resourceName);
napi_create_async_work(
env, nullptr, resourceName, TimeoutListener,
[](napi_env env, napi_status status, void* data) {
OptionsInfo* optionsInfo = reinterpret_cast<OptionsInfo*>(data);
napi_delete_async_work(env, optionsInfo->worker);
delete optionsInfo;
optionsInfo = nullptr;
},
reinterpret_cast<void*>(optionsInfo_), &optionsInfo_->worker);
napi_queue_async_work_with_qos(env, optionsInfo_->worker, napi_qos_user_initiated);
close(stdErrFd_[1]);
close(stdOutFd_[1]);
} else {
HILOG_ERROR("ChildProcess:: child process create failed");
}
}
在Spawn
函数中,就是经典的fork函数的写法:
- 先通过pipe函数创建两个管道,子进程可以通过该管道写入结果,父进程则通过管道读出结果。
- 获取待执行的指令
- 通过fork创建子进程,创建的子进程基本与父进程完全相同,两个进程都是从fork函数之下的代码开始执行,区别则是pid不同
- 如果pid等于0,则意味着这段代码在子进程中执行
- 如果pid大于0,则意味着这段代码在父进程中执行
- 如果pid小于0,则意味着这段代码在父进程中执行,且创建子进程出现错误
- 在子进程中,首先通过dup2函数将之前创建的两个管道,替换为标准输出与错误输出,以便将结果传递给父进程。然后通过execl函数对子进程进行重载,来执行一个可执行文件。即执行
/bin/sh -c 待执行的指令
,这样就达到了执行指令的目的 - 在主进程中,主要的流程是创建任务用于接受子进程的输出,以及超时相关的回调。
更多推荐
所有评论(0)