ShellUtils是Trinea为了方便开发者使用shell命令而开发的一个封装库,其本意是好的,但是如果对应要运行的脚本不够了解,就可能会引发严重的后果。
前两天在团建的时候,系统组的zuxi通信微信告诉我说在Ota14的电视上长按音量+/-键会导致crash,而且android和ios都有这个问题。并且给出log,部分log如下:
01-23 14:39:37.567 D/AndroidRuntime(24667): >>>>>> AndroidRuntime START com.android.internal.os.RuntimeInit <<<<<< 01-23 14:39:37.570 D/AndroidRuntime(24667): CheckJNI is OFF 01-23 14:39:37.581 W/WindowManager( 1698): >>> keyCode=25 down=true 01-23 14:39:37.582 I/InputDispatcher( 1698): Window 'Window{1fc069ce u0 com.helios.launcher/com.helios.launcher.LauncherActivity}' spent 17341.6ms processing the last input event: KeyEvent(deviceId=-1, source=0x00000101, action=0, flags=0x00000000, keyCode=25, scanCode=0, metaState=0x00000000, repeatCount=0), policyFlags=0x6b000000 01-23 14:39:37.583 I/Input (22179): injectKeyEvent: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_VOLUME_DOWN, scanCode=0, metaState=0, flags=0x0, repeatCount=0, eventTime=9753066, downTime=9753066, deviceId=-1, source=0x101 } 01-23 14:39:37.584 W/WindowManager( 1698): >>> keyCode=24 down=true 01-23 14:39:37.584 W/TelecomManager( 1698): Telecom Service not found. 01-23 14:39:37.584 W/TelecomManager( 1698): Telecom Service not found. 01-23 14:39:37.584 W/TelecomManager( 1698): Telecom Service not found. 01-23 14:39:37.584 W/TelecomManager( 1698): Telecom Service not found. 01-23 14:39:37.598 D/AndroidRuntime(24574): Calling main entry com.android.commands.input.Input 01-23 14:39:37.598 D/HeliosServerImpl( 2309): uri : / method : GET headers : {remote-addr=192.168.1.102, accept-encoding=gzip, host=192.168.1.100:12321, http-client-ip=192.168.1.102, user-agent=okhttp/3.4.1, connection=Keep-Alive} parms : {NanoHttpd.QUERY_STRING=Action=SentKey&Event=24, Action=SentKey, Event=24} files : {} 01-23 14:39:37.599 I/HeliosServerImpl( 2309): Deal KeyEvent & key = 24
很显然这不是微鲸助手的问题,而是中间件的问题了,但是为了找到问题的根源,对中间件代码进行了一番研究,发现对应的处理逻辑如下:
new Thread(new Runnable() { @Override public void run() { try { ShellUtils.execCommand("input keyevent " + event, false); } catch (Exception e) { e.printStackTrace(); } } }).start();
就不吐槽这个new Thread这种非常不节约的使用线程的方式了,进入到ShellUtils.execCommand():
public static CommandResult execCommand(String command, boolean isRoot) { return execCommand(new String[] {command}, isRoot, true); }
而execCommand()代码如下:
public static CommandResult execCommand(String[] commands, boolean isRoot, boolean isNeedResultMsg) { int result = -1; if (commands == null || commands.length == 0) { return new CommandResult(result, null, null); } Process process = null; BufferedReader successResult = null; BufferedReader errorResult = null; StringBuilder successMsg = null; StringBuilder errorMsg = null; DataOutputStream os = null; try { process = Runtime.getRuntime().exec("sh");//isRoot ? COMMAND_SU : COMMAND_SH os = new DataOutputStream(process.getOutputStream()); for (String command : commands) { if (command == null) { continue; } // donnot use os.writeBytes(commmand), avoid chinese charset error os.write(command.getBytes()); os.writeBytes(COMMAND_LINE_END); os.flush(); } os.writeBytes(COMMAND_EXIT); os.flush(); result = process.waitFor(); // get command result if (isNeedResultMsg) { successMsg = new StringBuilder(); errorMsg = new StringBuilder(); successResult = new BufferedReader(new InputStreamReader(process.getInputStream())); errorResult = new BufferedReader(new InputStreamReader(process.getErrorStream())); String s; while ((s = successResult.readLine()) != null) { successMsg.append(s); } while ((s = errorResult.readLine()) != null) { errorMsg.append(s); } } } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (os != null) { os.close(); } if (successResult != null) { successResult.close(); } if (errorResult != null) { errorResult.close(); } } catch (IOException e) { e.printStackTrace(); } if (process != null) { process.destroy(); } } return new CommandResult(result, successMsg == null ? null : successMsg.toString(), errorMsg == null ? null : errorMsg.toString()); }
之后跟踪到Runtime中:
public Process exec(String prog) throws java.io.IOException { return exec(prog, null, null); } public Process exec(String prog, String[] envp, File directory) throws java.io.IOException { // Sanity checks if (prog == null) { throw new NullPointerException("prog == null"); } else if (prog.isEmpty()) { throw new IllegalArgumentException("prog is empty"); } // Break down into tokens, as described in Java docs StringTokenizer tokenizer = new StringTokenizer(prog); int length = tokenizer.countTokens(); String[] progArray = new String[length]; for (int i = 0; i < length; i++) { progArray[i] = tokenizer.nextToken(); } // Delegate return exec(progArray, envp, directory); } public Process exec(String[] progArray, String[] envp, File directory) throws IOException { // ProcessManager is responsible for all argument checking. return ProcessManager.getInstance().exec(progArray, envp, directory, false); }
之后进入到ProcessManager中:
public Process exec(String[] taintedCommand, String[] taintedEnvironment, File workingDirectory, boolean redirectErrorStream) throws IOException { // Make sure we throw the same exceptions as the RI. if (taintedCommand == null) { throw new NullPointerException("taintedCommand == null"); } if (taintedCommand.length == 0) { throw new IndexOutOfBoundsException("taintedCommand.length == 0"); } // Handle security and safety by copying mutable inputs and checking them. String[] command = taintedCommand.clone(); String[] environment = taintedEnvironment != null ? taintedEnvironment.clone() : null; // Check we're not passing null Strings to the native exec. for (int i = 0; i < command.length; i++) { if (command[i] == null) { throw new NullPointerException("taintedCommand[" + i + "] == null"); } } // The environment is allowed to be null or empty, but no element may be null. if (environment != null) { for (int i = 0; i < environment.length; i++) { if (environment[i] == null) { throw new NullPointerException("taintedEnvironment[" + i + "] == null"); } } } FileDescriptor in = new FileDescriptor(); FileDescriptor out = new FileDescriptor(); FileDescriptor err = new FileDescriptor(); String workingPath = (workingDirectory == null) ? null : workingDirectory.getPath(); // Ensure onExit() doesn't access the process map before we add our // entry. synchronized (processReferences) { int pid; try { pid = exec(command, environment, workingPath, in, out, err, redirectErrorStream); } catch (IOException e) { IOException wrapper = new IOException("Error running exec()." + " Command: " + Arrays.toString(command) + " Working Directory: " + workingDirectory + " Environment: " + Arrays.toString(environment)); wrapper.initCause(e); throw wrapper; } ProcessImpl process = new ProcessImpl(pid, in, out, err); ProcessReference processReference = new ProcessReference(process, referenceQueue); processReferences.put(pid, processReference); /* * This will wake up the child monitor thread in case there * weren't previously any children to wait on. */ processReferences.notifyAll(); return process; } }
最终是调用到了一个native方法:
private static native int exec(String[] command, String[] environment, String workingDirectory, FileDescriptor in, FileDescriptor out, FileDescriptor err, boolean redirectErrorStream) throws IOException;
而这个方法对应的C++方法如下:
/** Executes a command in a child process. */ static pid_t ExecuteProcess(JNIEnv* env, char** commands, char** environment, const char* workingDirectory, jobject inDescriptor, jobject outDescriptor, jobject errDescriptor, jboolean redirectErrorStream) { // Create 4 pipes: stdin, stdout, stderr, and an exec() status pipe. int pipes[PIPE_COUNT * 2] = { -1, -1, -1, -1, -1, -1, -1, -1 }; for (int i = 0; i < PIPE_COUNT; i++) { if (pipe(pipes + i * 2) == -1) { jniThrowIOException(env, errno); ClosePipes(pipes, -1); return -1; } } int stdinIn = pipes[0]; int stdinOut = pipes[1]; int stdoutIn = pipes[2]; int stdoutOut = pipes[3]; int stderrIn = pipes[4]; int stderrOut = pipes[5]; int statusIn = pipes[6]; int statusOut = pipes[7]; pid_t childPid = fork(); // If fork() failed... if (childPid == -1) { jniThrowIOException(env, errno); ClosePipes(pipes, -1); return -1; } // If this is the child process... if (childPid == 0) { // Note: We cannot malloc(3) or free(3) after this point! // A thread in the parent that no longer exists in the child may have held the heap lock // when we forked, so an attempt to malloc(3) or free(3) would result in deadlock. // Replace stdin, out, and err with pipes. dup2(stdinIn, 0); dup2(stdoutOut, 1); if (redirectErrorStream) { dup2(stdoutOut, 2); } else { dup2(stderrOut, 2); } // Close all but statusOut. This saves some work in the next step. ClosePipes(pipes, statusOut); // Make statusOut automatically close if execvp() succeeds. fcntl(statusOut, F_SETFD, FD_CLOEXEC); // Close remaining unwanted open fds. CloseNonStandardFds(statusOut); // Switch to working directory. if (workingDirectory != NULL) { if (chdir(workingDirectory) == -1) { AbortChild(statusOut); } } // Set up environment. if (environment != NULL) { extern char** environ; // Standard, but not in any header file. environ = environment; } // Execute process. By convention, the first argument in the arg array // should be the command itself. execvp(commands[0], commands); AbortChild(statusOut); } // This is the parent process. // Close child's pipe ends. close(stdinIn); close(stdoutOut); close(stderrOut); close(statusOut); // Check status pipe for an error code. If execvp(2) succeeds, the other // end of the pipe should automatically close, in which case, we'll read // nothing. int child_errno; ssize_t count = TEMP_FAILURE_RETRY(read(statusIn, &child_errno, sizeof(int))); close(statusIn); if (count > 0) { // chdir(2) or execvp(2) in the child failed. // TODO: track which so we can be more specific in the detail message. jniThrowIOException(env, child_errno); close(stdoutIn); close(stdinOut); close(stderrIn); // Reap our zombie child right away. int status; int rc = TEMP_FAILURE_RETRY(waitpid(childPid, &status, 0)); if (rc == -1) { ALOGW("waitpid on failed exec failed: %s", strerror(errno)); } return -1; } // Fill in file descriptor wrappers. jniSetFileDescriptorOfFD(env, inDescriptor, stdoutIn); jniSetFileDescriptorOfFD(env, outDescriptor, stdinOut); jniSetFileDescriptorOfFD(env, errDescriptor, stderrIn); return childPid; } /** * Converts Java String[] to char** and delegates to ExecuteProcess(). */ static pid_t ProcessManager_exec(JNIEnv* env, jclass, jobjectArray javaCommands, jobjectArray javaEnvironment, jstring javaWorkingDirectory, jobject inDescriptor, jobject outDescriptor, jobject errDescriptor, jboolean redirectErrorStream) { ExecStrings commands(env, javaCommands); ExecStrings environment(env, javaEnvironment); // Extract working directory string. const char* workingDirectory = NULL; if (javaWorkingDirectory != NULL) { workingDirectory = env->GetStringUTFChars(javaWorkingDirectory, NULL); } pid_t result = ExecuteProcess(env, commands.get(), environment.get(), workingDirectory, inDescriptor, outDescriptor, errDescriptor, redirectErrorStream); // Clean up working directory string. if (javaWorkingDirectory != NULL) { env->ReleaseStringUTFChars(javaWorkingDirectory, workingDirectory); } return result; }
显然,利用fork()创建了一个子进程,并且在父子进程中使用管道传递数据.这样就基本搞清楚了Runtime.getRuntime.exec(“sh”)的本质其实就是管道通信。
但是,即使是创建了进程,又不是Zygote创建的,为何会调用到RuntimeInit呢?
当时在这里卡了一下,后面才想到是命令input可能有问题,打开它的脚本,发现如下:
# Script to start "input" on the device, which has a very rudimentary # shell. # base=/system export CLASSPATH=$base/framework/input.jar exec app_process $base/bin com.android.commands.input.Input $*
原来是先指定input.jar这个jar包的路径,再调用app_process来执行com.android.commands.input.Input的main()方法,这样就知道原因了: app_process对应的代码在frameworkks/base/cmds/app_process/app_main.cpp这个文件中,代码如下:
int main(int argc, char* const argv[]) { if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) { // Older kernels don't understand PR_SET_NO_NEW_PRIVS and return // EINVAL. Don't die on such kernels. if (errno != EINVAL) { LOG_ALWAYS_FATAL("PR_SET_NO_NEW_PRIVS failed: %s", strerror(errno)); return 12; } } AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv)); // Process command line arguments // ignore argv[0] argc--; argv++; // Everything up to '--' or first non '-' arg goes to the vm. // // The first argument after the VM args is the "parent dir", which // is currently unused. // // After the parent dir, we expect one or more the following internal // arguments : // // --zygote : Start in zygote mode // --start-system-server : Start the system server. // --application : Start in application (stand alone, non zygote) mode. // --nice-name : The nice name for this process. // // For non zygote starts, these arguments will be followed by // the main class name. All remaining arguments are passed to // the main method of this class. // // For zygote starts, all remaining arguments are passed to the zygote. // main function. // // Note that we must copy argument string values since we will rewrite the // entire argument block when we apply the nice name to argv0. int i; for (i = 0; i < argc; i++) { if (argv[i][0] != '-') { break; } if (argv[i][1] == '-' && argv[i][2] == 0) { ++i; // Skip --. break; } runtime.addOption(strdup(argv[i])); } // Parse runtime arguments. Stop at first unrecognized option. bool zygote = false; bool startSystemServer = false; bool application = false; String8 niceName; String8 className; ++i; // Skip unused "parent dir" argument. while (i < argc) { const char* arg = argv[i++]; if (strcmp(arg, "--zygote") == 0) { zygote = true; niceName = ZYGOTE_NICE_NAME; } else if (strcmp(arg, "--start-system-server") == 0) { startSystemServer = true; } else if (strcmp(arg, "--application") == 0) { application = true; } else if (strncmp(arg, "--nice-name=", 12) == 0) { niceName.setTo(arg + 12); } else if (strncmp(arg, "--", 2) != 0) { className.setTo(arg); break; } else { --i; break; } } Vector<String8> args; if (!className.isEmpty()) { // We're not in zygote mode, the only argument we need to pass // to RuntimeInit is the application argument. // // The Remainder of args get passed to startup class main(). Make // copies of them before we overwrite them with the process name. args.add(application ? String8("application") : String8("tool")); runtime.setClassNameAndArgs(className, argc - i, argv + i); } else { // We're in zygote mode. maybeCreateDalvikCache(); if (startSystemServer) { args.add(String8("start-system-server")); } char prop[PROP_VALUE_MAX]; if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) { LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.", ABI_LIST_PROPERTY); return 11; } String8 abiFlag("--abi-list="); abiFlag.append(prop); args.add(abiFlag); // In zygote mode, pass all remaining arguments to the zygote // main() method. for (; i < argc; ++i) { args.add(String8(argv[i])); } } if (!niceName.isEmpty()) { runtime.setArgv0(niceName.string()); set_process_name(niceName.string()); } if (zygote) { runtime.start("com.android.internal.os.ZygoteInit", args); } else if (className) { runtime.start("com.android.internal.os.RuntimeInit", args); } else { fprintf(stderr, "Error: no class name or --zygote supplied./n"); app_usage(); LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied."); return 10; } }
注意最后的判断语句,显然当传入Input类时,会调用runtim.start(“com.android.internal.os.RuntimeInit”),由于runtime是AppRuntime对象,而AppRuntime继承自AndroidRuntime,之后就会调用到AndroidRuntime::start()方法:
void AndroidRuntime::start(const char* className, const Vector<String8>& options) { ALOGD("/n>>>>>> AndroidRuntime START %s <<<<<</n", className != NULL ? className : "(unknown)"); static const String8 startSystemServer("start-system-server"); /* * 'startSystemServer == true' means runtime is obsolete and not run from * init.rc anymore, so we print out the boot start event here. */ for (size_t i = 0; i < options.size(); ++i) { if (options[i] == startSystemServer) { /* track our progress through the boot sequence */ const int LOG_BOOT_PROGRESS_START = 3000; LOG_EVENT_LONG(LOG_BOOT_PROGRESS_START, ns2ms(systemTime(SYSTEM_TIME_MONOTONIC))); } } const char* rootDir = getenv("ANDROID_ROOT"); if (rootDir == NULL) { rootDir = "/system"; if (!hasDir("/system")) { LOG_FATAL("No root directory specified, and /android does not exist."); return; } setenv("ANDROID_ROOT", rootDir, 1); } //const char* kernelHack = getenv("LD_ASSUME_KERNEL"); //ALOGD("Found LD_ASSUME_KERNEL='%s'/n", kernelHack); /* start the virtual machine */ JniInvocation jni_invocation; jni_invocation.Init(NULL); JNIEnv* env; if (startVm(&mJavaVM, &env) != 0) { return; } onVmCreated(env); /* * Register android functions. */ if (startReg(env) < 0) { ALOGE("Unable to register all android natives/n"); return; } /* * We want to call main() with a String array with arguments in it. * At present we have two arguments, the class name and an option string. * Create an array to hold them. */ jclass stringClass; jobjectArray strArray; jstring classNameStr; stringClass = env->FindClass("java/lang/String"); assert(stringClass != NULL); strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL); assert(strArray != NULL); classNameStr = env->NewStringUTF(className); assert(classNameStr != NULL); env->SetObjectArrayElement(strArray, 0, classNameStr); for (size_t i = 0; i < options.size(); ++i) { jstring optionsStr = env->NewStringUTF(options.itemAt(i).string()); assert(optionsStr != NULL); env->SetObjectArrayElement(strArray, i + 1, optionsStr); } /* * Start VM. This thread becomes the main thread of the VM, and will * not return until the VM exits. */ char* slashClassName = toSlashClassName(className); jclass startClass = env->FindClass(slashClassName); if (startClass == NULL) { ALOGE("JavaVM unable to locate class '%s'/n", slashClassName); /* keep going */ } else { jmethodID startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V"); if (startMeth == NULL) { ALOGE("JavaVM unable to find main() in '%s'/n", className); /* keep going */ } else { env->CallStaticVoidMethod(startClass, startMeth, strArray); #if 0 if (env->ExceptionCheck()) threadExitUncaughtException(env); #endif } } free(slashClassName); ALOGD("Shutting down VM/n"); if (mJavaVM->DetachCurrentThread() != JNI_OK) ALOGW("Warning: unable to detach main thread/n"); if (mJavaVM->DestroyJavaVM() != 0) ALOGW("Warning: VM did not shut down cleanly/n"); }
注意在这里面打印了日志而且会创建VM,所以长按音量+/-键相当于频繁创建VM,这样会使system_server挂断,从而导致系统重启。
解决方法很简单:不要使用Runtim.exec()以及input命令的方法来实现,而是使用VolumeManager.
另外,app_process其实是一个非常重要的进程,Zygote进程其实就是由它启动的,详情可以看我的这篇博客:Zygote完全解析(1)