ShellCommandExecutor.java

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * 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.
 */

package androidx.test.services.shellexecutor;

import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;

/** Executor to run shell commands with elevated permissions */
final class ShellCommandExecutor {

  private static final String TAG = "shell_cmd_exec";

  private final ExecutorService executor;

  ShellCommandExecutor(ExecutorService executor) {
    if (executor == null) {
      throw new IllegalArgumentException("You must provide an ExecutorService");
    }
    this.executor = executor;
  }

  private static void debug(String msg, Object... args) {
    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(TAG, String.format(msg, args));
    }
  }

  public void execute(ShellCommand shellCommand, OutputStream writeStdoutTo) throws IOException {

    List<String> toExecute = new ArrayList<>();

    if (shellCommand.executeThroughShell()) {
      toExecute.add("sh");
      toExecute.add("-c");
    }
    toExecute.add(shellCommand.getCommand());
    debug("Command to execute: %s", shellCommand.getCommand());
    if (shellCommand.getParameters() != null) {
      for (String parameter : shellCommand.getParameters()) {
        debug("Added param: %s", parameter);
        toExecute.add(parameter);
      }
    }

    ProcessBuilder pb = new ProcessBuilder(toExecute);

    if (shellCommand.getShellEnv() != null && shellCommand.getShellEnv().keySet() != null) {
      for (String key : shellCommand.getShellEnv().keySet()) {
        String value = shellCommand.getShellEnv().get(key);
        debug("Set envVar %s:%s", key, value);
        pb.environment().put(key, value);
      }
    }

    pb.redirectErrorStream(true);
    final Process p = pb.start();
    p.getOutputStream().close();
    p.getErrorStream().close();
    executor.execute(
        new Runnable() {
          @Override
          public void run() {
            while (true) {
              try {
                int returnCode = p.waitFor();
                debug("Process ended with return code %d", returnCode);
                return;
              } catch (InterruptedException e) {
                Log.e(TAG, "Process interrupted", e);
              }
            }
          }
        });

    executor.execute(
        new Runnable() {
          @Override
          public void run() {
            InputStream stdout = p.getInputStream();
            byte[] buf = new byte[ShellExecSharedConstants.BUFFER_SIZE];

            while (true) {
              try {
                int read = stdout.read(buf);

                if (read == -1) {
                  break;
                }

                writeStdoutTo.write(buf, 0, read);
                writeStdoutTo.flush();
              } catch (IOException e) {
                // A broken pipe exception is quite possible here and not cause for alarm.
                Log.i(TAG, "Writer disconnected, terminating");
                break;
              }
            }

            try {
              writeStdoutTo.close();
            } catch (IOException ioe) {
              Log.w(TAG, "Close threw an exception", ioe);
            }
          }
        });
  }
}