/* * This file is part of the RootTools Project: http://code.google.com/p/roottools/ * * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks * * This code is dual-licensed under the terms of the Apache License Version 2.0 and * the terms of the General Public License (GPL) Version 2. * You may use this code according to either of these licenses as is most appropriate * for your project on a case-by-case basis. * * The terms of each license can be found in the root directory of this project's repository as well as at: * * * http://www.apache.org/licenses/LICENSE-2.0 * * http://www.gnu.org/licenses/gpl-2.0.txt * * Unless required by applicable law or agreed to in writing, software * distributed under these Licenses is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See each License for the specific language governing permissions and * limitations under that License. */ package com.stericson.RootTools.execution; import java.io.*; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeoutException; import android.content.Context; import android.provider.DocumentsContract; import android.util.Log; import com.stericson.RootTools.RootTools; import com.stericson.RootTools.exceptions.RootDeniedException; public class Shell { public static enum ShellType { NORMAL, ROOT, CUSTOM } //this is only used with root shells public static enum ShellContext { NORMAL("normal"), //The normal context... SHELL("u:r:shell:s0"), //Unpriviliged shell (such as an adb shell) SYSTEM_SERVER("u:r:system_server:s0"), // system_server, u:r:system:s0 on some firmwares SYSTEM_APP("u:r:system_app:s0"), // System apps PLATFORM_APP("u:r:platform_app:s0"), // System apps UNTRUSTED_APP("u:r:untrusted_app:s0"), // Third-party apps RECOVERY("u:r:recovery:s0"); //Recovery private String value; private ShellContext(String value) { this.value = value; } public String getValue() { return this.value; } } //Statics -- visible to all private static final String token = "F*D^W@#FGF"; private static Shell rootShell = null; private static Shell shell = null; private static Shell customShell = null; //the default context for root shells... public static ShellContext defaultContext = ShellContext.NORMAL; //per shell private int shellTimeout = 25000; private ShellType shellType = null; private ShellContext shellContext = Shell.ShellContext.NORMAL; private String error = ""; private final Process proc; private final BufferedReader in; private final OutputStreamWriter out; private final List<Command> commands = new ArrayList<Command>(); //indicates whether or not to close the shell private boolean close = false; public boolean isExecuting = false; public boolean isReading = false; private int maxCommands = 5000; private int read = 0; private int write = 0; private int totalExecuted = 0; private int totalRead = 0; private boolean isCleaning = false; private Shell(String cmd, ShellType shellType, ShellContext shellContext, int shellTimeout) throws IOException, TimeoutException, RootDeniedException { RootTools.log("Starting shell: " + cmd); RootTools.log("Context: " + shellContext.getValue()); RootTools.log("Timeout: " + shellTimeout); this.shellType = shellType; this.shellTimeout = shellTimeout > 0 ? shellTimeout : this.shellTimeout; this.shellContext = shellContext; if(this.shellContext == ShellContext.NORMAL) { this.proc = new ProcessBuilder(cmd).redirectErrorStream(true).start(); } else { //only done for root shell... this.proc = new ProcessBuilder(cmd, "--context " + this.shellContext.getValue()).redirectErrorStream(true).start(); } this.in = new BufferedReader(new InputStreamReader(this.proc.getInputStream(), "UTF-8")); this.out = new OutputStreamWriter(this.proc.getOutputStream(), "UTF-8"); /** * Thread responsible for carrying out the requested operations */ Worker worker = new Worker(this); worker.start(); try { /** * The flow of execution will wait for the thread to die or wait until the * given timeout has expired. * * The result of the worker, which is determined by the exit code of the worker, * will tell us if the operation was completed successfully or it the operation * failed. */ worker.join(this.shellTimeout); /** * The operation could not be completed before the timeout occured. */ if (worker.exit == -911) { try { this.proc.destroy(); } catch (Exception e) {} closeQuietly(this.in); closeQuietly(this.out); throw new TimeoutException(this.error); } /** * Root access denied? */ else if (worker.exit == -42) { try { this.proc.destroy(); } catch (Exception e) {} closeQuietly(this.in); closeQuietly(this.out); throw new RootDeniedException("Root Access Denied"); } /** * Normal exit */ else { /** * The shell is open. * * Start two threads, one to handle the input and one to handle the output. * * input, and output are runnables that the threads execute. */ Thread si = new Thread(this.input, "Shell Input"); si.setPriority(Thread.NORM_PRIORITY); si.start(); Thread so = new Thread(this.output, "Shell Output"); so.setPriority(Thread.NORM_PRIORITY); so.start(); } } catch (InterruptedException ex) { worker.interrupt(); Thread.currentThread().interrupt(); throw new TimeoutException(); } } public Command add(Command command) throws IOException { if (this.close) throw new IllegalStateException( "Unable to add commands to a closed shell"); while (this.isCleaning) { //Don't add commands while cleaning ; } this.commands.add(command); this.notifyThreads(); return command; } public void useCWD(Context context) throws IOException, TimeoutException, RootDeniedException { add( new CommandCapture( -1, false, "cd " + context.getApplicationInfo().dataDir) ); } private void cleanCommands() { this.isCleaning = true; int toClean = Math.abs(this.maxCommands - (this.maxCommands / 4)); RootTools.log("Cleaning up: " + toClean); for (int i = 0; i < toClean; i++) { this.commands.remove(0); } this.read = this.commands.size() - 1; this.write = this.commands.size() - 1; this.isCleaning = false; } private void closeQuietly(final Reader input) { try { if (input != null) { input.close(); } } catch (Exception ignore) {} } private void closeQuietly(final Writer output) { try { if (output != null) { output.close(); } } catch (Exception ignore) {} } public void close() throws IOException { if (this == Shell.rootShell) Shell.rootShell = null; else if (this == Shell.shell) Shell.shell = null; else if (this == Shell.customShell) Shell.customShell = null; synchronized (this.commands) { /** * instruct the two threads monitoring input and output * of the shell to close. */ this.close = true; this.notifyThreads(); } } public static void closeCustomShell() throws IOException { if (Shell.customShell == null) return; Shell.customShell.close(); } public static void closeRootShell() throws IOException { if (Shell.rootShell == null) return; Shell.rootShell.close(); } public static void closeShell() throws IOException { if (Shell.shell == null) return; Shell.shell.close(); } public static void closeAll() throws IOException { Shell.closeShell(); Shell.closeRootShell(); Shell.closeCustomShell(); } public int getCommandQueuePosition(Command cmd) { return this.commands.indexOf(cmd); } public String getCommandQueuePositionString(Command cmd) { return "Command is in position " + getCommandQueuePosition(cmd) + " currently executing command at position " + this.write + " and the number of commands is " + commands.size(); } public static Shell getOpenShell() { if (Shell.customShell != null) return Shell.customShell; else if (Shell.rootShell != null) return Shell.rootShell; else return Shell.shell; } public static boolean isShellOpen() { return Shell.shell == null; } public static boolean isCustomShellOpen() { return Shell.customShell == null; } public static boolean isRootShellOpen() { return Shell.rootShell == null; } public static boolean isAnyShellOpen() { return Shell.shell != null || Shell.rootShell != null || Shell.customShell != null; } /** * Runnable to write commands to the open shell. * <p/> * When writing commands we stay in a loop and wait for new * commands to added to "commands" * <p/> * The notification of a new command is handled by the method add in this class */ private Runnable input = new Runnable() { public void run() { try { while (true) { synchronized (commands) { /** * While loop is used in the case that notifyAll is called * and there are still no commands to be written, a rare * case but one that could happen. */ while (!close && write >= commands.size()) { isExecuting = false; commands.wait(); } } if (write >= maxCommands) { /** * wait for the read to catch up. */ while (read != write) { RootTools.log("Waiting for read and write to catch up before cleanup."); } /** * Clean up the commands, stay neat. */ cleanCommands(); } /** * Write the new command * * We write the command followed by the token to indicate * the end of the command execution */ if (write < commands.size()) { isExecuting = true; Command cmd = commands.get(write); cmd.startExecution(); RootTools.log("Executing: " + cmd.getCommand()); out.write(cmd.getCommand()); String line = "\necho " + token + " " + totalExecuted + " $?\n"; out.write(line); out.flush(); write++; totalExecuted++; } else if (close) { /** * close the thread, the shell is closing. */ isExecuting = false; out.write("\nexit 0\n"); out.flush(); RootTools.log("Closing shell"); return; } } } catch (IOException e) { RootTools.log(e.getMessage(), 2, e); } catch (InterruptedException e) { RootTools.log(e.getMessage(), 2, e); } finally { write = 0; closeQuietly(out); } } }; protected void notifyThreads() { Thread t = new Thread() { public void run() { synchronized (commands) { commands.notifyAll(); } } }; t.start(); } /** * Runnable to monitor the responses from the open shell. */ private Runnable output = new Runnable() { public void run() { try { Command command = null; while (!close) { isReading = false; String line = in.readLine(); isReading = true; /** * If we recieve EOF then the shell closed */ if (line == null) break; if (command == null) { if (read >= commands.size()) { if (close) break; continue; } command = commands.get(read); } /** * trying to determine if all commands have been completed. * * if the token is present then the command has finished execution. */ int pos = line.indexOf(token); if (pos == -1) { /** * send the output for the implementer to process */ command.output(command.id, line); } if (pos > 0) { /** * token is suffix of output, send output part to implementer */ command.output(command.id, line.substring(0, pos)); } if (pos >= 0) { line = line.substring(pos); String fields[] = line.split(" "); if (fields.length >= 2 && fields[1] != null) { int id = 0; try { id = Integer.parseInt(fields[1]); } catch (NumberFormatException e) { } int exitCode = -1; try { exitCode = Integer.parseInt(fields[2]); } catch (NumberFormatException e) { } if (id == totalRead) { command.setExitCode(exitCode); command.commandFinished(); command = null; read++; totalRead++; continue; } } } } RootTools.log("Read all output"); try { proc.waitFor(); proc.destroy(); } catch (Exception e) {} closeQuietly(out); closeQuietly(in); RootTools.log("Shell destroyed"); while (read < commands.size()) { if (command == null) command = commands.get(read); command.terminated("Unexpected Termination."); command = null; read++; } read = 0; } catch (IOException e) { RootTools.log(e.getMessage(), 2, e); } } }; public static void runRootCommand(Command command) throws IOException, TimeoutException, RootDeniedException { Shell.startRootShell().add(command); } public static void runCommand(Command command) throws IOException, TimeoutException { Shell.startShell().add(command); } public static Shell startRootShell() throws IOException, TimeoutException, RootDeniedException { return Shell.startRootShell(0, 3); } public static Shell startRootShell(int timeout) throws IOException, TimeoutException, RootDeniedException { return Shell.startRootShell(timeout, 3); } public static Shell startRootShell(int timeout, int retry) throws IOException, TimeoutException, RootDeniedException { return Shell.startRootShell(timeout, Shell.defaultContext, retry); } public static Shell startRootShell(int timeout, ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException { if (Shell.rootShell == null) { RootTools.log("Starting Root Shell!"); String cmd = "su"; // keep prompting the user until they accept for x amount of times... int retries = 0; while (Shell.rootShell == null) { try { Shell.rootShell = new Shell(cmd, ShellType.ROOT, shellContext, timeout); } catch (IOException e) { if (retries++ >= retry) { RootTools.log("IOException, could not start shell"); throw e; } } } } else if (Shell.rootShell.shellContext != shellContext) { try { RootTools.log("Context is different than open shell, switching context..."); Shell.rootShell.switchRootShellContext(shellContext); } catch (IOException e) { RootTools.log("Context could not be switched for existing root shell..."); throw e; } } else { RootTools.log("Using Existing Root Shell!"); } return Shell.rootShell; } public static Shell startCustomShell(String shellPath) throws IOException, TimeoutException, RootDeniedException { return Shell.startCustomShell(shellPath, 0); } public static Shell startCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException { if (Shell.customShell == null) { RootTools.log("Starting Custom Shell!"); Shell.customShell = new Shell(shellPath, ShellType.CUSTOM, ShellContext.NORMAL, timeout); } else RootTools.log("Using Existing Custom Shell!"); return Shell.customShell; } public static Shell startShell() throws IOException, TimeoutException { return Shell.startShell(0); } public static Shell startShell(int timeout) throws IOException, TimeoutException { try { if (Shell.shell == null) { RootTools.log("Starting Shell!"); Shell.shell = new Shell("/system/bin/sh", ShellType.NORMAL, ShellContext.NORMAL, timeout); } else RootTools.log("Using Existing Shell!"); return Shell.shell; } catch (RootDeniedException e) { //Root Denied should never be thrown. throw new IOException(); } } public Shell switchRootShellContext(ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException { if(this.shellType == ShellType.ROOT) { try { Shell.closeRootShell(); } catch(Exception e) { RootTools.log("Problem closing shell while trying to switch context..."); } //create new root shell with new context... return Shell.startRootShell(this.shellTimeout, shellContext, 3); } else { //can only switch context on a root shell... RootTools.log("Can only switch context on a root shell!"); return this; } } protected static class Worker extends Thread { public int exit = -911; public Shell shell; private Worker(Shell shell) { this.shell = shell; } public void run() { /** * Trying to open the shell. * * We echo "Started" and we look for it in the output. * * If we find the output then the shell is open and we return. * * If we do not find it then we determine the error and report * it by setting the value of the variable exit */ try { shell.out.write("echo Started\n"); shell.out.flush(); while (true) { String line = shell.in.readLine(); if (line == null) { throw new EOFException(); } if ("".equals(line)) continue; if ("Started".equals(line)) { this.exit = 1; setShellOom(); break; } shell.error = "unkown error occured."; } } catch (IOException e) { exit = -42; if (e.getMessage() != null) shell.error = e.getMessage(); else shell.error = "RootAccess denied?."; } } /* * setOom for shell processes (sh and su if root shell) * and discard outputs * */ private void setShellOom() { try { Class<?> processClass = shell.proc.getClass(); Field field; try { field = processClass.getDeclaredField("pid"); } catch (NoSuchFieldException e) { field = processClass.getDeclaredField("id"); } field.setAccessible(true); int pid = (Integer) field.get(shell.proc); shell.out.write("(echo -17 > /proc/" + pid + "/oom_adj) &> /dev/null\n"); shell.out.write("(echo -17 > /proc/$$/oom_adj) &> /dev/null\n"); shell.out.flush(); } catch (Exception e) { e.printStackTrace(); } } } }