tor-android/RootCommands/src/main/java/org/sufficientlysecure/rootcommands/Shell.java

350 lines
11 KiB
Java

/*
* Copyright (C) 2012 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Adam Shanks, Jeremy Lakeman (RootTools)
*
* 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 org.sufficientlysecure.rootcommands;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import org.sufficientlysecure.rootcommands.command.Command;
import org.sufficientlysecure.rootcommands.util.Log;
import org.sufficientlysecure.rootcommands.util.RootAccessDeniedException;
import org.sufficientlysecure.rootcommands.util.Utils;
public class Shell implements Closeable {
private final Process shellProcess;
private final BufferedReader stdOutErr;
private final DataOutputStream outputStream;
private final List<Command> commands = new ArrayList<Command>();
private boolean close = false;
private static final String LD_LIBRARY_PATH = System.getenv("LD_LIBRARY_PATH");
private static final String token = "F*D^W@#FGF";
/**
* Start root shell
*
* @param customEnv
* @param baseDirectory
* @return
* @throws IOException
*/
public static Shell startRootShell(ArrayList<String> customEnv, String baseDirectory)
throws IOException, RootAccessDeniedException {
Log.d(RootCommands.TAG, "Starting Root Shell!");
// On some versions of Android (ICS) LD_LIBRARY_PATH is unset when using su
// We need to pass LD_LIBRARY_PATH over su for some commands to work correctly.
if (customEnv == null) {
customEnv = new ArrayList<String>();
}
customEnv.add("LD_LIBRARY_PATH=" + LD_LIBRARY_PATH);
Shell shell = new Shell(Utils.getSuPath(), customEnv, baseDirectory);
return shell;
}
/**
* Start root shell without custom environment and base directory
*
* @return
* @throws IOException
*/
public static Shell startRootShell() throws IOException, RootAccessDeniedException {
return startRootShell(null, null);
}
/**
* Start default sh shell
*
* @param customEnv
* @param baseDirectory
* @return
* @throws IOException
*/
public static Shell startShell(ArrayList<String> customEnv, String baseDirectory)
throws IOException {
Log.d(RootCommands.TAG, "Starting Shell!");
Shell shell = new Shell("sh", customEnv, baseDirectory);
return shell;
}
/**
* Start default sh shell without custom environment and base directory
*
* @return
* @throws IOException
*/
public static Shell startShell() throws IOException {
return startShell(null, null);
}
/**
* Start custom shell defined by shellPath
*
* @param shellPath
* @param customEnv
* @param baseDirectory
* @return
* @throws IOException
*/
public static Shell startCustomShell(String shellPath, ArrayList<String> customEnv,
String baseDirectory) throws IOException {
Log.d(RootCommands.TAG, "Starting Custom Shell!");
Shell shell = new Shell(shellPath, customEnv, baseDirectory);
return shell;
}
/**
* Start custom shell without custom environment and base directory
*
* @param shellPath
* @return
* @throws IOException
*/
public static Shell startCustomShell(String shellPath) throws IOException {
return startCustomShell(shellPath, null, null);
}
private Shell(String shell, ArrayList<String> customEnv, String baseDirectory)
throws IOException, RootAccessDeniedException {
Log.d(RootCommands.TAG, "Starting shell: " + shell);
// start shell process!
shellProcess = Utils.runWithEnv(shell, customEnv, baseDirectory);
// StdErr is redirected to StdOut, defined in Command.getCommand()
stdOutErr = new BufferedReader(new InputStreamReader(shellProcess.getInputStream()));
outputStream = new DataOutputStream(shellProcess.getOutputStream());
outputStream.write("echo Started\n".getBytes());
outputStream.flush();
while (true) {
String line = stdOutErr.readLine();
if (line == null)
throw new RootAccessDeniedException(
"stdout line is null! Access was denied or this executeable is not a shell!");
if ("".equals(line))
continue;
if ("Started".equals(line))
break;
destroyShellProcess();
throw new IOException("Unable to start shell, unexpected output \"" + line + "\"");
}
new Thread(inputRunnable, "Shell Input").start();
new Thread(outputRunnable, "Shell Output").start();
}
private Runnable inputRunnable = new Runnable() {
public void run() {
try {
writeCommands();
} catch (IOException e) {
Log.e(RootCommands.TAG, "IO Exception", e);
}
}
};
private Runnable outputRunnable = new Runnable() {
public void run() {
try {
readOutput();
} catch (IOException e) {
Log.e(RootCommands.TAG, "IOException", e);
} catch (InterruptedException e) {
Log.e(RootCommands.TAG, "InterruptedException", e);
}
}
};
/**
* Destroy shell process considering that the process could already be terminated
*/
private void destroyShellProcess() {
try {
// Yes, this really is the way to check if the process is
// still running.
shellProcess.exitValue();
} catch (IllegalThreadStateException e) {
// Only call destroy() if the process is still running;
// Calling it for a terminated process will not crash, but
// (starting with at least ICS/4.0) spam the log with INFO
// messages ala "Failed to destroy process" and "kill
// failed: ESRCH (No such process)".
shellProcess.destroy();
}
Log.d(RootCommands.TAG, "Shell destroyed");
}
/**
* Writes queued commands one after another into the opened shell. After an execution a token is
* written to seperate command output on read
*
* @throws IOException
*/
private void writeCommands() throws IOException {
try {
int commandIndex = 0;
while (true) {
DataOutputStream out;
synchronized (commands) {
while (!close && commandIndex >= commands.size()) {
commands.wait();
}
out = this.outputStream;
}
if (commandIndex < commands.size()) {
Command next = commands.get(commandIndex);
next.writeCommand(out);
String line = "\necho " + token + " " + commandIndex + " $?\n";
out.write(line.getBytes());
out.flush();
commandIndex++;
} else if (close) {
out.write("\nexit 0\n".getBytes());
out.flush();
Log.d(RootCommands.TAG, "Closing shell");
shellProcess.waitFor();
out.close();
return;
} else {
Thread.sleep(50);
}
}
} catch (InterruptedException e) {
Log.e(RootCommands.TAG, "interrupted while writing command", e);
}
}
/**
* Reads output line by line, seperated by token written after every command
*
* @throws IOException
* @throws InterruptedException
*/
private void readOutput() throws IOException, InterruptedException {
Command command = null;
// index of current command
int commandIndex = 0;
while (true) {
String lineStdOut = stdOutErr.readLine();
// terminate on EOF
if (lineStdOut == null)
break;
if (command == null) {
// break on close after last command
if (commandIndex >= commands.size()) {
if (close)
break;
continue;
}
// get current command
command = commands.get(commandIndex);
}
int pos = lineStdOut.indexOf(token);
if (pos > 0) {
command.processOutput(lineStdOut.substring(0, pos));
}
if (pos >= 0) {
lineStdOut = lineStdOut.substring(pos);
String fields[] = lineStdOut.split(" ");
int id = Integer.parseInt(fields[1]);
if (id == commandIndex) {
command.setExitCode(Integer.parseInt(fields[2]));
// go to next command
commandIndex++;
command = null;
continue;
}
}
command.processOutput(lineStdOut);
}
Log.d(RootCommands.TAG, "Read all output");
shellProcess.waitFor();
stdOutErr.close();
destroyShellProcess();
while (commandIndex < commands.size()) {
if (command == null) {
command = commands.get(commandIndex);
}
command.terminated("Unexpected Termination!");
commandIndex++;
command = null;
}
}
/**
* Add command to shell queue
*
* @param command
* @return
* @throws IOException
*/
public Command add(Command command) throws IOException {
if (close)
throw new IOException("Unable to add commands to a closed shell");
synchronized (commands) {
commands.add(command);
// set shell on the command object, to know where the command is running on
command.addedToShell(this, (commands.size() - 1));
commands.notifyAll();
}
return command;
}
/**
* Close shell
*
* @throws IOException
*/
public void close() throws IOException {
synchronized (commands) {
this.close = true;
commands.notifyAll();
}
}
/**
* Returns number of queued commands
*
* @return
*/
public int getCommandsSize() {
return commands.size();
}
}