/* * 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.content.Context; 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.util.CommandHelper; import com.cyanogenmod.filemanager.util.FileHelper; import java.io.ByteArrayOutputStream; 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.Arrays; import java.util.List; /** * An implementation of a {@link Console} based in the execution of shell commands.
*
* This class holds a shell bash 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 3 seconds should be enough for no-debugging environments private static final long DEFAULT_TIMEOUT = FileManagerApplication.isDebuggable() ? 20000L : 3000L; // A maximum operation timeout independently of the isWaitOnNewDataReceipt // of the program. A synchronous operation must not be more longer than // MAX_OPERATION_TIMEOUT + DEFAULT_TIMEOUT private static final long MAX_OPERATION_TIMEOUT = 30000L; private static final int DEFAULT_BUFFER = 512; //Shell References private final Shell mShell; 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 boolean mNewData = false; 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 */ ByteArrayOutputStream mSbIn = null; /** * @hide */ ByteArrayOutputStream mSbErr = null; private final SecureRandom mRandom; private ControlPatternInfo mControlPattern = new ControlPatternInfo(); private static class ControlPatternInfo { String mStartId1, mStartId2; String mEndId1, mEndId2; byte[] mStartControlBytes; int mEndControlLength; public void setNewPattern(String s1, String s2, String e1, String e2) { mStartId1 = s1; mStartId2 = s2; mEndId1 = e1; mEndId2 = e2; mStartControlBytes = (mStartId1 + "0" + mStartId2).getBytes(); mEndControlLength = mEndId1.length() + mEndId2.length() + 3; // leave 3 for exit code; } public byte[] getStartControlPatternBytes() { return mStartControlBytes; } public int getEndControlPatternLength() { return mEndControlLength; } public int[] getEndControlMatch(byte[] bytes) { return getEndControlMatch(bytes, false); } /** * Find end control pattern indices * @param bytes byte array to check against * @param getExitIndices if true, this method will return the indices of the values * in between the end control pattern (e.g. exit code), otherwise * it will return the start and end indices of the whole end pattern * @return if {@code getExitIndices} is false, See {@link #findControlPattern(byte[], byte[])} * otherwise it will return values as described in {@code getExitIndices} */ public int[] getEndControlMatch(byte[] bytes, boolean getExitIndices) { // in the end control pattern, we check for endId1, 1-3 chars for exit code, endId2 int start, end; final int[] end1PatternResult = findControlPattern(bytes, mEndId1.getBytes()); if (end1PatternResult != null) { start = end1PatternResult[0]; final int[] end2PatternResult = findControlPattern(bytes, mEndId2.getBytes()); if (end2PatternResult != null) { if (getExitIndices) { return new int[]{end1PatternResult[1], end2PatternResult[0]}; } end = end2PatternResult[1]; return new int[]{start, end}; } } return null; } /** * Find the start control pattern indices * @param bytes to check against * @return See {@link #findControlPattern(byte[], byte[])} */ public int[] getStartControlMatch(byte[] bytes) { return findControlPattern(bytes, getStartControlPatternBytes()); } /** * Checks whether {@code controlBytes} exists inside of {@code bytes} and returns the start * and end indices if it does. Returns null otherwise. * * @param bytes where to look for the pattern * @param controlBytes the pattern to search for * @return if {@code controlBytes} was found inside of {@code bytes} then this method will * return an int[] array of length 2: * */ private int[] findControlPattern(byte[] bytes, byte[] controlBytes) { if (bytes.length >= controlBytes.length) { for (int i = 0; i < bytes.length; i++) { boolean foundControlBytePattern = true; int patternEnd = -1; if (bytes[i] == controlBytes[0]) { int start = i; for (int j = start; j < controlBytes.length + start; j++) { if (j > bytes.length - 1) { foundControlBytePattern = false; break; } if (bytes[j] != controlBytes[j - start]) { foundControlBytePattern = false; break; } else { patternEnd = j; } } if (foundControlBytePattern) { return new int[]{start, patternEnd+1}; } } } } return null; } } /** * @hide */ int mBufferSize; private final ShellExecutableFactory mExecutableFactory; /** * Constructor of ShellConsole. * * @param shell The shell used to execute commands * @throws FileNotFoundException If the initial directory not exists * @throws IOException If initial directory couldn't be resolved */ public ShellConsole(Shell shell) throws FileNotFoundException, IOException { super(); this.mShell = shell; this.mExecutableFactory = new ShellExecutableFactory(this); this.mBufferSize = DEFAULT_BUFFER; //Restart the buffers this.mSbIn = new ByteArrayOutputStream(); this.mSbErr = new ByteArrayOutputStream(); //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 cmd = new ArrayList(); 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()]), this.mShell.getEnvironment(), new File(FileHelper.ROOT_DIRECTORY).getCanonicalFile()); synchronized (this.mSync) { this.mActive = true; } if (isTrace()) { Log.v(TAG, String.format("Create console %s, command: %s, args: %s, env: %s", //$NON-NLS-1$ this.mShell.getId(), this.mShell.getCommand(), this.mShell.getArguments(), Arrays.toString(this.mShell.getEnvironment()))); } //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(); // Wait indefinitely if the console is allocating a su command. We need to // wait to user response to SuperUser or SuperSu prompt (or whatever it is) // The rest of sync operations will run with a timeout. execute(processIdCmd, this.isPrivileged()); Integer pid = null; try { pid = processIdCmd.getResult().get(0); } catch (Exception e) { // Ignore } 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, null); 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, null); 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 synchronized void execute(Executable executable, Context ctx) throws ConsoleAllocException, InsufficientPermissionsException, NoSuchFileOrDirectory, OperationTimeoutException, ExecutionException, CommandNotFoundException, ReadOnlyFilesystemException { execute(executable, false); } /** * Method for execute a command in the operating system layer. * * @param executable The executable command to be executed * @param waitForSu Wait for su (do not used timeout) * @throws ConsoleAllocException If the console is not allocated * @throws InsufficientPermissionsException If an operation requires elevated permissions * @throws NoSuchFileOrDirectory If the file or directory was not found * @throws OperationTimeoutException If the operation exceeded the maximum time of wait * @throws CommandNotFoundException If the executable program was not found * @throws ExecutionException If the operation returns a invalid exit code * @throws ReadOnlyFilesystemException If the operation writes in a read-only filesystem */ private synchronized void execute(final Executable executable, final boolean waitForSu) 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 { //Synchronous execution (2 tries with 1 reallocation) final ShellConsole shell = ShellConsole.this; if (shell.syncExecute(program, true, false)) { shell.syncExecute(program, false, 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) program.setExitOnStdErrOutput(waitForSu); if (syncExecute(program, true, waitForSu) && !waitForSu) { syncExecute(program, false, 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 * @param waitForSu Wait for su (do not used timeout) * @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, boolean waitForSu) 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; final boolean async = program instanceof AsyncResultProgram; //Reset the buffers this.mStarted = false; this.mCancelled = false; this.mSbIn = new ByteArrayOutputStream(); this.mSbErr = new ByteArrayOutputStream(); //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$ this.mShell.getId(), program.getId(), cmd, args)); } //Is asynchronous program? Then set asynchronous program.setProgramListener(this); if (async) { ((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())); mControlPattern.setNewPattern(startId1, startId2, endId1, endId2); 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.mNewData = false; this.mOut.write(sb.toString().getBytes()); this.mOut.flush(); } } 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 (waitForSu || program.isIndefinitelyWait()) { this.mSync.wait(); } else { final long start = System.currentTimeMillis(); while (true) { this.mSync.wait(DEFAULT_TIMEOUT); if (!this.mFinished) { final long end = System.currentTimeMillis(); if (!program.isWaitOnNewDataReceipt() || !this.mNewData || (end - start >= MAX_OPERATION_TIMEOUT)) { throw new OperationTimeoutException(end - start, cmd); } // Still waiting for program ending this.mNewData = false; continue; } break; } } } } //End partial results? if (async) { synchronized (this.mPartialSync) { ((AsyncResultProgram)program).onRequestEndParsePartialResult(this.mCancelled); } } //Retrieve exit code int exitCode = getExitCode(this.mSbIn, async); if (async) { synchronized (this.mPartialSync) { ((AsyncResultProgram)program).onRequestExitCode(exitCode); } } if (isTrace()) { Log.v(TAG, String.format("%s-%s, command: %s, exitCode: %s", //$NON-NLS-1$ 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 (OperationTimeoutException otEx) { try { killCurrentCommand(); } catch (Exception e) { /**NON BLOCK **/} throw otEx; } 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() { final ShellConsole shell = ShellConsole.this; int read = 0; ByteArrayOutputStream sb = null; try { while (shell.mActive) { //Read only one byte with active wait final int r = in.read(); if (r == -1) { break; } // Type of command boolean async = shell.mActiveCommand != null && shell.mActiveCommand instanceof AsyncResultProgram; if (!async || sb == null) { sb = new ByteArrayOutputStream(); } if (!shell.mCancelled) { shell.mSbIn.write(r); if (!shell.mStarted) { shell.mStarted = isCommandStarted(shell.mSbIn); if (shell.mStarted) { byte[] bytes = shell.mSbIn.toByteArray(); sb = new ByteArrayOutputStream(); sb.write(bytes, 0, bytes.length); if (async) { synchronized (shell.mPartialSync) { ((AsyncResultProgram) shell.mActiveCommand). onRequestStartParsePartialResult(); } } } else { byte[] data = shell.mSbIn.toByteArray(); sb.write(data, 0, data.length); } } else { sb.write(r); } // New data received onNewData(); //Check if the command has finished (and extract the control) boolean finished = isCommandFinished(shell.mSbIn, sb); //Notify asynchronous partial data if (shell.mStarted && async) { AsyncResultProgram program = ((AsyncResultProgram)shell.mActiveCommand); String partial = sb.toString(); if (partial.length() >= shell.mControlPattern .getEndControlPatternLength()) { program.onRequestParsePartialResult(sb.toByteArray()); shell.toStdIn(partial); // Reset the temp buffer sb = new ByteArrayOutputStream(); } } if (finished) { if (!async) { shell.toStdIn(String.valueOf((char)r)); } else { AsyncResultProgram program = ((AsyncResultProgram)shell.mActiveCommand); String partial = sb.toString(); if (program != null) { program.onRequestParsePartialResult(sb.toByteArray()); } shell.toStdIn(partial); } //Notify the end notifyProcessFinished(); break; } if (!async && !finished) { shell.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(), shell.mBufferSize); byte[] data = new byte[available]; read = in.read(data); // Type of command async = shell.mActiveCommand != null && shell.mActiveCommand instanceof AsyncResultProgram; // Exit if active command is cancelled if (shell.mCancelled) continue; final String s = new String(data, 0, read); shell.mSbIn.write(data, 0, read); if (!shell.mStarted) { shell.mStarted = isCommandStarted(shell.mSbIn); if (shell.mStarted) { byte[] bytes = shell.mSbIn.toByteArray(); sb = new ByteArrayOutputStream(); sb.write(bytes, 0, bytes.length); if (async) { synchronized (shell.mPartialSync) { AsyncResultProgram p = ((AsyncResultProgram)shell.mActiveCommand); if (p != null) { p.onRequestStartParsePartialResult(); } } } } else { byte[] bytes = shell.mSbIn.toByteArray(); sb.write(bytes, 0, bytes.length); } } else { sb.write(data, 0, read); } // New data received onNewData(); //Check if the command has finished (and extract the control) boolean finished = isCommandFinished(shell.mSbIn, sb); //Notify asynchronous partial data if (async) { AsyncResultProgram program = ((AsyncResultProgram)shell.mActiveCommand); String partial = sb.toString(); if (partial.length() >= shell.mControlPattern .getEndControlPatternLength()) { if (program != null) { program.onRequestParsePartialResult(sb.toByteArray()); } shell.toStdIn(partial); // Reset the temp buffer sb = new ByteArrayOutputStream(); } } if (finished) { if (!async) { shell.toStdIn(s); } else { AsyncResultProgram program = ((AsyncResultProgram)shell.mActiveCommand); String partial = sb.toString(); if (program != null) { program.onRequestParsePartialResult(sb.toByteArray()); } shell.toStdIn(partial); } //Notify the end notifyProcessFinished(); break; } if (!async && !finished) { shell.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(shell.mSbIn); trimBuffer(shell.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() { final ShellConsole shell = ShellConsole.this; int read = 0; try { while (shell.mActive) { //Read only one byte with active wait int r = err.read(); if (r == -1) { break; } // Has the process received something that we dont expect? if (shell.mActiveCommand != null && shell.mActiveCommand.isExitOnStdErrOutput()) { notifyProcessFinished(); continue; } // Type of command boolean async = shell.mActiveCommand != null && shell.mActiveCommand instanceof AsyncResultProgram; ByteArrayOutputStream sb = new ByteArrayOutputStream(); if (!shell.mCancelled) { shell.mSbErr.write(r); sb.write(r); //Notify asynchronous partial data if (shell.mStarted && async) { AsyncResultProgram program = ((AsyncResultProgram)shell.mActiveCommand); if (program != null) { program.parsePartialErrResult(new String(new char[]{(char)r})); } } toStdErr(sb.toString()); } // New data received onNewData(); //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(), shell.mBufferSize); byte[] data = new byte[available]; read = err.read(data); // Type of command async = shell.mActiveCommand != null && shell.mActiveCommand instanceof AsyncResultProgram; // Add to stderr String s = new String(data, 0, read); shell.mSbErr.write(data, 0, read); sb.write(data, 0, read); //Notify asynchronous partial data if (async) { AsyncResultProgram program = ((AsyncResultProgram)shell.mActiveCommand); if (program != null) { program.parsePartialErrResult(s); } } toStdErr(s); // Has the process received something that we dont expect? if (shell.mActiveCommand != null && shell.mActiveCommand.isExitOnStdErrOutput()) { notifyProcessFinished(); break; } // New data received onNewData(); //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 (shell.mActiveCommand != null && shell.mActiveCommand instanceof AsyncResultProgram) { trimBuffer(shell.mSbIn); trimBuffer(shell.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()); this.mOut.flush(); } 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 (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 (this.mSync) { if (this.mActive) { this.mActive = false; this.mFinished = true; this.mSync.notify(); if (ex != null) { Log.w(TAG, "Exit with exception", ex); //$NON-NLS-1$ } } } } /** * Method that notifies the ending of the command execution. * @hide */ void notifyProcessFinished() { synchronized (this.mSync) { if (this.mActive) { this.mFinished = true; this.mSync.notify(); } } } /** * 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(ByteArrayOutputStream stdin) { if (stdin == null) return false; byte[] data = stdin.toByteArray(); final int[] match = mControlPattern.getStartControlMatch(data); if (match != null) { stdin.reset(); stdin.write(data, match[1], data.length - match[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(ByteArrayOutputStream stdin, ByteArrayOutputStream partial) { if (stdin == null) return false; int[] match = mControlPattern.getEndControlMatch(stdin.toByteArray()); boolean ret = match != null; // Remove partial if (ret && partial != null) { byte[] bytes = partial.toByteArray(); match = mControlPattern.getEndControlMatch(bytes); if (match != null) { partial.reset(); partial.write(bytes, 0, match[0]); } } return ret; } /** * New data was received * @hide */ void onNewData() { synchronized (this.mSync) { this.mNewData = true; } } /** * 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(ByteArrayOutputStream stdin, boolean async) { // If process was cancelled, don't expect a exit code. // Returns always 143 code if (this.mCancelled) { return 143; } byte[] bytes = stdin.toByteArray(); int[] match = mControlPattern.getEndControlMatch(bytes); if (match != null) { if (!async) { mSbIn.reset(); mSbIn.write(bytes, 0, match[0]); } // find the indices of the exit code and extract it match = mControlPattern.getEndControlMatch(bytes, true); return Integer.parseInt(new String(bytes, match[0], match[1] - match[0])); } 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(ByteArrayOutputStream sb) { final int bufferSize = mControlPattern .getEndControlPatternLength(); if (sb.size() > bufferSize) { byte[] data = sb.toByteArray(); sb.reset(); sb.write(data, data.length - bufferSize, bufferSize); } } /** * Method that kill the current command. * * @return boolean If the program was killed * @hide */ private boolean killCurrentCommand() { synchronized (this.mSync) { // 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; } if (this.mActiveCommand != null && this.mActiveCommand.getCommand() != null) { try { boolean isCancellable = true; if (this.mActiveCommand instanceof AsyncResultProgram) { final AsyncResultProgram asyncCmd = (AsyncResultProgram)this.mActiveCommand; isCancellable = asyncCmd.isCancellable(); } if (isCancellable) { try { //Get the PIDs in background List pids = CommandHelper.getProcessesIds( null, this.mShell.getPid(), FileManagerApplication.getBackgroundConsole()); for (Integer pid: pids) { if (pid != null) { CommandHelper.sendSignal( null, pid.intValue(), FileManagerApplication.getBackgroundConsole()); try { //Wait for process to be killed 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 kill current program: %s", ( (this.mActiveCommand == null) ? "" : this.mActiveCommand.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) { // 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; } if (this.mActiveCommand.getCommand() != null) { try { boolean isCancellable = true; if (this.mActiveCommand instanceof AsyncResultProgram) { final AsyncResultProgram asyncCmd = (AsyncResultProgram)this.mActiveCommand; isCancellable = asyncCmd.isCancellable(); } if (isCancellable) { try { //Get the PIDs in background List pids = CommandHelper.getProcessesIds( null, this.mShell.getPid(), FileManagerApplication.getBackgroundConsole()); for (Integer pid: pids) { if (pid != null) { CommandHelper.sendSignal( null, pid.intValue(), signal, FileManagerApplication.getBackgroundConsole()); try { //Wait for process to be signaled 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$ this.mActiveCommand.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; } }