/** * Contrib by Chainfire (see https://raw.github.com/Chainfire/libsuperuser/master/libsuperuser/src/eu/chainfire/libsuperuser/Shell.java) */ /* * Copyright (C) 2012 Jorrit "Chainfire" Jongma * * 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 com.asksven.andoid.common.contrib; import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Class providing functionality to execute commands in a (root) shell */ public class Shell { /** * Runs commands using the supplied shell, and returns the output, or null in * case of errors. * * Note that due to compatibility with older Android versions, * wantSTDERR is not implemented using redirectErrorStream, but rather appended * to the output. STDOUT and STDERR are thus not guaranteed to be in the correct * order in the output. * * Note as well that this code will intentionally crash when run in debug mode * from the main thread of the application. You should always execute shell * commands from a background thread. * * When in debug mode, the code will also excessively log the commands passed to * and the output returned from the shell. * * Though this function uses background threads to gobble STDOUT and STDERR so * a deadlock does not occur if the shell produces massive output, the output is * still stored in a List<String>, and as such doing something like "ls -lR /" * will probably have you run out of memory. * * @param shell The shell to use for executing the commands * @param commands The commands to execute * @param wantSTDERR Return STDERR in the output ? * @return Output of the commands, or null in case of an error */ public static List<String> run(String shell, String[] commands, boolean wantSTDERR) { String shellUpper = shell.toUpperCase(); // if (BuildConfig.DEBUG) { // // check if we're running in the main thread, and if so, crash if we're in debug mode, // // to let the developer know attention is needed here. // // if (Looper.myLooper() == Looper.getMainLooper()) { // Debug.log("Application attempted to run a shell command from the main thread"); // throw new ShellOnMainThreadException(); // } // // Debug.log(String.format("[%s%%] START", shellUpper)); // } List<String> res = Collections.synchronizedList(new ArrayList<String>()); try { // setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers Process process = Runtime.getRuntime().exec(shell); DataOutputStream STDIN = new DataOutputStream(process.getOutputStream()); StreamGobbler STDOUT = new StreamGobbler(shellUpper + "-", process.getInputStream(), res); StreamGobbler STDERR = new StreamGobbler(shellUpper + "*", process.getErrorStream(), wantSTDERR ? res : null); // start gobbling and write our commands to the shell STDOUT.start(); STDERR.start(); for (String write : commands) { STDIN.writeBytes(write + "\n"); STDIN.flush(); } STDIN.writeBytes("exit\n"); STDIN.flush(); // wait for our process to finish, while we gobble away in the background process.waitFor(); // make sure our threads are done gobbling, our streams are closed, and the process is // destroyed - while the latter two shouldn't be needed in theory, and may even produce // warnings, in "normal" Java they are required for guaranteed cleanup of resources, so // lets be safe and do this on Android as well try { STDIN.close(); } catch (IOException e) { } STDOUT.join(); STDERR.join(); process.destroy(); // in case of su, 255 usually indicates access denied if (shell.equals("su") && (process.exitValue() == 255)) { res = null; } } catch (IOException e) { // shell probably not found res = null; } catch (InterruptedException e) { // this should really be re-thrown res = null; } return res; } /** * This class provides utility functions to easily execute commands using SH */ public static class SH { /** * Runs command and return output * * @param command The command to run * @return Output of the command, or null in case of an error */ public static List<String> run(String command) { return Shell.run("sh", new String[] { command }, false); } /** * Runs commands and return output * * @param commands The commands to run * @return Output of the commands, or null in case of an error */ public static List<String> run(List<String> commands) { return Shell.run("sh", commands.toArray(new String[commands.size()]), false); } /** * Runs command and return output * * @param commands The commands to run * @return Output of the commands, or null in case of an error */ public static List<String> run(String[] commands) { return Shell.run("sh", commands, false); } } /** * This class provides utility functions to easily execute commands using SU * (root shell), as well as detecting whether or not root is available, and * if so which version. */ public static class SU { /** * Runs command as root (if available) and return output * * @param command The command to run * @return Output of the command, or null if root isn't available or in case of an error */ public static List<String> run(String command) { return Shell.run("su", new String[] { command }, false); } /** * Runs commands as root (if available) and return output * * @param command The commands to run * @return Output of the commands, or null if root isn't available or in case of an error */ public static List<String> run(List<String> commands) { return Shell.run("su", commands.toArray(new String[commands.size()]), false); } /** * Runs commands as root (if available) and return output * * @param command The commands to run * @return Output of the commands, or null if root isn't available or in case of an error */ public static List<String> run(String[] commands) { return Shell.run("su", commands, false); } /** * Detects whether or not superuser access is available, by checking the output * of the "id" command if available, checking if a shell runs at all otherwise * * @return True if superuser access available */ public static boolean available() { // this is only one of many ways this can be done List<String> ret = run(new String[] { "id", "echo -EOC-" }); if (ret == null) return false; for (String line : ret) { if (line.contains("uid=")) { // id command is working, let's see if we are actually root return line.contains("uid=0"); } else if (line.contains("-EOC-")) { // if we end up here, the id command isn't present, but at // least the su commands starts some kind of shell, let's // hope it has root priviliges - no way to know without // additional native binaries return true; } } return false; } /** * Detects the version of the su binary installed (if any), if supported by the binary. * Most binaries support two different version numbers, the public version that is * displayed to users, and an internal version number that is used for version number * comparisons. Returns null if su not available or retrieving the version isn't supported. * * Note that su binary version and GUI (APK) version can be completely different. * * @param internal Request human-readable version or application internal version * @return String containing the su version or null */ public static String version(boolean internal) { // we add an additional exit call, because the command // line options are not available in all su versions, // thus potentially launching a shell instead List<String> ret = Shell.run("sh", new String[] { internal ? "su -V" : "su -v", "exit" }, false); if (ret == null) return null; for (String line : ret) { if (!internal) { if (line.contains(".")) return line; } else { try { if (Integer.parseInt(line) > 0) return line; } catch(NumberFormatException e) { } } } return null; } } }