If an application needs to execute system Shell commands, it can do so through the runCmd(command: string, options?: ConditionType): ChildProcess function of @ohos.process.

Please note that this method has some limitations. Generally, it is not recommended to use, and this article is only for reference.

How to use

Download Full SDK

Since this API is a system application privilege and is not available to ordinary applications, it is necessary to download the Full SDK. If you already have the Full SDK, this step can be skipped. The Full SDK of version 11 or 12 can be downloaded from the daily builds of OpenHarmony CI. The link is: https://ci.openharmony.cn/workbench/cicd/dailybuild/dailylist

img

Download the API 11 SDK by selecting the OpenHarmony-4.1-Release branch, and download the API 12 SDK by selecting the OpenHarmony-5.0.2-Release branch.

After downloading and decompressing, point the OpenHarmony SDK to this directory through the settings of DevEco Studio.

Disable SeLinux

Due to the restrictions of SeLinux, when ordinary applications access the executable files under /bin, a permission error will be reported. Therefore, SeLinux needs to be disabled. It can be done by the following commands:

hdc shell setenforce 0

System Application Privilege

Since this API has the flag of “@systemapi Hide this for inner system use”, the application needs to be a system application. You can either pre-install the application into the system or obtain system application permissions through the APL method. The methods will not be elaborated here. If necessary, you can search it on the Internet.

Codes

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 = 'Failed';
  let errorOutputString: string = '';
  try {
    // waiting for child process finish
    const exitCode = await childProcess.wait();

    // get the standard output of child process
    const output = await childProcess.getOutput();
    outputString = buffer.from(output).toString('UTF-8');

    // get the standard error output of child process
    if (outputString === '\u0000') {
      const errorOutput = await childProcess.getErrorOutput();
      errorOutputString = buffer.from(errorOutput).toString('UTF-8');
    }

  } catch (error) {
    // handle errors
    Logger.error(TAG, JSON.stringify(error) ?? '');
    throw error as Error;
  }

  if (errorOutputString) {
    throw new Error(errorOutputString);
  }
  return outputString;
}

Above code defines the execCmd function, and the main process is as follows:

  1. After calling runCmd, a childProcess object will be returned, which represents child process.
  2. Call childProcess.wait() to wait for child process to finish execution.
  3. After child process finishes execution, use childProcess.getOutput() to obtain the result of the command execution, and use childProcess.getErrorOutput() to obtain the error after the command execution.
  4. If there is an error, throw an exception.
  5. Since the Shell command will be executed in the child process, there is no need to worry about blocking the main thread of the main process.

execCmd function is easy to use. For example, to execute a simple ls command:

execCmd('ls')
    .then(result => {})
    .catch(e => {});

The input parameter can be replaced with the command that needs to be executed.

Differences between 4.1 and 5.0

On 4.1 system, for the runCmd function, you only need to pass in the name of the executable file, such as ls or reboot. However, on 5.0, you need to pass in the full path of the executable file, like runCmd('/bin/ls').

Precautions

  1. After the runCmd function is executed, system will fork a child process from the current application process, which is represented by the childProcess object. The Shell command will be executed in the child process. This means that the permissions of the child process are the same as those of the application, and it doesn't have root privileges. Usually, application itself can't access files outside the sandbox directory, child process can't either.

  2. After testing, runCmd can only support a part of executable programs in the /bin directory.

  3. If the binary program accesses directories that the application can't access, unpredictable errors will occur. Similarly, errors will occur if the application calls APIs or operations that it doesn't have permission to access.

  4. If you have a self-written binary program that needs to be executed by the application, you can consider placing the program in the application's assets directory. Then during runtime, copy the file to the application's sandbox directory, and then execute the program by using the combination of the sandbox directory and the file name. You can refer to: https://gitee.com/kunyuan-hongke/openharmonyevaluate/blob/master/entry/src/main/ets/test/CpuMapTest.ets

Basic Principles

The code implementation of runCmd is located in 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;
    }

In fact, it instantiates a ChildProcess object and calls its Spawn function:

    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");
        }
    }

In the Spawn function, it follows the classic writing of the fork function:

  1. First, create two pipes through the pipe function. The child process can write results through this pipe, and the parent process reads the results through the pipe.
  2. Obtain the command that need to be executed.
  3. Create a child process through fork. The created child process is basically same to the parent process. Two processes all start to execute the code below fork function, and the difference is pid variable .
    • If pid is equal to 0, it means this code is executed in the child process.
    • If pid is greater than 0, it means this code is executed in the parent process.
    • If pid is less than 0, it means this code is executed in the parent process, and an error occurred while creating the child process.
  4. In the child process, first use the dup2 function to replace the two previously created pipes with standard output and error output, to pass the results to the parent process. Then, use the execl function to reload the child process to execute an executable file. That is, execute /bin/sh -c command, thus achieving the purpose of executing the command.
  5. In the main process, the main procedures are to create tasks to receive the output of the child process and callbacks related to timeouts.
Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐