Commit 185f32aa authored by Max Lv's avatar Max Lv

fix the selinux issue in galaxy s4

parent cb31dd2a
/*
* Copyright (C) 2007 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 com.github.shadowsocks;
import java.io.FileDescriptor;
/**
* Utility methods for creating and managing a subprocess.
* <p/>
* Note: The native methods access a package-private java.io.FileDescriptor
* field to get and set the raw Linux file descriptor. This might break if the
* implementation of java.io.FileDescriptor is changed.
*/
public class Exec {
static {
java.lang.System.loadLibrary("exec");
}
/** Close a given file descriptor. */
public static native void close(FileDescriptor fd);
/**
* Create a subprocess. Differs from java.lang.ProcessBuilder in that a pty
* is used to communicate with the subprocess.
* <p/>
* Callers are responsible for calling Exec.close() on the returned file
* descriptor.
*
* @param rdt Whether redirect stdout and stderr
* @param cmd The command to execute.
* @param args An array of arguments to the command.
* @param envVars An array of strings of the form "VAR=value" to be added to the
* environment of the process.
* @param scripts The scripts to execute.
* @param processId A one-element array to which the process ID of the started
* process will be written.
* @return File descriptor
*/
public static native FileDescriptor createSubprocess(int rdt, String cmd, String[] args,
String[] envVars, String scripts, int[] processId);
/** Send SIGHUP to a process group. */
public static native void hangupProcessGroup(int processId);
/**
* Causes the calling thread to wait for the process associated with the
* receiver to finish executing.
*
* @return The exit value of the Process being waited on
*/
public static native int waitFor(int processId);
}
/*
* Copyright (C) 2012-2013 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 eu.chainfire.libsuperuser;
import android.content.Context;
import android.os.Handler;
import android.widget.Toast;
/**
* Base application class to extend from, solving some issues with
* toasts and AsyncTasks you are likely to run into
*/
public class Application extends android.app.Application {
/**
* Shows a toast message
*
* @param context Any context belonging to this application
* @param message The message to show
*/
public static void toast(Context context, String message) {
// this is a static method so it is easier to call,
// as the context checking and casting is done for you
if (context == null) return;
if (!(context instanceof Application)) {
context = context.getApplicationContext();
}
if (context instanceof Application) {
final Context c = context;
final String m = message;
((Application)context).runInApplicationThread(new Runnable() {
@Override
public void run() {
Toast.makeText(c, m, Toast.LENGTH_LONG).show();
}
});
}
}
private static Handler mApplicationHandler = new Handler();
/**
* Run a runnable in the main application thread
*
* @param r Runnable to run
*/
public void runInApplicationThread(Runnable r) {
mApplicationHandler.post(r);
}
@Override
public void onCreate() {
super.onCreate();
try {
// workaround bug in AsyncTask, can show up (for example) when you toast from a service
// this makes sure AsyncTask's internal handler is created from the right (main) thread
Class.forName("android.os.AsyncTask");
} catch (ClassNotFoundException e) {
}
}
}
/*
* Copyright (C) 2012-2013 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 eu.chainfire.libsuperuser;
import android.os.Looper;
import android.util.Log;
import com.github.shadowsocks.BuildConfig;
/**
* Utility class for logging and debug features that (by default) does nothing when not in debug mode
*/
public class Debug {
// ----- DEBUGGING -----
private static boolean debug = BuildConfig.DEBUG;
/**
* <p>Enable or disable debug mode</p>
*
* <p>By default, debug mode is enabled for development
* builds and disabled for exported APKs - see
* BuildConfig.DEBUG</p>
*
* @param enabled Enable debug mode ?
*/
public static void setDebug(boolean enable) {
debug = enable;
}
/**
* <p>Is debug mode enabled ?</p>
*
* @return Debug mode enabled
*/
public static boolean getDebug() {
return debug;
}
// ----- LOGGING -----
public interface OnLogListener {
public void onLog(int type, String typeIndicator, String message);
}
public static final String TAG = "libsuperuser";
public static final int LOG_GENERAL = 0x0001;
public static final int LOG_COMMAND = 0x0002;
public static final int LOG_OUTPUT = 0x0004;
public static final int LOG_NONE = 0x0000;
public static final int LOG_ALL = 0xFFFF;
private static int logTypes = LOG_ALL;
private static OnLogListener logListener = null;
/**
* <p>Log a message (internal)</p>
*
* <p>Current debug and enabled logtypes decide what gets logged -
* even if a custom callback is registered</p>
*
* @param type Type of message to log
* @param typeIndicator String indicator for message type
* @param message The message to log
*/
private static void logCommon(int type, String typeIndicator, String message) {
if (debug && ((logTypes & type) == type)) {
if (logListener != null) {
logListener.onLog(type, typeIndicator, message);
} else {
Log.d(TAG, "[" + TAG + "][" + typeIndicator + "]" + (!message.startsWith("[") && !message.startsWith(" ") ? " " : "") + message);
}
}
}
/**
* <p>Log a "general" message</p>
*
* <p>These messages are infrequent and mostly occur at startup/shutdown or on error</p>
*
* @param message The message to log
*/
public static void log(String message) {
logCommon(LOG_GENERAL, "G", message);
}
/**
* <p>Log a "per-command" message</p>
*
* <p>This could produce a lot of output if the client runs many commands in the session</p>
*
* @param message The message to log
*/
public static void logCommand(String message) {
logCommon(LOG_COMMAND, "C", message);
}
/**
* <p>Log a line of stdout/stderr output</p>
*
* <p>This could produce a lot of output if the shell commands are noisy</p>
*
* @param message The message to log
*/
public static void logOutput(String message) {
logCommon(LOG_OUTPUT, "O", message);
}
/**
* <p>Enable or disable logging specific types of message</p>
*
* <p>You may | (or) LOG_* constants together. Note that
* debug mode must also be enabled for actual logging to
* occur.</p>
*
* @param type LOG_* constants
* @param enabled Enable or disable
*/
public static void setLogTypeEnabled(int type, boolean enable) {
if (enable) {
logTypes |= type;
} else {
logTypes &= ~type;
}
}
/**
* <p>Is logging for specific types of messages enabled ?</p>
*
* <p>You may | (or) LOG_* constants together, to learn if
* <b>all</b> passed message types are enabled for logging. Note
* that debug mode must also be enabled for actual logging
* to occur.</p>
*
* @param type LOG_* constants
*/
public static boolean getLogTypeEnabled(int type) {
return ((logTypes & type) == type);
}
/**
* <p>Is logging for specific types of messages enabled ?</p>
*
* <p>You may | (or) LOG_* constants together, to learn if
* <b>all</b> message types are enabled for logging. Takes
* debug mode into account for the result.</p>
*
* @param type LOG_* constants
*/
public static boolean getLogTypeEnabledEffective(int type) {
return getDebug() && getLogTypeEnabled(type);
}
/**
* <p>Register a custom log handler</p>
*
* <p>Replaces the log method (write to logcat) with your own
* handler. Whether your handler gets called is still dependent
* on debug mode and message types being enabled for logging.</p>
*
* @param onLogListener Custom log listener or NULL to revert to default
*/
public static void setOnLogListener(OnLogListener onLogListener) {
logListener = onLogListener;
}
/**
* <p>Get the currently registered custom log handler</p>
*
* @return Current custom log handler or NULL if none is present
*/
public static OnLogListener getOnLogListener() {
return logListener;
}
// ----- SANITY CHECKS -----
private static boolean sanityChecks = true;
/**
* <p>Enable or disable sanity checks</p>
*
* <p>Enables or disables the library crashing when su is called
* from the main thread.</p>
*
* @param enabled Enable or disable
*/
public static void setSanityChecksEnabled(boolean enable) {
sanityChecks = enable;
}
/**
* <p>Are sanity checks enabled ?</p>
*
* <p>Note that debug mode must also be enabled for actual
* sanity checks to occur.</p>
*
* @return True if enabled
*/
public static boolean getSanityChecksEnabled() {
return sanityChecks;
}
/**
* <p>Are sanity checks enabled ?</p>
*
* <p>Takes debug mode into account for the result.</p>
*
* @return True if enabled
*/
public static boolean getSanityChecksEnabledEffective() {
return getDebug() && getSanityChecksEnabled();
}
/**
* <p>Are we running on the main thread ?</p>
*
* @return Running on main thread ?
*/
public static boolean onMainThread() {
return ((Looper.myLooper() != null) && (Looper.myLooper() == Looper.getMainLooper()));
}
}
/*
* Copyright (C) 2012-2013 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 eu.chainfire.libsuperuser;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import android.os.Handler;
import android.os.Looper;
import eu.chainfire.libsuperuser.StreamGobbler.OnLineListener;
/**
* Class providing functionality to execute commands in a (root) shell
*/
public class Shell {
/**
* <p>Runs commands using the supplied shell, and returns the output, or null in
* case of errors.</p>
*
* <p>This method is deprecated and only provided for backwards compatibility.
* Use {@link #run(String, String[], String[], boolean)} instead, and see that
* same method for usage notes.</p>
*
* @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
*/
@Deprecated
public static List<String> run(String shell, String[] commands, boolean wantSTDERR) {
return run(shell, commands, null, wantSTDERR);
}
/**
* <p>Runs commands using the supplied shell, and returns the output, or null in
* case of errors.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>When in debug mode, the code will also excessively log the commands passed to
* and the output returned from the shell.</p>
*
* <p>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&lt;String&gt;, and as such doing something like <em>'ls -lR /'</em>
* will probably have you run out of memory.</p>
*
* @param shell The shell to use for executing the commands
* @param commands The commands to execute
* @param environment List of all environment variables (in 'key=value' format) or null for defaults
* @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, String[] environment, boolean wantSTDERR) {
String shellUpper = shell.toUpperCase(Locale.ENGLISH);
if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) {
// 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.
Debug.log(ShellOnMainThreadException.EXCEPTION_COMMAND);
throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_COMMAND);
}
Debug.logCommand(String.format("[%s%%] START", shellUpper));
List<String> res = Collections.synchronizedList(new ArrayList<String>());
try {
// Combine passed environment with system environment
if (environment != null) {
Map<String, String> newEnvironment = new HashMap<String, String>();
newEnvironment.putAll(System.getenv());
int split;
for (String entry : environment) {
if ((split = entry.indexOf("=")) >= 0) {
newEnvironment.put(entry.substring(0, split), entry.substring(split + 1));
}
}
int i = 0;
environment = new String[newEnvironment.size()];
for (Map.Entry<String, String> entry : newEnvironment.entrySet()) {
environment[i] = entry.getKey() + "=" + entry.getValue();
i++;
}
}
// setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers
Process process = Runtime.getRuntime().exec(shell, environment);
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) {
Debug.logCommand(String.format("[%s+] %s", shellUpper, write));
STDIN.write((write + "\n").getBytes("UTF-8"));
STDIN.flush();
}
STDIN.write("exit\n".getBytes("UTF-8"));
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 (SU.isSU(shell) && (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;
}
Debug.logCommand(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH)));
return res;
}
protected static String[] availableTestCommands = new String[] {
"echo -BOC-",
"id"
};
/**
* See if the shell is alive, and if so, check the UID
*
* @param ret Standard output from running availableTestCommands
* @param checkForRoot true if we are expecting this shell to be running as root
* @return true on success, false on error
*/
protected static boolean parseAvailableResult(List<String> ret, boolean checkForRoot) {
if (ret == null) return false;
// this is only one of many ways this can be done
boolean echo_seen = false;
for (String line : ret) {
if (line.contains("uid=")) {
// id command is working, let's see if we are actually root
return !checkForRoot || line.contains("uid=0");
} else if (line.contains("-BOC-")) {
// if we end up here, at least the su command starts some kind of shell,
// let's hope it has root privileges - no way to know without additional
// native binaries
echo_seen = true;
}
}
return echo_seen;
}
/**
* 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 }, null, 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()]), null, 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(String[] commands) {
return Shell.run("sh", commands, null, 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 }, null, false);
}
/**
* Runs commands as root (if available) and return output
*
* @param commands 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()]), null, false);
}
/**
* Runs commands as root (if available) and return output
*
* @param commands 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, null, 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(Shell.availableTestCommands);
return Shell.parseAvailableResult(ret, true);
}
/**
* <p>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.</p>
*
* <p>Note that su binary version and GUI (APK) version can be completely different.</p>
*
* @param internal Request human-readable version or application internal version
* @return String containing the su version or null
*/
public static String version(boolean internal) {
List<String> ret = Shell.run(
internal ? "su -V" : "su -v",
new String[] { },
null,
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;
}
/**
* Attempts to deduce if the shell command refers to a su shell
*
* @param shell Shell command to run
* @return Shell command appears to be su
*/
public static boolean isSU(String shell) {
// Strip parameters
int pos = shell.indexOf(' ');
if (pos >= 0) {
shell = shell.substring(0, pos);
}
// Strip path
pos = shell.lastIndexOf('/');
if (pos >= 0) {
shell = shell.substring(pos + 1);
}
return shell.equals("su");
}
/**
* Constructs a shell command to start a su shell using the supplied
* uid and SELinux context. This is can be an expensive operation,
* consider caching the result.
*
* @param uid Uid to use (0 == root)
* @param context (SELinux) context name to use or null
* @return Shell command
*/
public static String shell(int uid, String context) {
// su[ --context <context>][ <uid>]
String shell = "su";
// First known firmware with SELinux built-in was a 4.2 (17) leak
if ((context != null) && (android.os.Build.VERSION.SDK_INT >= 17)) {
Boolean enforcing = null;
// Detect enforcing through sysfs, not always present
if (enforcing == null) {
File f = new File("/sys/fs/selinux/enforce");
if (f.exists()) {
try {
InputStream is = new FileInputStream("/sys/fs/selinux/enforce");
try {
enforcing = (is.read() == '1');
} finally {
is.close();
}
} catch (Exception e) {
}
}
}
// 4.4+ builds are enforcing by default, take the gamble
if (enforcing == null) {
enforcing = (android.os.Build.VERSION.SDK_INT >= 19);
}
// Switching to a context in permissive mode is not generally
// useful aside from audit testing, but we want to avoid
// switching the context due to the increased chance of issues.
if (enforcing) {
String display = version(false);
String internal = version(true);
// We only know the format for SuperSU v1.90+ right now
if (
(display != null) &&
(internal != null) &&
(display.endsWith("SUPERSU")) &&
(Integer.valueOf(internal) >= 190)
) {
shell = String.format(Locale.ENGLISH, "%s --context %s", shell, context);
}
}
}
// Most su binaries support the "su <uid>" format, but in case
// they don't, lets skip it for the default 0 (root) case
if (uid > 0) {
shell = String.format(Locale.ENGLISH, "%s %d", shell, uid);
}
return shell;
}
}
/**
* Command result callback, notifies the recipient of the completion of a command
* block, including the (last) exit code, and the full output
*/
public interface OnCommandResultListener {
/**
* <p>Command result callback</p>
*
* <p>Depending on how and on which thread the shell was created, this callback
* may be executed on one of the gobbler threads. In that case, it is important
* the callback returns as quickly as possible, as delays in this callback may
* pause the native process or even result in a deadlock</p>
*
* <p>See {@link Shell.Interactive} for threading details</p>
*
* @param commandCode Value previously supplied to addCommand
* @param exitCode Exit code of the last command in the block
* @param output All output generated by the command block
*/
public void onCommandResult(int commandCode, int exitCode, List<String> output);
// for any onCommandResult callback
public static final int WATCHDOG_EXIT = -1;
public static final int SHELL_DIED = -2;
// for Interactive.open() callbacks only
public static final int SHELL_EXEC_FAILED = -3;
public static final int SHELL_WRONG_UID = -4;
public static final int SHELL_RUNNING = 0;
}
/**
* Internal class to store command block properties
*/
private static class Command {
private static int commandCounter = 0;
private final String[] commands;
private final int code;
private final OnCommandResultListener onCommandResultListener;
private final String marker;
public Command(String[] commands, int code, OnCommandResultListener onCommandResultListener) {
this.commands = commands;
this.code = code;
this.onCommandResultListener = onCommandResultListener;
this.marker = UUID.randomUUID().toString() + String.format("-%08x", ++commandCounter);
}
}
/**
* Builder class for {@link Shell.Interactive}
*/
public static class Builder {
private Handler handler = null;
private boolean autoHandler = true;
private String shell = "sh";
private boolean wantSTDERR = false;
private List<Command> commands = new LinkedList<Command>();
private Map<String, String> environment = new HashMap<String, String>();
private OnLineListener onSTDOUTLineListener = null;
private OnLineListener onSTDERRLineListener = null;
private int watchdogTimeout = 0;
/**
* <p>Set a custom handler that will be used to post all callbacks to</p>
*
* <p>See {@link Shell.Interactive} for further details on threading and handlers</p>
*
* @param handler Handler to use
* @return This Builder object for method chaining
*/
public Builder setHandler(Handler handler) { this.handler = handler; return this; }
/**
* <p>Automatically create a handler if possible ? Default to true</p>
*
* <p>See {@link Shell.Interactive} for further details on threading and handlers</p>
*
* @param autoHandler Auto-create handler ?
* @return This Builder object for method chaining
*/
public Builder setAutoHandler(boolean autoHandler) { this.autoHandler = autoHandler; return this; }
/**
* Set shell binary to use. Usually "sh" or "su", do not use a full path
* unless you have a good reason to
*
* @param shell Shell to use
* @return This Builder object for method chaining
*/
public Builder setShell(String shell) { this.shell = shell; return this; }
/**
* Convenience function to set "sh" as used shell
*
* @return This Builder object for method chaining
*/
public Builder useSH() { return setShell("sh"); }
/**
* Convenience function to set "su" as used shell
*
* @return This Builder object for method chaining
*/
public Builder useSU() { return setShell("su"); }
/**
* Set if error output should be appended to command block result output
*
* @param wantSTDERR Want error output ?
* @return This Builder object for method chaining
*/
public Builder setWantSTDERR(boolean wantSTDERR) { this.wantSTDERR = wantSTDERR; return this; }
/**
* Add or update an environment variable
*
* @param key Key of the environment variable
* @param value Value of the environment variable
* @return This Builder object for method chaining
*/
public Builder addEnvironment(String key, String value) { environment.put(key, value); return this; }
/**
* Add or update environment variables
*
* @param addEnvironment Map of environment variables
* @return This Builder object for method chaining
*/
public Builder addEnvironment(Map<String, String> addEnvironment) { environment.putAll(addEnvironment); return this; }
/**
* Add a command to execute
*
* @param command Command to execute
* @return This Builder object for method chaining
*/
public Builder addCommand(String command) { return addCommand(command, 0, null); }
/**
* <p>Add a command to execute, with a callback to be called on completion</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param command Command to execute
* @param code User-defined value passed back to the callback
* @param onCommandResultListener Callback to be called on completion
* @return This Builder object for method chaining
*/
public Builder addCommand(String command, int code, OnCommandResultListener onCommandResultListener) { return addCommand(new String[] { command }, code, onCommandResultListener); }
/**
* Add commands to execute
*
* @param commands Commands to execute
* @return This Builder object for method chaining
*/
public Builder addCommand(List<String> commands) { return addCommand(commands, 0, null); }
/**
* <p>Add commands to execute, with a callback to be called on completion (of all commands)</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param commands Commands to execute
* @param code User-defined value passed back to the callback
* @param onCommandResultListener Callback to be called on completion (of all commands)
* @return This Builder object for method chaining
*/
public Builder addCommand(List<String> commands, int code, OnCommandResultListener onCommandResultListener) { return addCommand(commands.toArray(new String[commands.size()]), code, onCommandResultListener); }
/**
* Add commands to execute
*
* @param commands Commands to execute
* @return This Builder object for method chaining
*/
public Builder addCommand(String[] commands) { return addCommand(commands, 0, null); }
/**
* <p>Add commands to execute, with a callback to be called on completion (of all commands)</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param commands Commands to execute
* @param code User-defined value passed back to the callback
* @param onCommandResultListener Callback to be called on completion (of all commands)
* @return This Builder object for method chaining
*/
public Builder addCommand(String[] commands, int code, OnCommandResultListener onCommandResultListener) { this.commands.add(new Command(commands, code, onCommandResultListener)); return this; }
/**
* <p>Set a callback called for every line output to STDOUT by the shell</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param onLineListener Callback to be called for each line
* @return This Builder object for method chaining
*/
public Builder setOnSTDOUTLineListener(OnLineListener onLineListener) { this.onSTDOUTLineListener = onLineListener; return this; }
/**
* <p>Set a callback called for every line output to STDERR by the shell</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param onLineListener Callback to be called for each line
* @return This Builder object for method chaining
*/
public Builder setOnSTDERRLineListener(OnLineListener onLineListener) { this.onSTDERRLineListener = onLineListener; return this; }
/**
* <p>Enable command timeout callback</p>
*
* <p>This will invoke the onCommandResult() callback with exitCode WATCHDOG_EXIT if a command takes longer than watchdogTimeout
* seconds to complete.</p>
*
* <p>If a watchdog timeout occurs, it generally means that the Interactive session is out of sync with the shell process. The
* caller should close the current session and open a new one.</p>
*
* @param watchdogTimeout Timeout, in seconds; 0 to disable
* @return This Builder object for method chaining
*/
public Builder setWatchdogTimeout(int watchdogTimeout) { this.watchdogTimeout = watchdogTimeout; return this; }
/**
* <p>Enable/disable reduced logcat output</p>
*
* <p>Note that this is a global setting</p>
*
* @param useMinimal true for reduced output, false for full output
* @return This Builder object for method chaining
*/
public Builder setMinimalLogging(boolean useMinimal) {
Debug.setLogTypeEnabled(Debug.LOG_COMMAND | Debug.LOG_OUTPUT, !useMinimal);
return this;
}
/**
* Construct a {@link Shell.Interactive} instance, and start the shell
*/
public Interactive open() { return new Interactive(this, null); }
/**
* Construct a {@link Shell.Interactive} instance, try to start the shell, and
* call onCommandResultListener to report success or failure
*
* @param onCommandResultListener Callback to return shell open status
*/
public Interactive open(OnCommandResultListener onCommandResultListener) {
return new Interactive(this, onCommandResultListener);
}
}
/**
* <p>An interactive shell - initially created with {@link Shell.Builder} - that
* executes blocks of commands you supply in the background, optionally calling
* callbacks as each block completes.</p>
*
* <p>STDERR output can be supplied as well, but 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.</p>
*
* <p>Note as well that the close() and waitForIdle() methods will intentionally
* crash when run in debug mode from the main thread of the application. Any blocking
* call should be run from a background thread.</p>
*
* <p>When in debug mode, the code will also excessively log the commands passed to
* and the output returned from the shell.</p>
*
* <p>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&lt;String&gt;, and as such doing something like <em>'ls -lR /'</em>
* will probably have you run out of memory when using a
* {@link Shell.OnCommandResultListener}. A work-around is to not supply this callback,
* but using (only) {@link Shell.Builder#setOnSTDOUTLineListener(OnLineListener)}. This
* way, an internal buffer will not be created and wasting your memory.</p>
*
* <h3>Callbacks, threads and handlers</h3>
*
* <p>On which thread the callbacks execute is dependent on your initialization. You can
* supply a custom Handler using {@link Shell.Builder#setHandler(Handler)} if needed.
* If you do not supply a custom Handler - unless you set {@link Shell.Builder#setAutoHandler(boolean)}
* to false - a Handler will be auto-created if the thread used for instantiation
* of the object has a Looper.</p>
*
* <p>If no Handler was supplied and it was also not auto-created, all callbacks will
* be called from either the STDOUT or STDERR gobbler threads. These are important
* threads that should be blocked as little as possible, as blocking them may in rare
* cases pause the native process or even create a deadlock.</p>
*
* <p>The main thread must certainly have a Looper, thus if you call {@link Shell.Builder#open()}
* from the main thread, a handler will (by default) be auto-created, and all the callbacks
* will be called on the main thread. While this is often convenient and easy to code with,
* you should be aware that if your callbacks are 'expensive' to execute, this may negatively
* impact UI performance.</p>
*
* <p>Background threads usually do <em>not</em> have a Looper, so calling {@link Shell.Builder#open()}
* from such a background thread will (by default) result in all the callbacks being executed
* in one of the gobbler threads. You will have to make sure the code you execute in these callbacks
* is thread-safe.</p>
*/
public static class Interactive {
private final Handler handler;
private final boolean autoHandler;
private final String shell;
private final boolean wantSTDERR;
private final List<Command> commands;
private final Map<String, String> environment;
private final OnLineListener onSTDOUTLineListener;
private final OnLineListener onSTDERRLineListener;
private int watchdogTimeout;
private Process process = null;
private DataOutputStream STDIN = null;
private StreamGobbler STDOUT = null;
private StreamGobbler STDERR = null;
private ScheduledThreadPoolExecutor watchdog = null;
private volatile boolean running = false;
private volatile boolean idle = true; // read/write only synchronized
private volatile boolean closed = true;
private volatile int callbacks = 0;
private volatile int watchdogCount;
private Object idleSync = new Object();
private Object callbackSync = new Object();
private volatile int lastExitCode = 0;
private volatile String lastMarkerSTDOUT = null;
private volatile String lastMarkerSTDERR = null;
private volatile Command command = null;
private volatile List<String> buffer = null;
/**
* The only way to create an instance: Shell.Builder::open()
*
* @param builder Builder class to take values from
*/
private Interactive(final Builder builder, final OnCommandResultListener onCommandResultListener) {
autoHandler = builder.autoHandler;
shell = builder.shell;
wantSTDERR = builder.wantSTDERR;
commands = builder.commands;
environment = builder.environment;
onSTDOUTLineListener = builder.onSTDOUTLineListener;
onSTDERRLineListener = builder.onSTDERRLineListener;
watchdogTimeout = builder.watchdogTimeout;
// If a looper is available, we offload the callbacks from the gobbling threads
// to whichever thread created us. Would normally do this in open(),
// but then we could not declare handler as final
if ((Looper.myLooper() != null) && (builder.handler == null) && autoHandler) {
handler = new Handler();
} else {
handler = builder.handler;
}
boolean ret = open();
if (onCommandResultListener == null) {
return;
} else if (ret == false) {
onCommandResultListener.onCommandResult(0, OnCommandResultListener.SHELL_EXEC_FAILED, null);
return;
}
// Allow up to 60 seconds for SuperSU/Superuser dialog, then enable the user-specified
// timeout for all subsequent operations
watchdogTimeout = 60;
addCommand(Shell.availableTestCommands, 0, new OnCommandResultListener() {
public void onCommandResult(int commandCode, int exitCode, List<String> output) {
if (exitCode == OnCommandResultListener.SHELL_RUNNING &&
Shell.parseAvailableResult(output, Shell.SU.isSU(shell)) != true) {
// shell is up, but it's brain-damaged
exitCode = OnCommandResultListener.SHELL_WRONG_UID;
}
watchdogTimeout = builder.watchdogTimeout;
onCommandResultListener.onCommandResult(0, exitCode, output);
}
});
}
@Override
protected void finalize() throws Throwable {
if (!closed && Debug.getSanityChecksEnabledEffective()) {
// waste of resources
Debug.log(ShellNotClosedException.EXCEPTION_NOT_CLOSED);
throw new ShellNotClosedException();
}
super.finalize();
}
/**
* Add a command to execute
*
* @param command Command to execute
*/
public void addCommand(String command) { addCommand(command, 0, null); }
/**
* <p>Add a command to execute, with a callback to be called on completion</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param command Command to execute
* @param code User-defined value passed back to the callback
* @param onCommandResultListener Callback to be called on completion
*/
public void addCommand(String command, int code, OnCommandResultListener onCommandResultListener) { addCommand(new String[] { command }, code, onCommandResultListener); }
/**
* Add commands to execute
*
* @param commands Commands to execute
*/
public void addCommand(List<String> commands) { addCommand(commands, 0, null); }
/**
* <p>Add commands to execute, with a callback to be called on completion (of all commands)</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param commands Commands to execute
* @param code User-defined value passed back to the callback
* @param onCommandResultListener Callback to be called on completion (of all commands)
*/
public void addCommand(List<String> commands, int code, OnCommandResultListener onCommandResultListener) { addCommand(commands.toArray(new String[commands.size()]), code, onCommandResultListener); }
/**
* Add commands to execute
*
* @param commands Commands to execute
*/
public void addCommand(String[] commands) { addCommand(commands, 0, null); }
/**
* <p>Add commands to execute, with a callback to be called on completion (of all commands)</p>
*
* <p>The thread on which the callback executes is dependent on various factors, see {@link Shell.Interactive} for further details</p>
*
* @param commands Commands to execute
* @param code User-defined value passed back to the callback
* @param onCommandResultListener Callback to be called on completion (of all commands)
*/
public synchronized void addCommand(String[] commands, int code, OnCommandResultListener onCommandResultListener) {
this.commands.add(new Command(commands, code, onCommandResultListener));
runNextCommand();
}
/**
* Run the next command if any and if ready, signals idle state if no commands left
*/
private void runNextCommand() {
runNextCommand(true);
}
/**
* Called from a ScheduledThreadPoolExecutor timer thread every second when there is an outstanding command
*/
private synchronized void handleWatchdog() {
final int exitCode;
if (watchdog == null) return;
if (watchdogTimeout == 0) return;
if (!isRunning()) {
exitCode = OnCommandResultListener.SHELL_DIED;
Debug.log(String.format("[%s%%] SHELL_DIED", shell.toUpperCase(Locale.ENGLISH)));
} else if (watchdogCount++ < watchdogTimeout) {
return;
} else {
exitCode = OnCommandResultListener.WATCHDOG_EXIT;
Debug.log(String.format("[%s%%] WATCHDOG_EXIT", shell.toUpperCase(Locale.ENGLISH)));
}
if (handler != null) {
postCallback(command, exitCode, buffer);
}
// prevent multiple callbacks for the same command
command = null;
buffer = null;
idle = true;
watchdog.shutdown();
watchdog = null;
kill();
}
/**
* Start the periodic timer when a command is submitted
*/
private void startWatchdog() {
if (watchdogTimeout == 0) {
return;
}
watchdogCount = 0;
watchdog = new ScheduledThreadPoolExecutor(1);
watchdog.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
handleWatchdog();
}
}, 1, 1, TimeUnit.SECONDS);
}
/**
* Disable the watchdog timer upon command completion
*/
private void stopWatchdog() {
if (watchdog != null) {
watchdog.shutdownNow();
watchdog = null;
}
}
/**
* Run the next command if any and if ready
*
* @param notifyIdle signals idle state if no commands left ?
*/
private void runNextCommand(boolean notifyIdle) {
// must always be called from a synchronized method
boolean running = isRunning();
if (!running) idle = true;
if (running && idle && (commands.size() > 0)) {
Command command = commands.get(0);
commands.remove(0);
buffer = null;
lastExitCode = 0;
lastMarkerSTDOUT = null;
lastMarkerSTDERR = null;
if (command.commands.length > 0) {
try {
if (command.onCommandResultListener != null) {
// no reason to store the output if we don't have an OnCommandResultListener
// user should catch the output with an OnLineListener in this case
buffer = Collections.synchronizedList(new ArrayList<String>());
}
idle = false;
this.command = command;
startWatchdog();
for (String write : command.commands) {
Debug.logCommand(String.format("[%s+] %s", shell.toUpperCase(Locale.ENGLISH), write));
STDIN.write((write + "\n").getBytes("UTF-8"));
}
STDIN.write(("echo " + command.marker + " $?\n").getBytes("UTF-8"));
STDIN.write(("echo " + command.marker + " >&2\n").getBytes("UTF-8"));
STDIN.flush();
} catch (IOException e) {
}
} else {
runNextCommand(false);
}
} else if (!running) {
// our shell died for unknown reasons - abort all submissions
while (commands.size() > 0) {
postCallback(commands.remove(0), OnCommandResultListener.SHELL_DIED, null);
}
}
if (idle && notifyIdle) {
synchronized(idleSync) {
idleSync.notifyAll();
}
}
}
/**
* Processes a STDOUT/STDERR line containing an end/exitCode marker
*/
private synchronized void processMarker() {
if (command.marker.equals(lastMarkerSTDOUT) && (command.marker.equals(lastMarkerSTDERR))) {
if (buffer != null) {
postCallback(command, lastExitCode, buffer);
}
stopWatchdog();
command = null;
buffer = null;
idle = true;
runNextCommand();
}
}
/**
* Process a normal STDOUT/STDERR line
*
* @param line Line to process
* @param listener Callback to call or null
*/
private synchronized void processLine(String line, OnLineListener listener) {
if (listener != null) {
if (handler != null) {
final String fLine = line;
final OnLineListener fListener = listener;
startCallback();
handler.post(new Runnable() {
@Override
public void run() {
try {
fListener.onLine(fLine);
} finally {
endCallback();
}
}
});
} else {
listener.onLine(line);
}
}
}
/**
* Add line to internal buffer
*
* @param line Line to add
*/
private synchronized void addBuffer(String line) {
if (buffer != null) {
buffer.add(line);
}
}
/**
* Increase callback counter
*/
private void startCallback() {
synchronized (callbackSync) {
callbacks++;
}
}
/**
* Schedule a callback to run on the appropriate thread
*/
private void postCallback(final Command fCommand, final int fExitCode, final List<String> fOutput) {
if (fCommand.onCommandResultListener == null) {
return;
}
if (handler == null) {
fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode, fOutput);
return;
}
startCallback();
handler.post(new Runnable() {
@Override
public void run() {
try {
fCommand.onCommandResultListener.onCommandResult(fCommand.code, fExitCode, fOutput);
} finally {
endCallback();
}
}
});
}
/**
* Decrease callback counter, signals callback complete state when dropped to 0
*/
private void endCallback() {
synchronized (callbackSync) {
callbacks--;
if (callbacks == 0) {
callbackSync.notifyAll();
}
}
}
/**
* Internal call that launches the shell, starts gobbling, and starts executing commands.
* See {@link Shell.Interactive}
*
* @return Opened successfully ?
*/
private synchronized boolean open() {
Debug.log(String.format("[%s%%] START", shell.toUpperCase(Locale.ENGLISH)));
try {
// setup our process, retrieve STDIN stream, and STDOUT/STDERR gobblers
if (environment.size() == 0) {
process = Runtime.getRuntime().exec(shell);
} else {
Map<String, String> newEnvironment = new HashMap<String, String>();
newEnvironment.putAll(System.getenv());
newEnvironment.putAll(environment);
int i = 0;
String[] env = new String[newEnvironment.size()];
for (Map.Entry<String, String> entry : newEnvironment.entrySet()) {
env[i] = entry.getKey() + "=" + entry.getValue();
i++;
}
process = Runtime.getRuntime().exec(shell, env);
}
STDIN = new DataOutputStream(process.getOutputStream());
STDOUT = new StreamGobbler(shell.toUpperCase(Locale.ENGLISH) + "-", process.getInputStream(), new OnLineListener() {
@Override
public void onLine(String line) {
synchronized (Interactive.this) {
if (command == null) {
return;
}
if (line.startsWith(command.marker)) {
try {
lastExitCode = Integer.valueOf(line.substring(command.marker.length() + 1), 10);
} catch (Exception e) {
}
lastMarkerSTDOUT = command.marker;
processMarker();
} else {
addBuffer(line);
processLine(line, onSTDOUTLineListener);
}
}
}
});
STDERR = new StreamGobbler(shell.toUpperCase(Locale.ENGLISH) + "*", process.getErrorStream(), new OnLineListener() {
@Override
public void onLine(String line) {
synchronized (Interactive.this) {
if (command == null) {
return;
}
if (line.startsWith(command.marker)) {
lastMarkerSTDERR = command.marker;
processMarker();
} else {
if (wantSTDERR) addBuffer(line);
processLine(line, onSTDERRLineListener);
}
}
}
});
// start gobbling and write our commands to the shell
STDOUT.start();
STDERR.start();
running = true;
closed = false;
runNextCommand();
return true;
} catch (IOException e) {
// shell probably not found
return false;
}
}
/**
* Close shell and clean up all resources. Call this when you are done with the shell.
* If the shell is not idle (all commands completed) you should not call this method
* from the main UI thread because it may block for a long time. This method will
* intentionally crash your app (if in debug mode) if you try to do this anyway.
*/
public void close() {
boolean _idle = isIdle(); // idle must be checked synchronized
synchronized (this) {
if (!running) return;
running = false;
closed = true;
}
// This method should not be called from the main thread unless the shell is idle
// and can be cleaned up with (minimal) waiting. Only throw in debug mode.
if (!_idle && Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) {
Debug.log(ShellOnMainThreadException.EXCEPTION_NOT_IDLE);
throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_NOT_IDLE);
}
if (!_idle) waitForIdle();
try {
STDIN.write(("exit\n").getBytes("UTF-8"));
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();
stopWatchdog();
process.destroy();
} catch (IOException e) {
// shell probably not found
} catch (InterruptedException e) {
// this should really be re-thrown
}
Debug.log(String.format("[%s%%] END", shell.toUpperCase(Locale.ENGLISH)));
}
/**
* Try to clean up as much as possible from a shell that's gotten itself wedged.
* Hopefully the StreamGobblers will croak on their own when the other side of
* the pipe is closed.
*/
public synchronized void kill() {
running = false;
closed = true;
try {
STDIN.close();
} catch (IOException e) {
}
try {
process.destroy();
} catch (Exception e) {
}
}
/**
* Is our shell still running ?
*
* @return Shell running ?
*/
public boolean isRunning() {
try {
// if this throws, we're still running
process.exitValue();
return false;
} catch (IllegalThreadStateException e) {
}
return true;
}
/**
* Have all commands completed executing ?
*
* @return Shell idle ?
*/
public synchronized boolean isIdle() {
if (!isRunning()) {
idle = true;
synchronized(idleSync) {
idleSync.notifyAll();
}
}
return idle;
}
/**
* <p>Wait for idle state. As this is a blocking call, you should not call it from the main UI thread.
* If you do so and debug mode is enabled, this method will intentionally crash your app.</p>
*
* <p>If not interrupted, this method will not return until all commands have finished executing.
* Note that this does not necessarily mean that all the callbacks have fired yet.</p>
*
* <p>If no Handler is used, all callbacks will have been executed when this method returns. If
* a Handler is used, and this method is called from a different thread than associated with the
* Handler's Looper, all callbacks will have been executed when this method returns as well.
* If however a Handler is used but this method is called from the same thread as associated
* with the Handler's Looper, there is no way to know.</p>
*
* <p>In practice this means that in most simple cases all callbacks will have completed when this
* method returns, but if you actually depend on this behavior, you should make certain this is
* indeed the case.</p>
*
* <p>See {@link Shell.Interactive} for further details on threading and handlers</p>
*
* @return True if wait complete, false if wait interrupted
*/
public boolean waitForIdle() {
if (Debug.getSanityChecksEnabledEffective() && Debug.onMainThread()) {
Debug.log(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE);
throw new ShellOnMainThreadException(ShellOnMainThreadException.EXCEPTION_WAIT_IDLE);
}
if (isRunning()) {
synchronized (idleSync) {
while (!idle) {
try {
idleSync.wait();
} catch (InterruptedException e) {
return false;
}
}
}
if (
(handler != null) &&
(handler.getLooper() != null) &&
(handler.getLooper() != Looper.myLooper())
) {
// If the callbacks are posted to a different thread than this one, we can wait until
// all callbacks have called before returning. If we don't use a Handler at all,
// the callbacks are already called before we get here. If we do use a Handler but
// we use the same Looper, waiting here would actually block the callbacks from being
// called
synchronized (callbackSync) {
while (callbacks > 0) {
try {
callbackSync.wait();
} catch (InterruptedException e) {
return false;
}
}
}
}
}
return true;
}
/**
* Are we using a Handler to post callbacks ?
*
* @return Handler used ?
*/
public boolean hasHandler() {
return (handler != null);
}
}
}
/*
* Copyright (C) 2012-2013 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 eu.chainfire.libsuperuser;
/**
* Exception class used to notify developer that a shell was not close()d
*/
@SuppressWarnings("serial")
public class ShellNotClosedException extends RuntimeException {
public static final String EXCEPTION_NOT_CLOSED = "Application did not close() interactive shell";
public ShellNotClosedException() {
super(EXCEPTION_NOT_CLOSED);
}
}
/*
* Copyright (C) 2012-2013 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 eu.chainfire.libsuperuser;
/**
* Exception class used to crash application when shell commands are executed
* from the main thread, and we are in debug mode.
*/
@SuppressWarnings("serial")
public class ShellOnMainThreadException extends RuntimeException {
public static final String EXCEPTION_COMMAND = "Application attempted to run a shell command from the main thread";
public static final String EXCEPTION_NOT_IDLE = "Application attempted to wait for a non-idle shell to close on the main thread";
public static final String EXCEPTION_WAIT_IDLE = "Application attempted to wait for a shell to become idle on the main thread";
public ShellOnMainThreadException(String message) {
super(message);
}
}
/*
* Copyright (C) 2012-2013 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 eu.chainfire.libsuperuser;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
/**
* Thread utility class continuously reading from an InputStream
*/
public class StreamGobbler extends Thread {
/**
* Line callback interface
*/
public interface OnLineListener {
/**
* <p>Line callback</p>
*
* <p>This callback should process the line as quickly as possible.
* Delays in this callback may pause the native process or even
* result in a deadlock</p>
*
* @param line String that was gobbled
*/
public void onLine(String line);
}
private String shell = null;
private BufferedReader reader = null;
private List<String> writer = null;
private OnLineListener listener = null;
/**
* <p>StreamGobbler constructor</p>
*
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
* possible to prevent a deadlock from occurring, or Process.waitFor() never
* returning (as the buffer is full, pausing the native process)</p>
*
* @param shell Name of the shell
* @param inputStream InputStream to read from
* @param outputList List<String> to write to, or null
*/
public StreamGobbler(String shell, InputStream inputStream, List<String> outputList) {
this.shell = shell;
reader = new BufferedReader(new InputStreamReader(inputStream));
writer = outputList;
}
/**
* <p>StreamGobbler constructor</p>
*
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
* possible to prevent a deadlock from occurring, or Process.waitFor() never
* returning (as the buffer is full, pausing the native process)</p>
*
* @param shell Name of the shell
* @param inputStream InputStream to read from
* @param onLineListener OnLineListener callback
*/
public StreamGobbler(String shell, InputStream inputStream, OnLineListener onLineListener) {
this.shell = shell;
reader = new BufferedReader(new InputStreamReader(inputStream));
listener = onLineListener;
}
@Override
public void run() {
// keep reading the InputStream until it ends (or an error occurs)
try {
String line = null;
while ((line = reader.readLine()) != null) {
Debug.logOutput(String.format("[%s] %s", shell, line));
if (writer != null) writer.add(line);
if (listener != null) listener.onLine(line);
}
} catch (IOException e) {
}
// make sure our stream is closed and resources will be freed
try {
reader.close();
} catch (IOException e) {
}
}
}
......@@ -130,21 +130,6 @@ LOCAL_LDLIBS := -llog
include $(BUILD_EXECUTABLE)
########################################################
## termExec
########################################################
include $(CLEAR_VARS)
LOCAL_MODULE:= exec
LOCAL_SRC_FILES:= \
termExec.cpp
LOCAL_LDLIBS := -ldl -llog
include $(BUILD_SHARED_LIBRARY)
########################################################
## system
########################################################
......
/*
* Copyright (C) 2008 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.
*/
/*
* Copyright (C) 2007 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.
*/
#define LOG_TAG "Shadowsocks"
#include "jni.h"
#include <android/log.h>
#define LOGI(...) do { __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__); } while(0)
#define LOGW(...) do { __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); } while(0)
#define LOGE(...) do { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); } while(0)
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <signal.h>
#include <stdio.h>
static jclass class_fileDescriptor;
static jfieldID field_fileDescriptor_descriptor;
static jmethodID method_fileDescriptor_init;
typedef unsigned short char16_t;
class String8 {
public:
String8() {
mString = 0;
}
~String8() {
if (mString) {
free(mString);
}
}
void set(const char16_t* o, size_t numChars) {
if (mString) {
free(mString);
}
mString = (char*) malloc(numChars + 1);
if (!mString) {
return;
}
for (size_t i = 0; i < numChars; i++) {
mString[i] = (char) o[i];
}
mString[numChars] = '\0';
}
const char* string() {
return mString;
}
private:
char* mString;
};
static int throwOutOfMemoryError(JNIEnv *env, const char *message)
{
jclass exClass;
const char *className = "java/lang/OutOfMemoryError";
exClass = env->FindClass(className);
return env->ThrowNew(exClass, message);
}
static int create_subprocess(const int rdt, const char *cmd, char *const argv[],
char *const envp[], const char* scripts, int* pProcessId)
{
pid_t pid;
int pfds[2];
int pfds2[2];
pipe(pfds);
if (rdt) {
pipe(pfds2);
}
pid = fork();
if(pid < 0) {
LOGE("- fork failed: %s -\n", strerror(errno));
return -1;
}
if(pid == 0){
signal(SIGPIPE, SIG_IGN);
if (envp) {
for (; *envp; ++envp) {
putenv(*envp);
}
}
dup2(pfds[0], 0);
close(pfds[1]);
if (rdt) {
close(1);
close(2);
dup2(pfds2[1], 1);
dup2(pfds2[1], 2);
close(pfds2[0]);
}
execv(cmd, argv);
fflush(NULL);
exit(0);
} else {
signal(SIGPIPE, SIG_IGN);
*pProcessId = (int) pid;
dup2(pfds[1], 1);
close(pfds[0]);
write(pfds[1], scripts, strlen(scripts)+1);
if (rdt) {
close(pfds2[1]);
return pfds2[0];
} else {
return -1;
}
}
}
static jobject android_os_Exec_createSubProcess(JNIEnv *env, jobject clazz,
jint rdt, jstring cmd, jobjectArray args, jobjectArray envVars, jstring scripts,
jintArray processIdArray)
{
const jchar* str = cmd ? env->GetStringCritical(cmd, 0) : 0;
String8 cmd_8;
if (str) {
cmd_8.set(str, env->GetStringLength(cmd));
env->ReleaseStringCritical(cmd, str);
}
const jchar* str_scripts = scripts ? env->GetStringCritical(scripts, 0) : 0;
String8 scripts_8;
if (str_scripts) {
scripts_8.set(str_scripts, env->GetStringLength(scripts));
env->ReleaseStringCritical(scripts, str_scripts);
}
jsize size = args ? env->GetArrayLength(args) : 0;
char **argv = NULL;
String8 tmp_8;
if (size > 0) {
argv = (char **)malloc((size+1)*sizeof(char *));
if (!argv) {
throwOutOfMemoryError(env, "Couldn't allocate argv array");
return NULL;
}
for (int i = 0; i < size; ++i) {
jstring arg = reinterpret_cast<jstring>(env->GetObjectArrayElement(args, i));
str = env->GetStringCritical(arg, 0);
if (!str) {
throwOutOfMemoryError(env, "Couldn't get argument from array");
return NULL;
}
tmp_8.set(str, env->GetStringLength(arg));
env->ReleaseStringCritical(arg, str);
argv[i] = strdup(tmp_8.string());
}
argv[size] = NULL;
}
size = envVars ? env->GetArrayLength(envVars) : 0;
char **envp = NULL;
if (size > 0) {
envp = (char **)malloc((size+1)*sizeof(char *));
if (!envp) {
throwOutOfMemoryError(env, "Couldn't allocate envp array");
return NULL;
}
for (int i = 0; i < size; ++i) {
jstring var = reinterpret_cast<jstring>(env->GetObjectArrayElement(envVars, i));
str = env->GetStringCritical(var, 0);
if (!str) {
throwOutOfMemoryError(env, "Couldn't get env var from array");
return NULL;
}
tmp_8.set(str, env->GetStringLength(var));
env->ReleaseStringCritical(var, str);
envp[i] = strdup(tmp_8.string());
}
envp[size] = NULL;
}
int procId;
int ptm = create_subprocess(rdt, cmd_8.string(), argv, envp, scripts_8.string(),
&procId);
if (argv) {
for (char **tmp = argv; *tmp; ++tmp) {
free(*tmp);
}
free(argv);
}
if (envp) {
for (char **tmp = envp; *tmp; ++tmp) {
free(*tmp);
}
free(envp);
}
if (processIdArray) {
int procIdLen = env->GetArrayLength(processIdArray);
if (procIdLen > 0) {
jboolean isCopy;
int* pProcId = (int*) env->GetPrimitiveArrayCritical(processIdArray, &isCopy);
if (pProcId) {
*pProcId = procId;
env->ReleasePrimitiveArrayCritical(processIdArray, pProcId, 0);
}
}
}
jobject result = env->NewObject(class_fileDescriptor, method_fileDescriptor_init);
if (!result) {
LOGE("Couldn't create a FileDescriptor.");
}
else {
env->SetIntField(result, field_fileDescriptor_descriptor, ptm);
}
return result;
}
static int android_os_Exec_waitFor(JNIEnv *env, jobject clazz,
jint procId) {
int status;
waitpid(procId, &status, 0);
int result = 0;
if (WIFEXITED(status)) {
result = WEXITSTATUS(status);
}
return result;
}
static void android_os_Exec_close(JNIEnv *env, jobject clazz, jobject fileDescriptor)
{
int fd;
fd = env->GetIntField(fileDescriptor, field_fileDescriptor_descriptor);
if (env->ExceptionOccurred() != NULL) {
return;
}
close(fd);
}
static void android_os_Exec_hangupProcessGroup(JNIEnv *env, jobject clazz,
jint procId) {
kill(-procId, SIGHUP);
}
static int register_FileDescriptor(JNIEnv *env)
{
jclass localRef_class_fileDescriptor = env->FindClass("java/io/FileDescriptor");
if (localRef_class_fileDescriptor == NULL) {
LOGE("Can't find class java/io/FileDescriptor");
return -1;
}
class_fileDescriptor = (jclass) env->NewGlobalRef(localRef_class_fileDescriptor);
env->DeleteLocalRef(localRef_class_fileDescriptor);
if (class_fileDescriptor == NULL) {
LOGE("Can't get global ref to class java/io/FileDescriptor");
return -1;
}
field_fileDescriptor_descriptor = env->GetFieldID(class_fileDescriptor, "descriptor", "I");
if (field_fileDescriptor_descriptor == NULL) {
LOGE("Can't find FileDescriptor.descriptor");
return -1;
}
method_fileDescriptor_init = env->GetMethodID(class_fileDescriptor, "<init>", "()V");
if (method_fileDescriptor_init == NULL) {
LOGE("Can't find FileDescriptor.init");
return -1;
}
return 0;
}
static const char *classPathName = "com/github/shadowsocks/Exec";
static JNINativeMethod method_table[] = {
{ "createSubprocess", "(ILjava/lang/String;[Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;[I)Ljava/io/FileDescriptor;",
(void*) android_os_Exec_createSubProcess },
{ "waitFor", "(I)I",
(void*) android_os_Exec_waitFor},
{ "close", "(Ljava/io/FileDescriptor;)V",
(void*) android_os_Exec_close},
{ "hangupProcessGroup", "(I)V",
(void*) android_os_Exec_hangupProcessGroup}
};
/*
* Register several native methods for one class.
*/
static int registerNativeMethods(JNIEnv* env, const char* className,
JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
LOGE("Native registration unable to find class '%s'", className);
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
LOGE("RegisterNatives failed for '%s'", className);
return JNI_FALSE;
}
return JNI_TRUE;
}
/*
* Register native methods for all classes we know about.
*
* returns JNI_TRUE on success.
*/
static int registerNatives(JNIEnv* env)
{
if (!registerNativeMethods(env, classPathName, method_table,
sizeof(method_table) / sizeof(method_table[0]))) {
return JNI_FALSE;
}
return JNI_TRUE;
}
// ----------------------------------------------------------------------------
/*
* This is called by the VM when the shared library is first loaded.
*/
typedef union {
JNIEnv* env;
void* venv;
} UnionJNIEnvToVoid;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
UnionJNIEnvToVoid uenv;
uenv.venv = NULL;
jint result = -1;
JNIEnv* env = NULL;
LOGI("JNI_OnLoad");
if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed");
goto bail;
}
env = uenv.env;
if ((result = register_FileDescriptor(env)) < 0) {
LOGE("ERROR: registerFileDescriptor failed");
goto bail;
}
if (registerNatives(env) != JNI_TRUE) {
LOGE("ERROR: registerNatives failed");
goto bail;
}
result = JNI_VERSION_1_4;
bail:
return result;
}
......@@ -49,7 +49,6 @@ import android.view._
import android.widget._
import com.google.analytics.tracking.android.{MapBuilder, EasyTracker}
import de.keyboardsurfer.android.widget.crouton.{Crouton, Style, Configuration}
import java.io._
import java.util.Hashtable
import net.saik0.android.unifiedpreference.UnifiedPreferenceFragment
import net.saik0.android.unifiedpreference.UnifiedSherlockPreferenceActivity
......@@ -63,7 +62,7 @@ import com.google.ads.{AdRequest, AdSize, AdView}
import net.simonvt.menudrawer.MenuDrawer
import com.github.shadowsocks.database._
import scala.collection.mutable.ListBuffer
import scala.collection.mutable.{ArrayBuffer, ListBuffer}
import com.github.shadowsocks.database.Profile
import com.nostra13.universalimageloader.core.download.BaseImageDownloader
import com.github.shadowsocks.preferences.{ProfileEditTextPreference, PasswordEditTextPreference, SummaryEditTextPreference}
......@@ -72,6 +71,11 @@ import com.github.shadowsocks.database.Item
import com.github.shadowsocks.database.Category
import com.google.zxing.integration.android.IntentIntegrator
import com.github.shadowsocks.aidl.{IShadowsocksServiceCallback, IShadowsocksService}
import java.io._
import scala.Some
import com.github.shadowsocks.database.Item
import com.github.shadowsocks.database.Category
import com.github.shadowsocks.utils.Console
class ProfileIconDownloader(context: Context, connectTimeout: Int, readTimeout: Int)
extends BaseImageDownloader(context, connectTimeout, readTimeout) {
......@@ -118,13 +122,17 @@ object Typefaces {
object Shadowsocks {
// Constants
val TAG = "Shadowsocks"
val REQUEST_CONNECT = 1
val PREFS_NAME = "Shadowsocks"
val PROXY_PREFS = Array(Key.profileName, Key.proxy, Key.remotePort, Key.localPort, Key.sitekey,
Key.encMethod)
val FEATRUE_PREFS = Array(Key.isGFWList, Key.isGlobalProxy, Key.proxyedApps, Key.isTrafficStat,
Key.isUdpDns, Key.isAutoConnect)
val TAG = "Shadowsocks"
val REQUEST_CONNECT = 1
val EXECUTABLES = Array(Executable.IPTABLES, Executable.PDNSD, Executable.REDSOCKS,
Executable.SS_LOCAL, Executable.SS_TUNNEL, Executable.TUN2SOCKS)
// Helper functions
def updateListPreference(pref: Preference, value: String) {
......@@ -415,26 +423,28 @@ class Shadowsocks
}
private def crash_recovery() {
val sb = new StringBuilder
sb.append("kill -9 `cat /data/data/com.github.shadowsocks/pdnsd.pid`").append("\n")
sb.append("kill -9 `cat /data/data/com.github.shadowsocks/ss-local.pid`").append("\n")
sb.append("kill -9 `cat /data/data/com.github.shadowsocks/ss-tunnel.pid`").append("\n")
sb.append("kill -9 `cat /data/data/com.github.shadowsocks/tun2socks.pid`").append("\n")
sb.append("killall -9 pdnsd").append("\n")
sb.append("killall -9 ss-local").append("\n")
sb.append("killall -9 ss-tunnel").append("\n")
sb.append("killall -9 tun2socks").append("\n")
sb.append("rm /data/data/com.github.shadowsocks/pdnsd.conf").append("\n")
sb.append("rm /data/data/com.github.shadowsocks/pdnsd.cache").append("\n")
Utils.runCommand(sb.toString())
sb.clear()
sb.append("kill -9 `cat /data/data/com.github.shadowsocks/redsocks.pid`").append("\n")
sb.append("killall -9 redsocks").append("\n")
sb.append("rm /data/data/com.github.shadowsocks/redsocks.conf").append("\n")
sb.append(Utils.getIptables).append(" -t nat -F OUTPUT").append("\n")
Utils.runRootCommand(sb.toString())
val ab = new ArrayBuffer[String]
ab.append("kill -9 `cat /data/data/com.github.shadowsocks/pdnsd.pid`")
ab.append("kill -9 `cat /data/data/com.github.shadowsocks/ss-local.pid`")
ab.append("kill -9 `cat /data/data/com.github.shadowsocks/ss-tunnel.pid`")
ab.append("kill -9 `cat /data/data/com.github.shadowsocks/tun2socks.pid`")
ab.append("killall -9 pdnsd")
ab.append("killall -9 ss-local")
ab.append("killall -9 ss-tunnel")
ab.append("killall -9 tun2socks")
ab.append("rm /data/data/com.github.shadowsocks/pdnsd.conf")
ab.append("rm /data/data/com.github.shadowsocks/pdnsd.cache")
Console.runCommand(ab.toArray)
ab.clear()
ab.append("kill -9 `cat /data/data/com.github.shadowsocks/redsocks.pid`")
ab.append("killall -9 redsocks")
ab.append("rm /data/data/com.github.shadowsocks/redsocks.conf")
ab.append(Utils.getIptables + " -t nat -F OUTPUT")
Console.runRootCommand(ab.toArray)
}
private def getVersionName: String = {
......@@ -587,7 +597,7 @@ class Shadowsocks
// Bind to the service
spawn {
val isRoot = Utils.getRoot
val isRoot = Console.isRoot
handler.post(new Runnable {
override def run() {
status.edit.putBoolean(Key.isRoot, isRoot).commit()
......@@ -908,12 +918,12 @@ class Shadowsocks
def reset() {
crash_recovery()
copyAssets(System.getABI)
Utils.runCommand("chmod 755 /data/data/com.github.shadowsocks/iptables\n"
+ "chmod 755 /data/data/com.github.shadowsocks/redsocks\n"
+ "chmod 755 /data/data/com.github.shadowsocks/pdnsd\n"
+ "chmod 755 /data/data/com.github.shadowsocks/ss-local\n"
+ "chmod 755 /data/data/com.github.shadowsocks/ss-tunnel\n"
+ "chmod 755 /data/data/com.github.shadowsocks/tun2socks\n")
val ab = new ArrayBuffer[String]
for (executable <- Shadowsocks.EXECUTABLES) {
ab.append("chmod 755 " + Path.BASE + executable)
}
Console.runCommand(ab.toArray)
}
private def recovery() {
......
......@@ -50,7 +50,6 @@ import android.os._
import android.support.v4.app.NotificationCompat
import android.util.Log
import com.google.analytics.tracking.android.{Fields, MapBuilder, EasyTracker}
import java.io._
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import org.apache.http.conn.util.InetAddressUtils
......@@ -62,6 +61,8 @@ import com.github.shadowsocks.utils._
import scala.Some
import android.graphics.Color
import com.github.shadowsocks.aidl.Config
import scala.collection.mutable.ArrayBuffer
import java.io.File
case class TrafficStat(tx: Long, rx: Long, timestamp: Long)
......@@ -107,7 +108,7 @@ class ShadowsocksNatService extends Service with BaseService {
Path.BASE + "ss-local.pid")
.format(config.proxy, config.remotePort, config.localPort, config.sitekey, config.encMethod)
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
Utils.runCommand(cmd)
System.exec(cmd)
}
def startDnsDaemon() {
......@@ -123,8 +124,8 @@ class ShadowsocksNatService extends Service with BaseService {
})
Path.BASE + "pdnsd -c " + Path.BASE + "pdnsd.conf"
}
Utils.runCommand(cmd)
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
System.exec(cmd)
}
def getVersionName: String = {
......@@ -146,7 +147,7 @@ class ShadowsocksNatService extends Service with BaseService {
ConfigUtils.printToFile(new File(Path.BASE + "redsocks.conf"))(p => {
p.println(conf)
})
Utils.runRootCommand(cmd)
Console.runRootCommand(cmd)
}
/** Called when the activity is first created. */
......@@ -267,24 +268,25 @@ class ShadowsocksNatService extends Service with BaseService {
}
def killProcesses() {
Utils.runRootCommand(Utils.getIptables + " -t nat -F OUTPUT")
Console.runRootCommand(Utils.getIptables + " -t nat -F OUTPUT")
val ab = new ArrayBuffer[String]
val sb = new StringBuilder
ab.append("kill -9 `cat " + Path.BASE +"redsocks.pid`")
ab.append("killall -9 redsocks")
sb ++= "kill -9 `cat " ++= Path.BASE ++= "redsocks.pid`" ++= "\n"
sb ++= "killall -9 redsocks" ++= "\n"
Utils.runRootCommand(sb.toString())
Console.runRootCommand(ab.toArray)
sb.clear()
ab.clear()
sb ++= "kill -9 `cat " ++= Path.BASE ++= "pdnsd.pid`" ++= "\n"
sb ++= "killall -9 pdnsd" ++= "\n"
sb ++= "kill -9 `cat " ++= Path.BASE ++= "ss-local.pid`" ++= "\n"
sb ++= "killall -9 ss-local" ++= "\n"
sb ++= "kill -9 `cat " ++= Path.BASE ++= "ss-tunnel.pid`" ++= "\n"
sb ++= "killall -9 ss-tunnel" ++= "\n"
ab.append("kill -9 `cat " + Path.BASE + "ss-local.pid`")
ab.append("killall -9 ss-local")
ab.append("kill -9 `cat " + Path.BASE + "ss-tunnel.pid`")
ab.append("killall -9 ss-tunnel")
ab.append("kill -9 `cat " + Path.BASE + "pdnsd.pid`")
ab.append("killall -9 pdnsd")
Utils.runCommand(sb.toString())
Console.runRootCommand(ab.toArray)
}
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
......@@ -292,8 +294,7 @@ class ShadowsocksNatService extends Service with BaseService {
}
def flushDNS() {
Utils.runRootCommand("ndc resolver flushdefaultif\n"
+ "ndc resolver flushif wlan0\n")
Console.runRootCommand(Array("ndc resolver flushdefaultif", "ndc resolver flushif wlan0"))
}
def setupIptables: Boolean = {
......@@ -354,9 +355,9 @@ class ShadowsocksNatService extends Service with BaseService {
}
}
val init_rules: String = init_sb.toString()
Utils.runRootCommand(init_rules, 30 * 1000)
Console.runRootCommand(init_rules)
val redt_rules: String = http_sb.toString()
Utils.runRootCommand(redt_rules)
Console.runRootCommand(redt_rules)
true
}
......
......@@ -73,7 +73,7 @@ class ShadowsocksRunnerActivity extends Activity {
def isVpnEnabled: Boolean = {
if (vpnEnabled < 0) {
vpnEnabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH
&& !Utils.getRoot) {
&& !Console.isRoot) {
1
} else {
0
......
......@@ -47,7 +47,6 @@ import android.os._
import android.support.v4.app.NotificationCompat
import android.util.Log
import com.google.analytics.tracking.android.{Fields, MapBuilder, EasyTracker}
import java.io._
import android.net.VpnService
import org.apache.http.conn.util.InetAddressUtils
import android.os.Message
......@@ -57,6 +56,8 @@ import java.net.InetAddress
import com.github.shadowsocks.utils._
import scala.Some
import com.github.shadowsocks.aidl.{IShadowsocksService, Config}
import scala.collection.mutable.ArrayBuffer
import java.io.File
class ShadowsocksVpnService extends VpnService with BaseService {
......@@ -96,7 +97,8 @@ class ShadowsocksVpnService extends VpnService with BaseService {
})
Path.BASE + "pdnsd -c " + Path.BASE + "pdnsd.conf"
}
Utils.runCommand(cmd)
if (BuildConfig.DEBUG) Log.d(TAG, cmd)
System.exec(cmd)
}
def getVersionName: String = {
......@@ -248,18 +250,18 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
def killProcesses() {
val sb = new StringBuilder
sb ++= "kill -9 `cat " ++= Path.BASE ++= "ss-local.pid`" ++= "\n"
sb ++= "killall -9 ss-local" ++= "\n"
sb ++= "kill -9 `cat " ++= Path.BASE ++= "ss-tunnel.pid`" ++= "\n"
sb ++= "killall -9 ss-tunnel" ++= "\n"
sb ++= "kill -9 `cat " ++= Path.BASE ++= "tun2socks.pid`" ++= "\n"
sb ++= "killall -9 tun2socks" ++= "\n"
sb ++= "kill -9 `cat " ++= Path.BASE ++= "pdnsd.pid`" ++= "\n"
sb ++= "killall -9 pdnsd" ++= "\n"
Utils.runCommand(sb.toString())
val ab = new ArrayBuffer[String]
ab.append("kill -9 `cat " + Path.BASE + "ss-local.pid`")
ab.append("killall -9 ss-local")
ab.append("kill -9 `cat " + Path.BASE + "ss-tunnel.pid`")
ab.append("killall -9 ss-tunnel")
ab.append("kill -9 `cat " + Path.BASE + "tun2socks.pid`")
ab.append("killall -9 tun2socks")
ab.append("kill -9 `cat " + Path.BASE + "pdnsd.pid`")
ab.append("killall -9 pdnsd")
Console.runCommand(ab.toArray)
}
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = {
......
package com.github.shadowsocks.utils
import eu.chainfire.libsuperuser.Shell
import eu.chainfire.libsuperuser.Shell.{OnCommandResultListener, SU, Builder}
import java.util
import android.util.Log
object Console {
private var shell: Shell.Interactive = null
private var rootShell: Shell.Interactive = null
private def openShell() {
if (shell == null) {
val builder = new Builder()
shell = builder
.useSH()
.setWatchdogTimeout(10)
.open(new OnCommandResultListener {
override def onCommandResult(commandCode: Int, exitCode: Int,
output: util.List[String]) {
if (exitCode < 0) shell = null
}
})
}
}
private def openRootShell() {
if (rootShell == null) {
val builder = new Builder()
rootShell = builder
.setShell(SU.shell(0, "u:r:untrusted_app:s0"))
.setWantSTDERR(true)
.setWatchdogTimeout(10)
.open()
}
}
def runCommand(command: String) {
runCommand(Array(command))
}
def runCommand(commands: Array[String]) {
if (shell == null) {
openShell()
}
shell.addCommand(commands)
shell.waitForIdle()
}
def runRootCommand(command: String): String = runRootCommand(Array(command))
def runRootCommand(commands: Array[String]): String = {
if (!isRoot) {
return null
}
if (rootShell == null) {
openRootShell()
}
val sb = new StringBuilder
rootShell.addCommand(commands, 0, new OnCommandResultListener {
override def onCommandResult(commandCode: Int, exitCode: Int,
output: util.List[String]) {
if (exitCode < 0) {
rootShell = null
} else {
import scala.collection.JavaConversions._
output.foreach(line => sb.append(line).append('\n'))
}
}
})
if (rootShell.waitForIdle()) sb.toString()
else null
}
def isRoot: Boolean = SU.available()
}
......@@ -42,6 +42,15 @@ package com.github.shadowsocks.utils
import android.content.{Intent, SharedPreferences}
import com.github.shadowsocks.aidl.Config
object Executable {
val REDSOCKS = "redsocks"
val PDNSD = "pdnsd"
val SS_LOCAL = "ss-local"
val SS_TUNNEL = "ss-tunnel"
val IPTABLES = "iptables"
val TUN2SOCKS = "tun2socks"
}
object Msg {
val CONNECT_FINISH = 1
val CONNECT_SUCCESS = 2
......
......@@ -134,8 +134,7 @@ object Utils {
private def toggleAboveApiLevel17() {
// Android 4.2 and above
Utils.runRootCommand("ndc resolver flushdefaultif\n"
+ "ndc resolver flushif wlan0\n")
Console.runRootCommand(Array("ndc resolver flushdefaultif", "ndc resolver flushif wlan0"))
//Utils.runRootCommand("settings put global airplane_mode_on 1\n"
// + "am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true\n"
......@@ -207,22 +206,19 @@ object Utils {
def resolve(host: String, enableIPv6: Boolean): Option[String] = {
if (enableIPv6 && Utils.isIPv6Support) {
resolve(host, Type.AAAA) match {
case Some(addr) => {
case Some(addr) =>
return Some(addr)
}
case None =>
}
}
resolve(host, Type.A) match {
case Some(addr) => {
case Some(addr) =>
return Some(addr)
}
case None =>
}
resolve(host) match {
case Some(addr) => {
case Some(addr) =>
return Some(addr)
}
case None =>
}
None
......@@ -248,10 +244,9 @@ object Utils {
}
}
} catch {
case ex: Exception => {
case ex: Exception =>
Log.e(TAG, "Failed to get interfaces' addresses.", ex)
}
}
None
}
......@@ -276,10 +271,9 @@ object Utils {
}
}
} catch {
case ex: Exception => {
case ex: Exception =>
Log.e(TAG, "Failed to get interfaces' addresses.", ex)
}
}
false
}
......@@ -289,23 +283,20 @@ object Utils {
*/
def checkIptables() {
if (!Utils.getRoot) {
if (!Console.isRoot) {
iptables = DEFAULT_IPTABLES
return
}
iptables = DEFAULT_IPTABLES
var lines: String = null
var compatible: Boolean = false
var version: Boolean = false
val sb = new StringBuilder
val command = iptables + " --version\n" + iptables + " -L -t nat -n\n" + "exit\n"
val exitcode = runScript(command, sb, 10 * 1000, asroot = true)
if (exitcode == TIME_OUT) return
val command = Array(iptables + " --version", iptables + " -L -t nat -n")
val lines = Console.runRootCommand(command)
if (lines == null) return
lines = sb.toString()
if (lines.contains("OUTPUT")) {
compatible = true
}
......@@ -346,11 +337,10 @@ object Utils {
val appInfo: ApplicationInfo = pm.getApplicationInfo(packages(0), 0)
return pm.getApplicationIcon(appInfo)
} catch {
case e: PackageManager.NameNotFoundException => {
case e: PackageManager.NameNotFoundException =>
Log.e(c.getPackageName, "No package found matching with the uid " + uid)
}
}
}
} else {
Log.e(c.getPackageName, "Package not found for uid " + uid)
}
......@@ -367,25 +357,16 @@ object Utils {
iptables
}
def getShell: String = {
if (shell == null) {
shell = DEFAULT_SHELL
if (!new File(shell).exists) shell = "sh"
}
shell
}
def initHasRedirectSupported() {
if (!Utils.getRoot) return
if (!Console.isRoot) return
hasRedirectSupport = 1
val sb = new StringBuilder
val command = Utils.getIptables + " -t nat -A OUTPUT -p udp --dport 54 -j REDIRECT --to 8154"
val exitcode: Int = runScript(command, sb, 10 * 1000, asroot = true)
val lines = sb.toString()
val lines = Console.runRootCommand(command)
Utils.runRootCommand(command.replace("-A", "-D"))
if (exitcode == TIME_OUT) return
Console.runRootCommand(command.replace("-A", "-D"))
if (lines == null) return
if (lines.contains("No chain/target/match")) {
hasRedirectSupport = 0
}
......@@ -394,196 +375,11 @@ object Utils {
def isInitialized: Boolean = {
initialized match {
case true => true
case _ => {
case _ =>
initialized = true
false
}
}
}
def getRoot: Boolean = {
if (isRoot != -1) return isRoot == 1
if (new File(DEFAULT_ROOT).exists) {
root_shell = DEFAULT_ROOT
} else if (new File(ALTERNATIVE_ROOT).exists) {
root_shell = ALTERNATIVE_ROOT
} else {
root_shell = "su"
}
val sb = new StringBuilder
val command: String = "id\n"
val exitcode: Int = runScript(command, sb, 10 * 1000, asroot = true)
if (exitcode == TIME_OUT) {
return false
}
val lines = sb.toString()
if (lines.contains("uid=0")) {
isRoot = 1
} else {
if (rootTries >= 1) isRoot = 0
rootTries += 1
}
isRoot == 1
}
def runCommand(command: String): Boolean = {
runCommand(command, 10 * 1000)
}
def runCommand(command: String, timeout: Int): Boolean = {
if (BuildConfig.DEBUG) Log.d(TAG, command)
runScript(command, null, timeout, asroot = false)
true
}
def runRootCommand(command: String): Boolean = {
runRootCommand(command, 10 * 1000)
}
def runRootCommand(command: String, timeout: Int): Boolean = {
if (!Utils.getRoot) {
Log.e(TAG, "Cannot get ROOT permission: " + root_shell)
return false
}
if (BuildConfig.DEBUG) Log.d(TAG, command)
runScript(command, null, timeout, asroot = true)
true
}
def runScript(script: String, result: StringBuilder, timeout: Long, asroot: Boolean): Int = {
val runner: Utils.ScriptRunner = new Utils.ScriptRunner(script, result, asroot)
runner.start()
try {
if (timeout > 0) {
runner.join(timeout)
} else {
runner.join()
}
if (runner.isAlive) {
runner.destroy()
runner.join(1000)
return TIME_OUT
}
} catch {
case ex: InterruptedException => {
return TIME_OUT
}
}
runner.exitcode
}
/**
* Internal thread used to execute scripts (as root or not).
* Creates a new script runner.
*
* @param scripts script to run
* @param result result output
* @param asroot if true, executes the script as root
*/
class ScriptRunner(val scripts: String, val result: StringBuilder, val asroot: Boolean)
extends Thread {
var exitcode: Int = -1
val pid: Array[Int] = new Array[Int](1)
var pipe: FileDescriptor = null
override def destroy() {
if (pid(0) != -1) {
Exec.hangupProcessGroup(pid(0))
pid(0) = -1
}
if (pipe != null) {
Exec.close(pipe)
pipe = null
}
}
def createSubprocess(processId: Array[Int], cmd: String): FileDescriptor = {
val args = parse(cmd)
val arg0 = args(0)
Exec
.createSubprocess(if (result != null) 1 else 0, arg0, args, null, scripts + "\nexit\n",
processId)
}
def parse(cmd: String): Array[String] = {
val PLAIN = 0
val INQUOTE = 2
val SKIP = 3
var state = PLAIN
val result: ArrayBuffer[String] = new ArrayBuffer[String]
val builder = new StringBuilder()
cmd foreach {
ch => {
state match {
case PLAIN => {
ch match {
case c if Character.isWhitespace(c) => {
result += builder.toString
builder.clear()
}
case '"' => state = INQUOTE
case _ => builder += ch
}
}
case INQUOTE => {
ch match {
case '\\' => state = SKIP
case '"' => state = PLAIN
case _ => builder += ch
}
}
case SKIP => {
builder += ch
state = INQUOTE
}
}
}
}
if (builder.length > 0) {
result += builder.toString
}
result.toArray
}
override def run() {
pid(0) = -1
try {
if (this.asroot) {
pipe = createSubprocess(pid, root_shell)
} else {
pipe = createSubprocess(pid, getShell)
}
if (pid(0) != -1) {
exitcode = Exec.waitFor(pid(0))
}
if (result == null || pipe == null) return
val stdout: InputStream = new FileInputStream(pipe)
val buf = new Array[Byte](8192)
var read: Int = 0
while (stdout.available > 0) {
read = stdout.read(buf)
result.append(new String(buf, 0, read))
}
} catch {
case ex: Exception => {
Log.e(TAG, "Cannot execute command", ex)
if (result != null) result.append("\n").append(ex)
}
} finally {
if (pipe != null) {
Exec.close(pipe)
}
if (pid(0) != -1) {
Exec.hangupProcessGroup(pid(0))
}
}
}
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment