/**
* 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;
}
}
}