2 * Copyright (C) 2012 The CyanogenMod Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.cyanogenmod.filemanager.console.shell;
19 import android.content.Context;
20 import android.util.Log;
22 import com.cyanogenmod.filemanager.FileManagerApplication;
23 import com.cyanogenmod.filemanager.commands.AsyncResultExecutable;
24 import com.cyanogenmod.filemanager.commands.Executable;
25 import com.cyanogenmod.filemanager.commands.ExecutableFactory;
26 import com.cyanogenmod.filemanager.commands.GroupsExecutable;
27 import com.cyanogenmod.filemanager.commands.IdentityExecutable;
28 import com.cyanogenmod.filemanager.commands.ProcessIdExecutable;
29 import com.cyanogenmod.filemanager.commands.SIGNAL;
30 import com.cyanogenmod.filemanager.commands.shell.AsyncResultProgram;
31 import com.cyanogenmod.filemanager.commands.shell.Command;
32 import com.cyanogenmod.filemanager.commands.shell.InvalidCommandDefinitionException;
33 import com.cyanogenmod.filemanager.commands.shell.Program;
34 import com.cyanogenmod.filemanager.commands.shell.Shell;
35 import com.cyanogenmod.filemanager.commands.shell.ShellExecutableFactory;
36 import com.cyanogenmod.filemanager.commands.shell.SyncResultProgram;
37 import com.cyanogenmod.filemanager.console.CommandNotFoundException;
38 import com.cyanogenmod.filemanager.console.Console;
39 import com.cyanogenmod.filemanager.console.ConsoleAllocException;
40 import com.cyanogenmod.filemanager.console.ExecutionException;
41 import com.cyanogenmod.filemanager.console.InsufficientPermissionsException;
42 import com.cyanogenmod.filemanager.console.NoSuchFileOrDirectory;
43 import com.cyanogenmod.filemanager.console.OperationTimeoutException;
44 import com.cyanogenmod.filemanager.console.ReadOnlyFilesystemException;
45 import com.cyanogenmod.filemanager.model.Identity;
46 import com.cyanogenmod.filemanager.util.CommandHelper;
47 import com.cyanogenmod.filemanager.util.FileHelper;
49 import java.io.ByteArrayOutputStream;
51 import java.io.FileNotFoundException;
52 import java.io.IOException;
53 import java.io.InputStream;
54 import java.io.OutputStream;
55 import java.security.SecureRandom;
56 import java.text.ParseException;
57 import java.util.ArrayList;
58 import java.util.Arrays;
59 import java.util.List;
62 * An implementation of a {@link Console} based in the execution of shell commands.<br/>
64 * This class holds a <code>shell bash</code> program associated with the application, being
65 * a wrapper to execute all other programs (like shell does in linux), capturing the
66 * output (stdin and stderr) and the exit code of the program executed.
68 public abstract class ShellConsole extends Console implements Program.ProgramListener {
70 private static final String TAG = "ShellConsole"; //$NON-NLS-1$
72 // A timeout of 3 seconds should be enough for no-debugging environments
73 private static final long DEFAULT_TIMEOUT =
74 FileManagerApplication.isDebuggable() ? 20000L : 3000L;
76 // A maximum operation timeout independently of the isWaitOnNewDataReceipt
77 // of the program. A synchronous operation must not be more longer than
78 // MAX_OPERATION_TIMEOUT + DEFAULT_TIMEOUT
79 private static final long MAX_OPERATION_TIMEOUT = 30000L;
81 private static final int DEFAULT_BUFFER = 512;
84 private final Shell mShell;
85 private Identity mIdentity;
88 private final Object mSync = new Object();
92 final Object mPartialSync = new Object();
96 boolean mActive = false;
97 private boolean mFinished = true;
98 private boolean mNewData = false;
99 private Process mProc = null;
103 Program mActiveCommand = null;
114 private InputStream mIn = null;
115 private InputStream mErr = null;
116 private OutputStream mOut = null;
120 ByteArrayOutputStream mSbIn = null;
124 ByteArrayOutputStream mSbErr = null;
126 private final SecureRandom mRandom;
128 private ControlPatternInfo mControlPattern = new ControlPatternInfo();
129 private static class ControlPatternInfo {
130 String mStartId1, mStartId2;
131 String mEndId1, mEndId2;
132 byte[] mStartControlBytes;
133 int mEndControlLength;
135 public void setNewPattern(String s1, String s2, String e1, String e2) {
140 mStartControlBytes = (mStartId1 + "0" + mStartId2).getBytes();
141 mEndControlLength = mEndId1.length() + mEndId2.length() + 3; // leave 3 for exit code;
144 public byte[] getStartControlPatternBytes() {
145 return mStartControlBytes;
148 public int getEndControlPatternLength() {
149 return mEndControlLength;
152 public int[] getEndControlMatch(byte[] bytes) {
153 return getEndControlMatch(bytes, false);
157 * Find end control pattern indices
158 * @param bytes byte array to check against
159 * @param getExitIndices if true, this method will return the indices of the values
160 * in between the end control pattern (e.g. exit code), otherwise
161 * it will return the start and end indices of the whole end pattern
162 * @return if {@code getExitIndices} is false, See {@link #findControlPattern(byte[], byte[])}
163 * otherwise it will return values as described in {@code getExitIndices}
165 public int[] getEndControlMatch(byte[] bytes, boolean getExitIndices) {
166 // in the end control pattern, we check for endId1, 1-3 chars for exit code, endId2
169 final int[] end1PatternResult = findControlPattern(bytes, mEndId1.getBytes());
170 if (end1PatternResult != null) {
171 start = end1PatternResult[0];
173 final int[] end2PatternResult = findControlPattern(bytes, mEndId2.getBytes());
174 if (end2PatternResult != null) {
175 if (getExitIndices) {
176 return new int[]{end1PatternResult[1], end2PatternResult[0]};
179 end = end2PatternResult[1];
180 return new int[]{start, end};
187 * Find the start control pattern indices
188 * @param bytes to check against
189 * @return See {@link #findControlPattern(byte[], byte[])}
191 public int[] getStartControlMatch(byte[] bytes) {
192 return findControlPattern(bytes, getStartControlPatternBytes());
196 * Checks whether {@code controlBytes} exists inside of {@code bytes} and returns the start
197 * and end indices if it does. Returns null otherwise.
199 * @param bytes where to look for the pattern
200 * @param controlBytes the pattern to search for
201 * @return if {@code controlBytes} was found inside of {@code bytes} then this method will
202 * return an int[] array of length 2:
204 * <li>[0] the starting index of the control pattern
205 * <li>[1] the ending index of the control pattern + 1 (like {{@link java.lang.String#substring(int, int)})
208 private int[] findControlPattern(byte[] bytes, byte[] controlBytes) {
209 if (bytes.length >= controlBytes.length) {
210 for (int i = 0; i < bytes.length; i++) {
211 boolean foundControlBytePattern = true;
213 if (bytes[i] == controlBytes[0]) {
215 for (int j = start; j < controlBytes.length + start; j++) {
216 if (j > bytes.length - 1) {
217 foundControlBytePattern = false;
220 if (bytes[j] != controlBytes[j - start]) {
221 foundControlBytePattern = false;
227 if (foundControlBytePattern) {
228 return new int[]{start, patternEnd+1};
242 private final ShellExecutableFactory mExecutableFactory;
245 * Constructor of <code>ShellConsole</code>.
247 * @param shell The shell used to execute commands
248 * @throws FileNotFoundException If the initial directory not exists
249 * @throws IOException If initial directory couldn't be resolved
251 public ShellConsole(Shell shell)
252 throws FileNotFoundException, IOException {
255 this.mExecutableFactory = new ShellExecutableFactory(this);
257 this.mBufferSize = DEFAULT_BUFFER;
259 //Restart the buffers
260 this.mSbIn = new ByteArrayOutputStream();
261 this.mSbErr = new ByteArrayOutputStream();
263 //Generate an aleatory secure random generator
265 this.mRandom = SecureRandom.getInstance("SHA1PRNG"); //$NON-NLS-1$
266 } catch (Exception ex) {
267 throw new IOException(ex);
275 public ExecutableFactory getExecutableFactory() {
276 return this.mExecutableFactory;
283 public Identity getIdentity() {
284 return this.mIdentity;
288 * Method that returns the buffer size
290 * @return int The buffer size
292 public int getBufferSize() {
293 return this.mBufferSize;
297 * Method that sets the buffer size
299 * @param bufferSize the The buffer size
301 public void setBufferSize(int bufferSize) {
302 this.mBufferSize = bufferSize;
309 public final boolean isActive() {
317 public final void alloc() throws ConsoleAllocException {
319 //Create command string
320 List<String> cmd = new ArrayList<String>();
321 cmd.add(this.mShell.getCommand());
322 if (this.mShell.getArguments() != null && this.mShell.getArguments().length() > 0) {
323 cmd.add(this.mShell.getArguments());
327 Runtime rt = Runtime.getRuntime();
330 cmd.toArray(new String[cmd.size()]),
331 this.mShell.getEnvironment(),
332 new File(FileHelper.ROOT_DIRECTORY).getCanonicalFile());
333 synchronized (this.mSync) {
338 String.format("Create console %s, command: %s, args: %s, env: %s", //$NON-NLS-1$
340 this.mShell.getCommand(),
341 this.mShell.getArguments(),
342 Arrays.toString(this.mShell.getEnvironment())));
346 this.mIn = this.mProc.getInputStream();
347 this.mErr = this.mProc.getErrorStream();
348 this.mOut = this.mProc.getOutputStream();
349 if (this.mIn == null || this.mErr == null || this.mOut == null) {
352 } catch (Throwable ex) {
355 throw new ConsoleAllocException("Console buffer allocation error."); //$NON-NLS-1$
358 //Starts a thread for extract output, and check timeout
359 createStdInThread(this.mIn);
360 createStdErrThread(this.mErr);
362 //Wait for thread start
365 //Check if process its active
366 checkIfProcessExits();
367 synchronized (this.mSync) {
369 throw new ConsoleAllocException("Shell not started."); //$NON-NLS-1$
373 // Retrieve the PID of the shell
374 ProcessIdExecutable processIdCmd =
375 getExecutableFactory().
376 newCreator().createShellProcessIdExecutable();
377 // Wait indefinitely if the console is allocating a su command. We need to
378 // wait to user response to SuperUser or SuperSu prompt (or whatever it is)
379 // The rest of sync operations will run with a timeout.
380 execute(processIdCmd, this.isPrivileged());
383 pid = processIdCmd.getResult().get(0);
384 } catch (Exception e) {
388 throw new ConsoleAllocException(
389 "can't retrieve the PID of the shell."); //$NON-NLS-1$
391 this.mShell.setPid(pid.intValue());
394 IdentityExecutable identityCmd =
395 getExecutableFactory().newCreator().createIdentityExecutable();
396 execute(identityCmd, null);
397 this.mIdentity = identityCmd.getResult();
398 // Identity command is required for root console detection,
399 // but Groups command is not used for now. Also, this command is causing
400 // problems on some implementations (maybe toolbox?) which don't
401 // recognize the root AID and returns an error. Safely ignore on error.
403 if (this.mIdentity.getGroups().size() == 0) {
405 GroupsExecutable groupsCmd =
406 getExecutableFactory().newCreator().createGroupsExecutable();
407 execute(groupsCmd, null);
408 this.mIdentity.setGroups(groupsCmd.getResult());
410 } catch (Exception ex) {
411 Log.w(TAG, "Groups command failed. Ignored.", ex); //$NON-NLS-1$
414 } catch (Exception ex) {
417 } catch (Throwable ex2) {
420 throw new ConsoleAllocException("Console allocation error.", ex); //$NON-NLS-1$
429 public final void dealloc() {
430 synchronized (this.mSync) {
432 this.mActive = false;
433 this.mFinished = true;
437 if (this.mIn != null) {
440 } catch (Throwable ex) {
444 if (this.mErr != null) {
447 } catch (Throwable ex) {
451 if (this.mOut != null) {
454 } catch (Throwable ex) {
458 this.mProc.destroy();
459 } catch (Throwable e) {/**NON BLOCK**/}
473 public final void realloc() throws ConsoleAllocException {
482 public synchronized void execute(Executable executable, Context ctx)
483 throws ConsoleAllocException, InsufficientPermissionsException, NoSuchFileOrDirectory,
484 OperationTimeoutException, ExecutionException, CommandNotFoundException,
485 ReadOnlyFilesystemException {
486 execute(executable, false);
490 * Method for execute a command in the operating system layer.
492 * @param executable The executable command to be executed
493 * @param waitForSu Wait for su (do not used timeout)
494 * @throws ConsoleAllocException If the console is not allocated
495 * @throws InsufficientPermissionsException If an operation requires elevated permissions
496 * @throws NoSuchFileOrDirectory If the file or directory was not found
497 * @throws OperationTimeoutException If the operation exceeded the maximum time of wait
498 * @throws CommandNotFoundException If the executable program was not found
499 * @throws ExecutionException If the operation returns a invalid exit code
500 * @throws ReadOnlyFilesystemException If the operation writes in a read-only filesystem
502 private synchronized void execute(final Executable executable, final boolean waitForSu)
503 throws ConsoleAllocException, InsufficientPermissionsException,
504 CommandNotFoundException, NoSuchFileOrDirectory,
505 OperationTimeoutException, ExecutionException, ReadOnlyFilesystemException {
508 if (!(executable instanceof Program)) {
509 throw new CommandNotFoundException("executable not instanceof Program"); //$NON-NLS-1$
512 //Asynchronous or synchronous execution?
513 final Program program = (Program)executable;
514 if (executable instanceof AsyncResultExecutable) {
515 Thread asyncThread = new Thread(new Runnable() {
518 //Synchronous execution (but asynchronous running in a thread)
519 //This way syncExecute is locked until this thread ends
521 //Synchronous execution (2 tries with 1 reallocation)
522 final ShellConsole shell = ShellConsole.this;
523 if (shell.syncExecute(program, true, false)) {
524 shell.syncExecute(program, false, false);
526 } catch (Exception ex) {
527 if (((AsyncResultExecutable)executable).getAsyncResultListener() != null) {
528 ((AsyncResultExecutable)executable).
529 getAsyncResultListener().onException(ex);
532 Log.e(TAG, "Fail asynchronous execution", ex); //$NON-NLS-1$
539 //Synchronous execution (2 tries with 1 reallocation)
540 program.setExitOnStdErrOutput(waitForSu);
541 if (syncExecute(program, true, waitForSu) && !waitForSu) {
542 syncExecute(program, false, false);
548 * Method for execute a program command in the operating system layer in a synchronous way.
550 * @param program The program to execute
551 * @param reallocate If the console must be reallocated on i/o error
552 * @param waitForSu Wait for su (do not used timeout)
553 * @return boolean If the console was reallocated
554 * @throws ConsoleAllocException If the console is not allocated
555 * @throws InsufficientPermissionsException If an operation requires elevated permissions
556 * @throws CommandNotFoundException If the command was not found
557 * @throws NoSuchFileOrDirectory If the file or directory was not found
558 * @throws OperationTimeoutException If the operation exceeded the maximum time of wait
559 * @throws ExecutionException If the operation returns a invalid exit code
560 * @throws ReadOnlyFilesystemException If the operation writes in a read-only filesystem
563 synchronized boolean syncExecute(
564 final Program program, boolean reallocate, boolean waitForSu)
565 throws ConsoleAllocException, InsufficientPermissionsException,
566 CommandNotFoundException, NoSuchFileOrDirectory,
567 OperationTimeoutException, ExecutionException, ReadOnlyFilesystemException {
570 //Check the console status before send command
573 synchronized (this.mSync) {
575 throw new ConsoleAllocException("No console allocated"); //$NON-NLS-1$
579 //Saves the active command reference
580 this.mActiveCommand = program;
581 final boolean async = program instanceof AsyncResultProgram;
584 this.mStarted = false;
585 this.mCancelled = false;
586 this.mSbIn = new ByteArrayOutputStream();
587 this.mSbErr = new ByteArrayOutputStream();
589 //Random start/end identifiers
591 String.format("/#%d#/", Long.valueOf(this.mRandom.nextLong())); //$NON-NLS-1$
593 String.format("/#%d#/", Long.valueOf(this.mRandom.nextLong())); //$NON-NLS-1$
595 String.format("/#%d#/", Long.valueOf(this.mRandom.nextLong())); //$NON-NLS-1$
597 String.format("/#%d#/", Long.valueOf(this.mRandom.nextLong())); //$NON-NLS-1$
599 //Create command string
600 String cmd = program.getCommand();
601 String args = program.getArguments();
606 String.format("%s-%s, command: %s, args: %s", //$NON-NLS-1$
613 //Is asynchronous program? Then set asynchronous
614 program.setProgramListener(this);
616 ((AsyncResultProgram)program).setOnCancelListener(this);
617 ((AsyncResultProgram)program).setOnEndListener(this);
620 //Send the command + a control code with exit code
621 //The process has finished where control control code is present.
622 //This control code is unique in every invocation and is secure random
623 //generated (control code 1 + exit code + control code 2)
625 boolean hasEndControl = (!(program instanceof AsyncResultProgram) ||
626 (program instanceof AsyncResultProgram &&
627 ((AsyncResultProgram)program).isExpectEnd()));
628 mControlPattern.setNewPattern(startId1, startId2, endId1, endId2);
631 Command.getStartCodeCommandInfo(
632 FileManagerApplication.getInstance().getResources());
633 startCmd = String.format(
634 startCmd, "'" + startId1 +//$NON-NLS-1$
635 "'", "'" + startId2 + "'"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
637 Command.getExitCodeCommandInfo(
638 FileManagerApplication.getInstance().getResources());
639 endCmd = String.format(
640 endCmd, "'" + endId1 + //$NON-NLS-1$
641 "'", "'" + endId2 + "'"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
642 StringBuilder sb = new StringBuilder()
644 .append(" ") //$NON-NLS-1$
646 .append(" ") //$NON-NLS-1$
649 sb = sb.append(" ") //$NON-NLS-1$
652 sb.append(FileHelper.NEWLINE);
653 synchronized (this.mSync) {
654 this.mFinished = false;
655 this.mNewData = false;
656 this.mOut.write(sb.toString().getBytes());
658 } catch (InvalidCommandDefinitionException icdEx) {
659 throw new CommandNotFoundException(
660 "ExitCodeCommandInfo not found", icdEx); //$NON-NLS-1$
663 //Now, wait for buffers to be filled
664 synchronized (this.mSync) {
665 if (!this.mFinished) {
666 if (waitForSu || program.isIndefinitelyWait()) {
669 final long start = System.currentTimeMillis();
671 this.mSync.wait(DEFAULT_TIMEOUT);
672 if (!this.mFinished) {
673 final long end = System.currentTimeMillis();
674 if (!program.isWaitOnNewDataReceipt() ||
676 (end - start >= MAX_OPERATION_TIMEOUT)) {
677 throw new OperationTimeoutException(end - start, cmd);
680 // Still waiting for program ending
681 this.mNewData = false;
690 //End partial results?
692 synchronized (this.mPartialSync) {
693 ((AsyncResultProgram)program).onRequestEndParsePartialResult(this.mCancelled);
698 int exitCode = getExitCode(this.mSbIn, async);
700 synchronized (this.mPartialSync) {
701 ((AsyncResultProgram)program).onRequestExitCode(exitCode);
706 String.format("%s-%s, command: %s, exitCode: %s", //$NON-NLS-1$
710 String.valueOf(exitCode)));
713 //Check if invocation was successfully or not
714 if (!program.isIgnoreShellStdErrCheck()) {
715 //Wait for stderr buffer to be filled
719 } catch (Throwable ex) {/**NON BLOCK**/}
721 this.mShell.checkStdErr(this.mActiveCommand, exitCode, this.mSbErr.toString());
723 this.mShell.checkExitCode(exitCode);
724 program.checkExitCode(exitCode);
725 program.checkStdErr(exitCode, this.mSbErr.toString());
727 //Parse the result? Only if not partial results
728 if (program instanceof SyncResultProgram) {
730 ((SyncResultProgram)program).parse(
731 this.mSbIn.toString(), this.mSbErr.toString());
732 } catch (ParseException pEx) {
733 throw new ExecutionException(
734 "SyncResultProgram parse failed", pEx); //$NON-NLS-1$
738 //Invocation finished. Now program.getResult() has the result of
739 //the operation, if any exists
741 } catch (OperationTimeoutException otEx) {
743 killCurrentCommand();
744 } catch (Exception e) { /**NON BLOCK **/}
747 } catch (IOException ioEx) {
752 throw new ExecutionException("Console allocation error.", ioEx); //$NON-NLS-1$
754 } catch (InterruptedException ioEx) {
759 throw new ExecutionException("Console allocation error.", ioEx); //$NON-NLS-1$
762 //Dereference the active command
763 this.mActiveCommand = null;
771 * Method that creates the standard input thread for read program response.
773 * @param in The standard input buffer
774 * @return Thread The standard input thread
776 private Thread createStdInThread(final InputStream in) {
777 Thread t = new Thread(new Runnable() {
778 @SuppressWarnings("synthetic-access")
781 final ShellConsole shell = ShellConsole.this;
783 ByteArrayOutputStream sb = null;
785 while (shell.mActive) {
786 //Read only one byte with active wait
787 final int r = in.read();
794 shell.mActiveCommand != null &&
795 shell.mActiveCommand instanceof AsyncResultProgram;
796 if (!async || sb == null) {
797 sb = new ByteArrayOutputStream();
800 if (!shell.mCancelled) {
801 shell.mSbIn.write(r);
802 if (!shell.mStarted) {
803 shell.mStarted = isCommandStarted(shell.mSbIn);
804 if (shell.mStarted) {
805 byte[] bytes = shell.mSbIn.toByteArray();
806 sb = new ByteArrayOutputStream();
807 sb.write(bytes, 0, bytes.length);
809 synchronized (shell.mPartialSync) {
810 ((AsyncResultProgram)
811 shell.mActiveCommand).
812 onRequestStartParsePartialResult();
816 byte[] data = shell.mSbIn.toByteArray();
817 sb.write(data, 0, data.length);
826 //Check if the command has finished (and extract the control)
827 boolean finished = isCommandFinished(shell.mSbIn, sb);
829 //Notify asynchronous partial data
830 if (shell.mStarted && async) {
831 AsyncResultProgram program =
832 ((AsyncResultProgram)shell.mActiveCommand);
833 String partial = sb.toString();
834 if (partial.length() >= shell.mControlPattern
835 .getEndControlPatternLength()) {
836 program.onRequestParsePartialResult(sb.toByteArray());
837 shell.toStdIn(partial);
839 // Reset the temp buffer
840 sb = new ByteArrayOutputStream();
846 shell.toStdIn(String.valueOf((char)r));
848 AsyncResultProgram program =
849 ((AsyncResultProgram)shell.mActiveCommand);
850 String partial = sb.toString();
851 if (program != null) {
852 program.onRequestParsePartialResult(sb.toByteArray());
854 shell.toStdIn(partial);
858 notifyProcessFinished();
861 if (!async && !finished) {
862 shell.toStdIn(String.valueOf((char)r));
866 //Has more data? Read with available as more as exists
867 //or maximum loop count is rebased
869 while (in.available() > 0 && count < 10) {
872 Math.min(in.available(), shell.mBufferSize);
873 byte[] data = new byte[available];
874 read = in.read(data);
878 shell.mActiveCommand != null &&
879 shell.mActiveCommand instanceof AsyncResultProgram;
881 // Exit if active command is cancelled
882 if (shell.mCancelled) continue;
884 final String s = new String(data, 0, read);
885 shell.mSbIn.write(data, 0, read);
886 if (!shell.mStarted) {
887 shell.mStarted = isCommandStarted(shell.mSbIn);
888 if (shell.mStarted) {
889 byte[] bytes = shell.mSbIn.toByteArray();
890 sb = new ByteArrayOutputStream();
891 sb.write(bytes, 0, bytes.length);
893 synchronized (shell.mPartialSync) {
894 AsyncResultProgram p =
895 ((AsyncResultProgram)shell.mActiveCommand);
897 p.onRequestStartParsePartialResult();
902 byte[] bytes = shell.mSbIn.toByteArray();
903 sb.write(bytes, 0, bytes.length);
906 sb.write(data, 0, read);
912 //Check if the command has finished (and extract the control)
913 boolean finished = isCommandFinished(shell.mSbIn, sb);
915 //Notify asynchronous partial data
917 AsyncResultProgram program = ((AsyncResultProgram)shell.mActiveCommand);
918 String partial = sb.toString();
919 if (partial.length() >= shell.mControlPattern
920 .getEndControlPatternLength()) {
921 if (program != null) {
922 program.onRequestParsePartialResult(sb.toByteArray());
924 shell.toStdIn(partial);
926 // Reset the temp buffer
927 sb = new ByteArrayOutputStream();
935 AsyncResultProgram program =
936 ((AsyncResultProgram)shell.mActiveCommand);
937 String partial = sb.toString();
938 if (program != null) {
939 program.onRequestParsePartialResult(sb.toByteArray());
941 shell.toStdIn(partial);
945 notifyProcessFinished();
948 if (!async && !finished) {
952 //Wait for buffer to be filled
955 } catch (Throwable ex) {/**NON BLOCK**/}
958 //Asynchronous programs can cause a lot of output, control buffers
959 //for a low memory footprint
961 trimBuffer(shell.mSbIn);
962 trimBuffer(shell.mSbErr);
965 //Check if process has exited
966 checkIfProcessExits();
968 } catch (Exception ioEx) {
969 notifyProcessExit(ioEx);
973 t.setName(String.format("%s", "stdin")); //$NON-NLS-1$//$NON-NLS-2$
979 * Method that echoes the stdin
981 * @param stdin The buffer of the stdin
984 void toStdIn(String stdin) {
985 //Audit (if not cancelled)
986 if (!this.mCancelled && isTrace() && stdin.length() > 0) {
989 "stdin: %s", stdin)); //$NON-NLS-1$
994 * Method that creates the standard error thread for read program response.
996 * @param err The standard error buffer
997 * @return Thread The standard error thread
999 private Thread createStdErrThread(final InputStream err) {
1000 Thread t = new Thread(new Runnable() {
1003 final ShellConsole shell = ShellConsole.this;
1006 while (shell.mActive) {
1007 //Read only one byte with active wait
1013 // Has the process received something that we dont expect?
1014 if (shell.mActiveCommand != null &&
1015 shell.mActiveCommand.isExitOnStdErrOutput()) {
1016 notifyProcessFinished();
1022 shell.mActiveCommand != null &&
1023 shell.mActiveCommand instanceof AsyncResultProgram;
1025 ByteArrayOutputStream sb = new ByteArrayOutputStream();
1026 if (!shell.mCancelled) {
1027 shell.mSbErr.write(r);
1030 //Notify asynchronous partial data
1031 if (shell.mStarted && async) {
1032 AsyncResultProgram program =
1033 ((AsyncResultProgram)shell.mActiveCommand);
1034 if (program != null) {
1035 program.parsePartialErrResult(new String(new char[]{(char)r}));
1039 toStdErr(sb.toString());
1042 // New data received
1045 //Has more data? Read with available as more as exists
1046 //or maximum loop count is rebased
1048 while (err.available() > 0 && count < 10) {
1050 int available = Math.min(err.available(), shell.mBufferSize);
1051 byte[] data = new byte[available];
1052 read = err.read(data);
1056 shell.mActiveCommand != null &&
1057 shell.mActiveCommand instanceof AsyncResultProgram;
1060 String s = new String(data, 0, read);
1061 shell.mSbErr.write(data, 0, read);
1062 sb.write(data, 0, read);
1064 //Notify asynchronous partial data
1066 AsyncResultProgram program =
1067 ((AsyncResultProgram)shell.mActiveCommand);
1068 if (program != null) {
1069 program.parsePartialErrResult(s);
1074 // Has the process received something that we dont expect?
1075 if (shell.mActiveCommand != null &&
1076 shell.mActiveCommand.isExitOnStdErrOutput()) {
1077 notifyProcessFinished();
1081 // New data received
1084 //Wait for buffer to be filled
1087 } catch (Throwable ex) {
1092 //Asynchronous programs can cause a lot of output, control buffers
1093 //for a low memory footprint
1094 if (shell.mActiveCommand != null &&
1095 shell.mActiveCommand instanceof AsyncResultProgram) {
1096 trimBuffer(shell.mSbIn);
1097 trimBuffer(shell.mSbErr);
1100 } catch (Exception ioEx) {
1101 notifyProcessExit(ioEx);
1105 t.setName(String.format("%s", "stderr")); //$NON-NLS-1$//$NON-NLS-2$
1111 * Method that echoes the stderr
1113 * @param stdin The buffer of the stderr
1116 void toStdErr(String stderr) {
1117 //Audit (if not cancelled)
1118 if (!this.mCancelled && isTrace()) {
1121 "stderr: %s", stderr)); //$NON-NLS-1$
1126 * Method that checks the console status and restart the console
1127 * if this is unusable.
1129 * @throws ConsoleAllocException If the console can't be reallocated
1131 private void checkConsole() throws ConsoleAllocException {
1133 //Test write something to the buffer
1134 this.mOut.write(FileHelper.NEWLINE.getBytes());
1135 this.mOut.write(FileHelper.NEWLINE.getBytes());
1136 } catch (IOException ioex) {
1137 //Something is wrong with the buffers. Reallocate console.
1139 "Something is wrong with the console buffers. Reallocate console.", //$NON-NLS-1$
1142 //Reallocate the damage console
1148 * Method that verifies if the process had exited.
1151 void checkIfProcessExits() {
1153 if (this.mProc != null) {
1154 synchronized (this.mSync) {
1155 this.mProc.exitValue();
1157 this.mActive = false; //Exited
1159 } catch (IllegalThreadStateException itsEx) {
1165 * Method that notifies the ending of the process.
1167 * @param ex The exception, only if the process exit with a exception.
1171 void notifyProcessExit(Exception ex) {
1172 synchronized (this.mSync) {
1174 this.mActive = false;
1175 this.mFinished = true;
1176 this.mSync.notify();
1178 Log.w(TAG, "Exit with exception", ex); //$NON-NLS-1$
1185 * Method that notifies the ending of the command execution.
1188 void notifyProcessFinished() {
1189 synchronized (this.mSync) {
1191 this.mFinished = true;
1192 this.mSync.notify();
1198 * Method that returns if the command has started by checking the
1199 * standard input buffer. This method also removes the control start command
1200 * from the buffer, if it's present, leaving in the buffer the new data bytes.
1202 * @param stdin The standard in buffer
1203 * @return boolean If the command has started
1206 boolean isCommandStarted(ByteArrayOutputStream stdin) {
1207 if (stdin == null) return false;
1208 byte[] data = stdin.toByteArray();
1209 final int[] match = mControlPattern.getStartControlMatch(data);
1210 if (match != null) {
1212 stdin.write(data, match[1], data.length - match[1]);
1219 * Method that returns if the command has finished by checking the
1220 * standard input buffer.
1222 * @param stdin The standard in buffer
1223 * @return boolean If the command has finished
1226 boolean isCommandFinished(ByteArrayOutputStream stdin, ByteArrayOutputStream partial) {
1227 if (stdin == null) return false;
1229 int[] match = mControlPattern.getEndControlMatch(stdin.toByteArray());
1230 boolean ret = match != null;
1232 if (ret && partial != null) {
1234 byte[] bytes = partial.toByteArray();
1235 match = mControlPattern.getEndControlMatch(bytes);
1237 if (match != null) {
1239 partial.write(bytes, match[0], match[1]);
1246 * New data was received
1250 synchronized (this.mSync) {
1251 this.mNewData = true;
1256 * Method that returns the exit code of the last executed command.
1258 * @param stdin The standard in buffer
1259 * @return int The exit code of the last executed command
1261 private int getExitCode(ByteArrayOutputStream stdin, boolean async) {
1262 // If process was cancelled, don't expect a exit code.
1263 // Returns always 143 code
1264 if (this.mCancelled) {
1268 byte[] bytes = stdin.toByteArray();
1269 int[] match = mControlPattern.getEndControlMatch(bytes);
1271 if (match != null) {
1274 mSbIn.write(bytes, 0, match[0]);
1276 // find the indices of the exit code and extract it
1277 match = mControlPattern.getEndControlMatch(bytes, true);
1278 return Integer.parseInt(new String(bytes, match[0], match[1] - match[0]));
1284 * Method that trim a buffer, let in the buffer some
1285 * text to ensure that the exit code is in there.
1287 * @param sb The buffer to trim
1290 @SuppressWarnings("static-method") void trimBuffer(ByteArrayOutputStream sb) {
1291 final int bufferSize = mControlPattern
1292 .getEndControlPatternLength();
1293 if (sb.size() > bufferSize) {
1294 byte[] data = sb.toByteArray();
1296 sb.write(data, data.length - bufferSize, bufferSize);
1301 * Method that kill the current command.
1303 * @return boolean If the program was killed
1306 private boolean killCurrentCommand() {
1307 synchronized (this.mSync) {
1308 // Check background console
1310 FileManagerApplication.getBackgroundConsole();
1311 } catch (Exception e) {
1312 Log.w(TAG, "There is not background console. Not allowed.", e); //$NON-NLS-1$
1316 if (this.mActiveCommand != null && this.mActiveCommand.getCommand() != null) {
1318 boolean isCancellable = true;
1319 if (this.mActiveCommand instanceof AsyncResultProgram) {
1320 final AsyncResultProgram asyncCmd =
1321 (AsyncResultProgram)this.mActiveCommand;
1322 isCancellable = asyncCmd.isCancellable();
1325 if (isCancellable) {
1327 //Get the PIDs in background
1328 List<Integer> pids =
1329 CommandHelper.getProcessesIds(
1331 this.mShell.getPid(),
1332 FileManagerApplication.getBackgroundConsole());
1333 for (Integer pid: pids) {
1335 CommandHelper.sendSignal(
1338 FileManagerApplication.getBackgroundConsole());
1340 //Wait for process to be killed
1342 } catch (Throwable ex) {
1350 this.mCancelled = true;
1351 notifyProcessFinished();
1352 this.mSync.notify();
1355 } catch (Throwable ex) {
1357 String.format("Unable to kill current program: %s",
1359 (this.mActiveCommand == null) ?
1361 this.mActiveCommand.getCommand()
1371 * Method that send a signal to the current command.
1373 * @param SIGNAL The signal to send
1374 * @return boolean If the signal was sent
1377 private boolean sendSignalToCurrentCommand(SIGNAL signal) {
1378 synchronized (this.mSync) {
1379 // Check background console
1381 FileManagerApplication.getBackgroundConsole();
1382 } catch (Exception e) {
1383 Log.w(TAG, "There is not background console. Not allowed.", e); //$NON-NLS-1$
1387 if (this.mActiveCommand.getCommand() != null) {
1389 boolean isCancellable = true;
1390 if (this.mActiveCommand instanceof AsyncResultProgram) {
1391 final AsyncResultProgram asyncCmd =
1392 (AsyncResultProgram)this.mActiveCommand;
1393 isCancellable = asyncCmd.isCancellable();
1396 if (isCancellable) {
1398 //Get the PIDs in background
1399 List<Integer> pids =
1400 CommandHelper.getProcessesIds(
1402 this.mShell.getPid(),
1403 FileManagerApplication.getBackgroundConsole());
1404 for (Integer pid: pids) {
1406 CommandHelper.sendSignal(
1410 FileManagerApplication.getBackgroundConsole());
1412 //Wait for process to be signaled
1414 } catch (Throwable ex) {
1422 this.mCancelled = true;
1423 notifyProcessFinished();
1424 this.mSync.notify();
1427 } catch (Throwable ex) {
1429 String.format("Unable to send signal to current program: %s", //$NON-NLS-1$
1430 this.mActiveCommand.getCommand()), ex);
1441 public boolean onEnd() {
1442 //Kill the current command on end request
1443 return killCurrentCommand();
1450 public boolean onSendSignal(SIGNAL signal) {
1451 //Send a signal to the current command on end request
1452 return sendSignalToCurrentCommand(signal);
1459 public boolean onCancel() {
1460 //Kill the current command on cancel request
1461 return killCurrentCommand();
1468 public boolean onRequestWrite(
1469 byte[] data, int offset, int byteCount) throws ExecutionException {
1471 // Method that write to the stdin the data requested by the program
1472 if (this.mOut != null) {
1473 this.mOut.write(data, offset, byteCount);
1478 } catch (Exception ex) {
1479 String msg = String.format("Unable to write data to program: %s", //$NON-NLS-1$
1480 this.mActiveCommand.getCommand());
1481 Log.e(TAG, msg, ex);
1482 throw new ExecutionException(msg, ex);
1491 public OutputStream getOutputStream() {