/*
* Copyright (C) 2012 The CyanogenMod 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.cyanogenmod.filemanager.console.shell;
import android.util.Log;
import com.cyanogenmod.filemanager.FileManagerApplication;
import com.cyanogenmod.filemanager.commands.AsyncResultExecutable;
import com.cyanogenmod.filemanager.commands.Executable;
import com.cyanogenmod.filemanager.commands.ExecutableFactory;
import com.cyanogenmod.filemanager.commands.GroupsExecutable;
import com.cyanogenmod.filemanager.commands.IdentityExecutable;
import com.cyanogenmod.filemanager.commands.ProcessIdExecutable;
import com.cyanogenmod.filemanager.commands.SIGNAL;
import com.cyanogenmod.filemanager.commands.shell.AsyncResultProgram;
import com.cyanogenmod.filemanager.commands.shell.Command;
import com.cyanogenmod.filemanager.commands.shell.InvalidCommandDefinitionException;
import com.cyanogenmod.filemanager.commands.shell.Program;
import com.cyanogenmod.filemanager.commands.shell.Shell;
import com.cyanogenmod.filemanager.commands.shell.ShellExecutableFactory;
import com.cyanogenmod.filemanager.commands.shell.SyncResultProgram;
import com.cyanogenmod.filemanager.console.CommandNotFoundException;
import com.cyanogenmod.filemanager.console.Console;
import com.cyanogenmod.filemanager.console.ConsoleAllocException;
import com.cyanogenmod.filemanager.console.ExecutionException;
import com.cyanogenmod.filemanager.console.InsufficientPermissionsException;
import com.cyanogenmod.filemanager.console.NoSuchFileOrDirectory;
import com.cyanogenmod.filemanager.console.OperationTimeoutException;
import com.cyanogenmod.filemanager.console.ReadOnlyFilesystemException;
import com.cyanogenmod.filemanager.model.Identity;
import com.cyanogenmod.filemanager.preferences.FileManagerSettings;
import com.cyanogenmod.filemanager.preferences.Preferences;
import com.cyanogenmod.filemanager.util.CommandHelper;
import com.cyanogenmod.filemanager.util.FileHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An implementation of a {@link Console} based in the execution of shell commands.<br/>
* <br/>
* This class holds a <code>shell bash</code> program associated with the application, being
* a wrapper to execute all other programs (like shell does in linux), capturing the
* output (stdin and stderr) and the exit code of the program executed.
*/
public abstract class ShellConsole extends Console implements Program.ProgramListener {
private static final String TAG = "ShellConsole"; //$NON-NLS-1$
// A timeout of 5 seconds should be enough for no-debugging environments
private static final long DEFAULT_TIMEOUT =
FileManagerApplication.isDebuggable() ? 20000L : 3000L;
private static final int DEFAULT_BUFFER = 512;
//Shell References
private final Shell mShell;
private final String mInitialDirectory;
private Identity mIdentity;
//Process References
private final Object mSync = new Object();
/**
* @hide
*/
final Object mPartialSync = new Object();
/**
* @hide
*/
boolean mActive = false;
private boolean mFinished = true;
private Process mProc = null;
/**
* @hide
*/
Program mActiveCommand = null;
/**
* @hide
*/
boolean mCancelled;
/**
* @hide
*/
boolean mStarted;
//Buffers
private InputStream mIn = null;
private InputStream mErr = null;
private OutputStream mOut = null;
/**
* @hide
*/
StringBuffer mSbIn = null;
/**
* @hide
*/
StringBuffer mSbErr = null;
private final SecureRandom mRandom;
private String mStartControlPattern;
private String mEndControlPattern;
/**
* @hide
*/
int mBufferSize;
private final ShellExecutableFactory mExecutableFactory;
/**
* Constructor of <code>ShellConsole</code>.
*
* @param shell The shell used to execute commands
* @throws FileNotFoundException If the default initial directory not exists
* @throws IOException If initial directory couldn't be resolved
*/
public ShellConsole(Shell shell) throws FileNotFoundException, IOException {
this(shell, Preferences.getSharedPreferences().getString(
FileManagerSettings.SETTINGS_INITIAL_DIR.getId(),
(String)FileManagerSettings.SETTINGS_INITIAL_DIR.getDefaultValue()));
}
/**
* Constructor of <code>ShellConsole</code>.
*
* @param shell The shell used to execute commands
* @param initialDirectory The initial directory of the shell
* @throws FileNotFoundException If the initial directory not exists
* @throws IOException If initial directory couldn't be resolved
*/
public ShellConsole(Shell shell, String initialDirectory)
throws FileNotFoundException, IOException {
super();
this.mShell = shell;
this.mExecutableFactory = new ShellExecutableFactory(this);
this.mBufferSize = DEFAULT_BUFFER;
//Resolve and checks the initial directory
File f = new File(initialDirectory);
while (FileHelper.isSymlink(f)) {
f = FileHelper.resolveSymlink(f);
}
if (!f.exists() || !f.isDirectory()) {
throw new FileNotFoundException(f.toString());
}
this.mInitialDirectory = initialDirectory;
//Restart the buffers
this.mSbIn = new StringBuffer();
this.mSbErr = new StringBuffer();
//Generate an aleatory secure random generator
try {
this.mRandom = SecureRandom.getInstance("SHA1PRNG"); //$NON-NLS-1$
} catch (Exception ex) {
throw new IOException(ex);
}
}
/**
* {@inheritDoc}
*/
@Override
public ExecutableFactory getExecutableFactory() {
return this.mExecutableFactory;
}
/**
* {@inheritDoc}
*/
@Override
public Identity getIdentity() {
return this.mIdentity;
}
/**
* Method that returns the buffer size
*
* @return int The buffer size
*/
public int getBufferSize() {
return this.mBufferSize;
}
/**
* Method that sets the buffer size
*
* @param bufferSize the The buffer size
*/
public void setBufferSize(int bufferSize) {
this.mBufferSize = bufferSize;
}
/**
* {@inheritDoc}
*/
@Override
public final boolean isActive() {
return this.mActive;
}
/**
* {@inheritDoc}
*/
@Override
public final void alloc() throws ConsoleAllocException {
try {
//Create command string
List<String> cmd = new ArrayList<String>();
cmd.add(this.mShell.getCommand());
if (this.mShell.getArguments() != null && this.mShell.getArguments().length() > 0) {
cmd.add(this.mShell.getArguments());
}
//Create the process
Runtime rt = Runtime.getRuntime();
this.mProc =
rt.exec(
cmd.toArray(new String[cmd.size()]),
null,
new File(this.mInitialDirectory));
synchronized (this.mSync) {
this.mActive = true;
}
if (isTrace()) {
Log.v(TAG,
String.format("Create console %s, command: %s, args: %s", //$NON-NLS-1$
this.mShell.getId(),
this.mShell.getCommand(),
this.mShell.getArguments()));
}
//Allocate buffers
this.mIn = this.mProc.getInputStream();
this.mErr = this.mProc.getErrorStream();
this.mOut = this.mProc.getOutputStream();
if (this.mIn == null || this.mErr == null || this.mOut == null) {
try {
dealloc();
} catch (Throwable ex) {
/**NON BLOCK**/
}
throw new ConsoleAllocException("Console buffer allocation error."); //$NON-NLS-1$
}
//Starts a thread for extract output, and check timeout
createStdInThread(this.mIn);
createStdErrThread(this.mErr);
//Wait for thread start
Thread.sleep(50L);
//Check if process its active
checkIfProcessExits();
synchronized (this.mSync) {
if (!this.mActive) {
throw new ConsoleAllocException("Shell not started."); //$NON-NLS-1$
}
}
// Retrieve the PID of the shell
ProcessIdExecutable processIdCmd =
getExecutableFactory().
newCreator().createShellProcessIdExecutable();
execute(processIdCmd);
Integer pid = processIdCmd.getResult();
if (pid == null) {
throw new ConsoleAllocException(
"can't retrieve the PID of the shell."); //$NON-NLS-1$
}
this.mShell.setPid(pid.intValue());
//Retrieve identity
IdentityExecutable identityCmd =
getExecutableFactory().newCreator().createIdentityExecutable();
execute(identityCmd);
this.mIdentity = identityCmd.getResult();
// Identity command is required for root console detection,
// but Groups command is not used for now. Also, this command is causing
// problems on some implementations (maybe toolbox?) which don't
// recognize the root AID and returns an error. Safely ignore on error.
try {
if (this.mIdentity.getGroups().size() == 0) {
//Try with groups
GroupsExecutable groupsCmd =
getExecutableFactory().newCreator().createGroupsExecutable();
execute(groupsCmd);
this.mIdentity.setGroups(groupsCmd.getResult());
}
} catch (Exception ex) {
Log.w(TAG, "Groups command failed. Ignored.", ex); //$NON-NLS-1$
}
} catch (Exception ex) {
try {
dealloc();
} catch (Throwable ex2) {
/**NON BLOCK**/
}
throw new ConsoleAllocException("Console allocation error.", ex); //$NON-NLS-1$
}
}
/**
* {@inheritDoc}
*/
@Override
public final void dealloc() {
synchronized (this.mSync) {
if (this.mActive) {
this.mActive = false;
this.mFinished = true;
//Close buffers
try {
if (this.mIn != null) {
this.mIn.close();
}
} catch (Throwable ex) {
/**NON BLOCK**/
}
try {
if (this.mErr != null) {
this.mErr.close();
}
} catch (Throwable ex) {
/**NON BLOCK**/
}
try {
if (this.mOut != null) {
this.mOut.close();
}
} catch (Throwable ex) {
/**NON BLOCK**/
}
try {
this.mProc.destroy();
} catch (Throwable e) {/**NON BLOCK**/}
this.mIn = null;
this.mErr = null;
this.mOut = null;
this.mSbIn = null;
this.mSbErr = null;
}
}
}
/**
* {@inheritDoc}
*/
@Override
public final void realloc() throws ConsoleAllocException {
dealloc();
alloc();
}
/**
* {@inheritDoc}
*/
@Override
public final synchronized void execute(final Executable executable)
throws ConsoleAllocException, InsufficientPermissionsException,
CommandNotFoundException, NoSuchFileOrDirectory,
OperationTimeoutException, ExecutionException, ReadOnlyFilesystemException {
//Is a program?
if (!(executable instanceof Program)) {
throw new CommandNotFoundException("executable not instanceof Program"); //$NON-NLS-1$
}
//Asynchronous or synchronous execution?
final Program program = (Program)executable;
if (executable instanceof AsyncResultExecutable) {
Thread asyncThread = new Thread(new Runnable() {
@Override
public void run() {
//Synchronous execution (but asynchronous running in a thread)
//This way syncExecute is locked until this thread ends
try {
if (ShellConsole.this.syncExecute(program, true)) {
ShellConsole.this.syncExecute(program, false);
}
} catch (Exception ex) {
if (((AsyncResultExecutable)executable).getAsyncResultListener() != null) {
((AsyncResultExecutable)executable).
getAsyncResultListener().onException(ex);
} else {
//Capture exception
Log.e(TAG, "Fail asynchronous execution", ex); //$NON-NLS-1$
}
}
}
});
asyncThread.start();
} else {
//Synchronous execution (2 tries with 1 reallocation)
if (syncExecute(program, true)) {
syncExecute(program, false);
}
}
}
/**
* Method for execute a program command in the operating system layer in a synchronous way.
*
* @param program The program to execute
* @param reallocate If the console must be reallocated on i/o error
* @return boolean If the console was reallocated
* @throws ConsoleAllocException If the console is not allocated
* @throws InsufficientPermissionsException If an operation requires elevated permissions
* @throws CommandNotFoundException If the command was not found
* @throws NoSuchFileOrDirectory If the file or directory was not found
* @throws OperationTimeoutException If the operation exceeded the maximum time of wait
* @throws ExecutionException If the operation returns a invalid exit code
* @throws ReadOnlyFilesystemException If the operation writes in a read-only filesystem
* @hide
*/
synchronized boolean syncExecute(final Program program, boolean reallocate)
throws ConsoleAllocException, InsufficientPermissionsException,
CommandNotFoundException, NoSuchFileOrDirectory,
OperationTimeoutException, ExecutionException, ReadOnlyFilesystemException {
try {
//Check the console status before send command
checkConsole();
synchronized (this.mSync) {
if (!this.mActive) {
throw new ConsoleAllocException("No console allocated"); //$NON-NLS-1$
}
}
//Saves the active command reference
this.mActiveCommand = program;
//Reset the buffers
this.mStarted = false;
this.mCancelled = false;
this.mSbIn = new StringBuffer();
this.mSbErr = new StringBuffer();
//Random start/end identifiers
String startId1 =
String.format("/#%d#/", Long.valueOf(this.mRandom.nextLong())); //$NON-NLS-1$
String startId2 =
String.format("/#%d#/", Long.valueOf(this.mRandom.nextLong())); //$NON-NLS-1$
String endId1 =
String.format("/#%d#/", Long.valueOf(this.mRandom.nextLong())); //$NON-NLS-1$
String endId2 =
String.format("/#%d#/", Long.valueOf(this.mRandom.nextLong())); //$NON-NLS-1$
//Create command string
String cmd = program.getCommand();
String args = program.getArguments();
//Audit command
if (isTrace()) {
Log.v(TAG,
String.format("%s-%s, command: %s, args: %s", //$NON-NLS-1$
ShellConsole.this.mShell.getId(),
program.getId(),
cmd,
args));
}
//Is asynchronous program? Then set asynchronous
program.setProgramListener(this);
if (program instanceof AsyncResultProgram) {
((AsyncResultProgram)program).setOnCancelListener(this);
((AsyncResultProgram)program).setOnEndListener(this);
}
//Send the command + a control code with exit code
//The process has finished where control control code is present.
//This control code is unique in every invocation and is secure random
//generated (control code 1 + exit code + control code 2)
try {
boolean hasEndControl = (!(program instanceof AsyncResultProgram) ||
(program instanceof AsyncResultProgram &&
((AsyncResultProgram)program).isExpectEnd()));
this.mStartControlPattern = startId1 + "\\d{1,3}" + startId2; //$NON-NLS-1$
this.mEndControlPattern = endId1 + "\\d{1,3}" + endId2; //$NON-NLS-1$
String startCmd =
Command.getStartCodeCommandInfo(
FileManagerApplication.getInstance().getResources());
startCmd = String.format(
startCmd, "'" + startId1 +//$NON-NLS-1$
"'", "'" + startId2 + "'"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
String endCmd =
Command.getExitCodeCommandInfo(
FileManagerApplication.getInstance().getResources());
endCmd = String.format(
endCmd, "'" + endId1 + //$NON-NLS-1$
"'", "'" + endId2 + "'"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
StringBuilder sb = new StringBuilder()
.append(startCmd)
.append(" ") //$NON-NLS-1$
.append(cmd)
.append(" ") //$NON-NLS-1$
.append(args);
if (hasEndControl) {
sb = sb.append(" ") //$NON-NLS-1$
.append(endCmd);
}
sb.append(FileHelper.NEWLINE);
synchronized (this.mSync) {
this.mFinished = false;
this.mOut.write(sb.toString().getBytes());
}
} catch (InvalidCommandDefinitionException icdEx) {
throw new CommandNotFoundException(
"ExitCodeCommandInfo not found", icdEx); //$NON-NLS-1$
}
//Now, wait for buffers to be filled
synchronized (this.mSync) {
if (!this.mFinished) {
if (program instanceof AsyncResultProgram) {
this.mSync.wait();
} else {
this.mSync.wait(DEFAULT_TIMEOUT);
if (!this.mFinished) {
throw new OperationTimeoutException(DEFAULT_TIMEOUT, cmd);
}
}
}
}
//End partial results?
if (program instanceof AsyncResultProgram) {
synchronized (this.mPartialSync) {
((AsyncResultProgram)program).onRequestEndParsePartialResult(this.mCancelled);
}
}
//Retrieve exit code
int exitCode = getExitCode(this.mSbIn);
if (program instanceof AsyncResultProgram) {
synchronized (this.mPartialSync) {
((AsyncResultProgram)program).onRequestExitCode(exitCode);
}
}
if (isTrace()) {
Log.v(TAG,
String.format("%s-%s, command: %s, exitCode: %s", //$NON-NLS-1$
ShellConsole.this.mShell.getId(),
program.getId(),
cmd,
String.valueOf(exitCode)));
}
//Check if invocation was successfully or not
if (!program.isIgnoreShellStdErrCheck()) {
//Wait for stderr buffer to be filled
if (exitCode != 0) {
try {
Thread.sleep(100L);
} catch (Throwable ex) {/**NON BLOCK**/}
}
this.mShell.checkStdErr(this.mActiveCommand, exitCode, this.mSbErr.toString());
}
this.mShell.checkExitCode(exitCode);
program.checkExitCode(exitCode);
program.checkStdErr(exitCode, this.mSbErr.toString());
//Parse the result? Only if not partial results
if (program instanceof SyncResultProgram) {
try {
((SyncResultProgram)program).parse(
this.mSbIn.toString(), this.mSbErr.toString());
} catch (ParseException pEx) {
throw new ExecutionException(
"SyncResultProgram parse failed", pEx); //$NON-NLS-1$
}
}
//Invocation finished. Now program.getResult() has the result of
//the operation, if any exists
} catch (IOException ioEx) {
if (reallocate) {
realloc();
return true;
}
throw new ExecutionException("Console allocation error.", ioEx); //$NON-NLS-1$
} catch (InterruptedException ioEx) {
if (reallocate) {
realloc();
return true;
}
throw new ExecutionException("Console allocation error.", ioEx); //$NON-NLS-1$
} finally {
//Dereference the active command
this.mActiveCommand = null;
}
//Operation complete
return false;
}
/**
* Method that creates the standard input thread for read program response.
*
* @param in The standard input buffer
* @return Thread The standard input thread
*/
private Thread createStdInThread(final InputStream in) {
Thread t = new Thread(new Runnable() {
@SuppressWarnings("synthetic-access")
@Override
public void run() {
int read = 0;
StringBuffer sb = null;
try {
while (ShellConsole.this.mActive) {
//Read only one byte with active wait
final int r = in.read();
if (r == -1) {
break;
}
// Type of command
boolean async =
ShellConsole.this.mActiveCommand != null &&
ShellConsole.this.mActiveCommand instanceof AsyncResultProgram;
if (!async || sb == null) {
sb = new StringBuffer();
}
if (!ShellConsole.this.mCancelled) {
ShellConsole.this.mSbIn.append((char)r);
if (!ShellConsole.this.mStarted) {
ShellConsole.this.mStarted =
isCommandStarted(ShellConsole.this.mSbIn);
if (ShellConsole.this.mStarted) {
sb = new StringBuffer(ShellConsole.this.mSbIn.toString());
if (async) {
synchronized (ShellConsole.this.mPartialSync) {
((AsyncResultProgram)ShellConsole.
this.mActiveCommand).
onRequestStartParsePartialResult();
}
}
} else {
sb.append(ShellConsole.this.mSbIn.toString());
}
} else {
sb.append((char)r);
}
//Check if the command has finished (and extract the control)
boolean finished = isCommandFinished(ShellConsole.this.mSbIn, sb);
//Notify asynchronous partial data
if (ShellConsole.this.mStarted && async) {
AsyncResultProgram program =
((AsyncResultProgram)ShellConsole.this.mActiveCommand);
String partial = sb.toString();
int cc = ShellConsole.this.mEndControlPattern.length();
if (partial.length() >= cc) {
program.onRequestParsePartialResult(partial);
ShellConsole.this.toStdIn(partial);
// Reset the temp buffer
sb = new StringBuffer();
}
}
if (finished) {
if (!async) {
ShellConsole.this.toStdIn(String.valueOf((char)r));
} else {
AsyncResultProgram program =
((AsyncResultProgram)ShellConsole.this.mActiveCommand);
String partial = sb.toString();
if (program != null) {
program.onRequestParsePartialResult(partial);
}
ShellConsole.this.toStdIn(partial);
}
//Notify the end
notifyProcessFinished();
break;
}
if (!async && !finished) {
ShellConsole.this.toStdIn(String.valueOf((char)r));
}
}
//Has more data? Read with available as more as exists
//or maximum loop count is rebased
int count = 0;
while (in.available() > 0 && count < 10) {
count++;
int available =
Math.min(in.available(), ShellConsole.this.mBufferSize);
byte[] data = new byte[available];
read = in.read(data);
// Type of command
async =
ShellConsole.this.mActiveCommand != null &&
ShellConsole.this.mActiveCommand instanceof AsyncResultProgram;
// Exit if active command is cancelled
if (ShellConsole.this.mCancelled) continue;
final String s = new String(data, 0, read);
ShellConsole.this.mSbIn.append(s);
if (!ShellConsole.this.mStarted) {
ShellConsole.this.mStarted =
isCommandStarted(ShellConsole.this.mSbIn);
if (ShellConsole.this.mStarted) {
sb = new StringBuffer(ShellConsole.this.mSbIn.toString());
if (async) {
synchronized (ShellConsole.this.mPartialSync) {
AsyncResultProgram p =
((AsyncResultProgram)ShellConsole.
this.mActiveCommand);
if (p != null) {
p.onRequestStartParsePartialResult();
}
}
}
} else {
sb.append(ShellConsole.this.mSbIn.toString());
}
} else {
sb.append(s);
}
//Check if the command has finished (and extract the control)
boolean finished = isCommandFinished(ShellConsole.this.mSbIn, sb);
//Notify asynchronous partial data
if (async) {
AsyncResultProgram program =
((AsyncResultProgram)ShellConsole.this.mActiveCommand);
String partial = sb.toString();
int cc = ShellConsole.this.mEndControlPattern.length();
if (partial.length() >= cc) {
if (program != null) {
program.onRequestParsePartialResult(partial);
}
ShellConsole.this.toStdIn(partial);
// Reset the temp buffer
sb = new StringBuffer();
}
}
if (finished) {
if (!async) {
ShellConsole.this.toStdIn(s);
} else {
AsyncResultProgram program =
((AsyncResultProgram)ShellConsole.this.mActiveCommand);
String partial = sb.toString();
if (program != null) {
program.onRequestParsePartialResult(partial);
}
ShellConsole.this.toStdIn(partial);
}
//Notify the end
notifyProcessFinished();
break;
}
if (!async && !finished) {
ShellConsole.this.toStdIn(s);
}
//Wait for buffer to be filled
try {
Thread.sleep(1L);
} catch (Throwable ex) {/**NON BLOCK**/}
}
//Asynchronous programs can cause a lot of output, control buffers
//for a low memory footprint
if (async) {
trimBuffer(ShellConsole.this.mSbIn);
trimBuffer(ShellConsole.this.mSbErr);
}
//Check if process has exited
checkIfProcessExits();
}
} catch (Exception ioEx) {
notifyProcessExit(ioEx);
}
}
});
t.setName(String.format("%s", "stdin")); //$NON-NLS-1$//$NON-NLS-2$
t.start();
return t;
}
/**
* Method that echoes the stdin
*
* @param stdin The buffer of the stdin
* @hide
*/
void toStdIn(String stdin) {
//Audit (if not cancelled)
if (!this.mCancelled && isTrace() && stdin.length() > 0) {
Log.v(TAG,
String.format(
"stdin: %s", stdin)); //$NON-NLS-1$
}
}
/**
* Method that creates the standard error thread for read program response.
*
* @param err The standard error buffer
* @return Thread The standard error thread
*/
private Thread createStdErrThread(final InputStream err) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
int read = 0;
try {
while (ShellConsole.this.mActive) {
//Read only one byte with active wait
int r = err.read();
if (r == -1) {
break;
}
// Type of command
boolean async =
ShellConsole.this.mActiveCommand != null &&
ShellConsole.this.mActiveCommand instanceof AsyncResultProgram;
StringBuffer sb = new StringBuffer();
if (!ShellConsole.this.mCancelled) {
ShellConsole.this.mSbErr.append((char)r);
sb.append((char)r);
//Notify asynchronous partial data
if (ShellConsole.this.mStarted && async) {
AsyncResultProgram program =
((AsyncResultProgram)ShellConsole.this.mActiveCommand);
if (program != null) {
program.parsePartialErrResult(new String(new char[]{(char)r}));
}
}
toStdErr(sb.toString());
}
//Has more data? Read with available as more as exists
//or maximum loop count is rebased
int count = 0;
while (err.available() > 0 && count < 10) {
count++;
int available = Math.min(err.available(),
ShellConsole.this.mBufferSize);
byte[] data = new byte[available];
read = err.read(data);
// Type of command
async =
ShellConsole.this.mActiveCommand != null &&
ShellConsole.this.mActiveCommand instanceof AsyncResultProgram;
// Add to stderr
String s = new String(data, 0, read);
ShellConsole.this.mSbErr.append(s);
sb.append(s);
//Notify asynchronous partial data
if (async) {
AsyncResultProgram program =
((AsyncResultProgram)ShellConsole.this.mActiveCommand);
if (program != null) {
program.parsePartialErrResult(s);
}
}
toStdErr(s);
//Wait for buffer to be filled
try {
Thread.sleep(50L);
} catch (Throwable ex) {
/**NON BLOCK**/
}
}
//Asynchronous programs can cause a lot of output, control buffers
//for a low memory footprint
if (ShellConsole.this.mActiveCommand != null &&
ShellConsole.this.mActiveCommand instanceof AsyncResultProgram) {
trimBuffer(ShellConsole.this.mSbIn);
trimBuffer(ShellConsole.this.mSbErr);
}
}
} catch (Exception ioEx) {
notifyProcessExit(ioEx);
}
}
});
t.setName(String.format("%s", "stderr")); //$NON-NLS-1$//$NON-NLS-2$
t.start();
return t;
}
/**
* Method that echoes the stderr
*
* @param stdin The buffer of the stderr
* @hide
*/
void toStdErr(String stderr) {
//Audit (if not cancelled)
if (!this.mCancelled && isTrace()) {
Log.v(TAG,
String.format(
"stderr: %s", stderr)); //$NON-NLS-1$
}
}
/**
* Method that checks the console status and restart the console
* if this is unusable.
*
* @throws ConsoleAllocException If the console can't be reallocated
*/
private void checkConsole() throws ConsoleAllocException {
try {
//Test write something to the buffer
this.mOut.write(FileHelper.NEWLINE.getBytes());
this.mOut.write(FileHelper.NEWLINE.getBytes());
} catch (IOException ioex) {
//Something is wrong with the buffers. Reallocate console.
Log.w(TAG,
"Something is wrong with the console buffers. Reallocate console.", //$NON-NLS-1$
ioex);
//Reallocate the damage console
realloc();
}
}
/**
* Method that verifies if the process had exited.
* @hide
*/
void checkIfProcessExits() {
try {
if (this.mProc != null) {
synchronized (ShellConsole.this.mSync) {
this.mProc.exitValue();
}
this.mActive = false; //Exited
}
} catch (IllegalThreadStateException itsEx) {
//Not exited
}
}
/**
* Method that notifies the ending of the process.
*
* @param ex The exception, only if the process exit with a exception.
* Otherwise null
* @hide
*/
void notifyProcessExit(Exception ex) {
synchronized (ShellConsole.this.mSync) {
if (this.mActive) {
this.mSync.notify();
this.mActive = false;
this.mFinished = true;
if (ex != null) {
Log.w(TAG, "Exits with exception", ex); //$NON-NLS-1$
}
}
}
}
/**
* Method that notifies the ending of the command execution.
* @hide
*/
void notifyProcessFinished() {
synchronized (ShellConsole.this.mSync) {
if (this.mActive) {
this.mSync.notify();
this.mFinished = true;
}
}
}
/**
* Method that returns if the command has started by checking the
* standard input buffer. This method also removes the control start command
* from the buffer, if it's present, leaving in the buffer the new data bytes.
*
* @param stdin The standard in buffer
* @return boolean If the command has started
* @hide
*/
boolean isCommandStarted(StringBuffer stdin) {
if (stdin == null) return false;
Pattern pattern = Pattern.compile(this.mStartControlPattern);
Matcher matcher = pattern.matcher(stdin.toString());
if (matcher.find()) {
stdin.replace(0, matcher.end(), ""); //$NON-NLS-1$
return true;
}
return false;
}
/**
* Method that returns if the command has finished by checking the
* standard input buffer.
*
* @param stdin The standard in buffer
* @return boolean If the command has finished
* @hide
*/
boolean isCommandFinished(StringBuffer stdin, StringBuffer partial) {
Pattern pattern = Pattern.compile(this.mEndControlPattern);
if (stdin == null) return false;
Matcher matcher = pattern.matcher(stdin.toString());
boolean ret = matcher.find();
// Remove partial
if (ret && partial != null) {
matcher = pattern.matcher(partial.toString());
if (matcher.find()) {
partial.replace(matcher.start(), matcher.end(), ""); //$NON-NLS-1$
}
}
return ret;
}
/**
* Method that returns the exit code of the last executed command.
*
* @param stdin The standard in buffer
* @return int The exit code of the last executed command
*/
private int getExitCode(StringBuffer stdin) {
// If process was cancelled, don't expect a exit code.
// Returns always 143 code
if (this.mCancelled) {
return 143;
}
// Parse the stdin seeking exit code pattern
String txt = stdin.toString();
Pattern pattern = Pattern.compile(this.mEndControlPattern);
Matcher matcher = pattern.matcher(txt);
if (matcher.find()) {
this.mSbIn = new StringBuffer(txt.substring(0, matcher.start()));
String exitTxt = matcher.group();
return Integer.parseInt(
exitTxt.substring(
exitTxt.indexOf("#/") + 2, //$NON-NLS-1$
exitTxt.indexOf("/#", 2))); //$NON-NLS-1$
}
return 255;
}
/**
* Method that trim a buffer, let in the buffer some
* text to ensure that the exit code is in there.
*
* @param sb The buffer to trim
* @hide
*/
@SuppressWarnings("static-method") void trimBuffer(StringBuffer sb) {
final int bufferSize = 200;
if (sb.length() > bufferSize) {
sb.delete(0, sb.length() - bufferSize);
}
}
/**
* Method that kill the current command.
*
* @return boolean If the program was killed
* @hide
*/
private boolean killCurrentCommand() {
synchronized (this.mSync) {
//Is synchronous program? Otherwise it can't be cancelled
if (!(this.mActiveCommand instanceof AsyncResultProgram)) {
return false;
}
// Check background console
try {
FileManagerApplication.getBackgroundConsole();
} catch (Exception e) {
Log.w(TAG, "There is not background console. Not allowed.", e); //$NON-NLS-1$
return false;
}
final AsyncResultProgram program = (AsyncResultProgram)this.mActiveCommand;
if (program.getCommand() != null) {
try {
if (program.isCancellable()) {
//Get the PID in background
Integer pid =
CommandHelper.getProcessId(
null,
this.mShell.getPid(),
program.getCommand(),
FileManagerApplication.getBackgroundConsole());
if (pid != null) {
CommandHelper.sendSignal(
null,
pid.intValue(),
FileManagerApplication.getBackgroundConsole());
try {
//Wait for process kill
Thread.sleep(100L);
} catch (Throwable ex) {
/**NON BLOCK**/
}
this.mCancelled = true;
notifyProcessFinished();
this.mSync.notify();
return this.mCancelled;
}
}
} catch (Throwable ex) {
Log.w(TAG,
String.format("Unable to kill current program: %s", //$NON-NLS-1$
program.getCommand()), ex);
}
}
}
return false;
}
/**
* Method that send a signal to the current command.
*
* @param SIGNAL The signal to send
* @return boolean If the signal was sent
* @hide
*/
private boolean sendSignalToCurrentCommand(SIGNAL signal) {
synchronized (this.mSync) {
//Is synchronous program? Otherwise it can't be cancelled
if (!(this.mActiveCommand instanceof AsyncResultProgram)) {
return false;
}
// Check background console
try {
FileManagerApplication.getBackgroundConsole();
} catch (Exception e) {
Log.w(TAG, "There is not background console. Not allowed.", e); //$NON-NLS-1$
return false;
}
final AsyncResultProgram program = (AsyncResultProgram)this.mActiveCommand;
if (program.getCommand() != null) {
try {
if (program.isCancellable()) {
try {
//Get the PID in background
Integer pid =
CommandHelper.getProcessId(
null,
this.mShell.getPid(),
program.getCommand(),
FileManagerApplication.getBackgroundConsole());
if (pid != null) {
CommandHelper.sendSignal(
null,
pid.intValue(),
signal,
FileManagerApplication.getBackgroundConsole());
try {
//Wait for process kill
Thread.sleep(100L);
} catch (Throwable ex) {
/**NON BLOCK**/
}
return true;
}
} finally {
// It's finished
this.mCancelled = true;
notifyProcessFinished();
this.mSync.notify();
}
}
} catch (Throwable ex) {
Log.w(TAG,
String.format("Unable to send signal to current program: %s", //$NON-NLS-1$
program.getCommand()), ex);
}
}
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public boolean onEnd() {
//Kill the current command on end request
return killCurrentCommand();
}
/**
* {@inheritDoc}
*/
@Override
public boolean onSendSignal(SIGNAL signal) {
//Send a signal to the current command on end request
return sendSignalToCurrentCommand(signal);
}
/**
* {@inheritDoc}
*/
@Override
public boolean onCancel() {
//Kill the current command on cancel request
return killCurrentCommand();
}
/**
* {@inheritDoc}
*/
@Override
public boolean onRequestWrite(
byte[] data, int offset, int byteCount) throws ExecutionException {
try {
// Method that write to the stdin the data requested by the program
if (this.mOut != null) {
this.mOut.write(data, offset, byteCount);
this.mOut.flush();
Thread.yield();
return true;
}
} catch (Exception ex) {
String msg = String.format("Unable to write data to program: %s", //$NON-NLS-1$
this.mActiveCommand.getCommand());
Log.e(TAG, msg, ex);
throw new ExecutionException(msg, ex);
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public OutputStream getOutputStream() {
return this.mOut;
}
}