1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 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 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 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. 15 */ 16 17 package com.android.tradefed.util; 18 19 import com.android.tradefed.log.LogUtil.CLog; 20 21 import com.google.common.annotations.VisibleForTesting; 22 23 import java.io.BufferedOutputStream; 24 import java.io.ByteArrayOutputStream; 25 import java.io.File; 26 import java.io.IOException; 27 import java.io.InputStream; 28 import java.io.OutputStream; 29 import java.util.Arrays; 30 import java.util.HashMap; 31 import java.util.HashSet; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.Set; 35 import java.util.Timer; 36 import java.util.TimerTask; 37 import java.util.concurrent.CountDownLatch; 38 import java.util.concurrent.TimeUnit; 39 40 /** 41 * A collection of helper methods for executing operations. 42 */ 43 public class RunUtil implements IRunUtil { 44 45 public static final String RUNNABLE_NOTIFIER_NAME = "RunnableNotifier"; 46 public static final String INHERITIO_PREFIX = "inheritio-"; 47 48 private static final int POLL_TIME_INCREASE_FACTOR = 4; 49 private static final long THREAD_JOIN_POLL_INTERVAL = 30 * 1000; 50 private static final long IO_THREAD_JOIN_INTERVAL = 5 * 1000; 51 private static final long PROCESS_DESTROY_TIMEOUT_SEC = 2; 52 private static IRunUtil sDefaultInstance = null; 53 private File mWorkingDir = null; 54 private Map<String, String> mEnvVariables = new HashMap<String, String>(); 55 private Set<String> mUnsetEnvVariables = new HashSet<String>(); 56 private EnvPriority mEnvVariablePriority = EnvPriority.UNSET; 57 private ThreadLocal<Boolean> mIsInterruptAllowed = new ThreadLocal<Boolean>() { 58 @Override 59 protected Boolean initialValue() { 60 return Boolean.FALSE; 61 } 62 }; 63 private Map<Long, String> mInterruptThreads = new HashMap<>(); 64 private ThreadLocal<Timer> mWatchdogInterrupt = null; 65 private boolean mInterruptibleGlobal = false; 66 67 /** 68 * Create a new {@link RunUtil} object to use. 69 */ 70 public RunUtil() { 71 } 72 73 /** 74 * Get a reference to the default {@link RunUtil} object. 75 * <p/> 76 * This is useful for callers who want to use IRunUtil without customization. 77 * Its recommended that callers who do need a custom IRunUtil instance 78 * (ie need to call either {@link #setEnvVariable(String, String)} or 79 * {@link #setWorkingDir(File)} create their own copy. 80 */ 81 public static IRunUtil getDefault() { 82 if (sDefaultInstance == null) { 83 sDefaultInstance = new RunUtil(); 84 } 85 return sDefaultInstance; 86 } 87 88 /** 89 * {@inheritDoc} 90 */ 91 @Override 92 public synchronized void setWorkingDir(File dir) { 93 if (this.equals(sDefaultInstance)) { 94 throw new UnsupportedOperationException("Cannot setWorkingDir on default RunUtil"); 95 } 96 mWorkingDir = dir; 97 } 98 99 /** 100 * {@inheritDoc} 101 */ 102 @Override 103 public synchronized void setEnvVariable(String name, String value) { 104 if (this.equals(sDefaultInstance)) { 105 throw new UnsupportedOperationException("Cannot setEnvVariable on default RunUtil"); 106 } 107 mEnvVariables.put(name, value); 108 } 109 110 /** 111 * {@inheritDoc} 112 * Environment variables may inherit from the parent process, so we need to delete 113 * the environment variable from {@link ProcessBuilder#environment()} 114 * 115 * @param key the variable name 116 * @see ProcessBuilder#environment() 117 */ 118 @Override 119 public synchronized void unsetEnvVariable(String key) { 120 if (this.equals(sDefaultInstance)) { 121 throw new UnsupportedOperationException("Cannot unsetEnvVariable on default RunUtil"); 122 } 123 mUnsetEnvVariables.add(key); 124 } 125 126 /** 127 * {@inheritDoc} 128 */ 129 @Override 130 public CommandResult runTimedCmd(final long timeout, final String... command) { 131 return runTimedCmd(timeout, null, null, true, command); 132 } 133 134 /** 135 * {@inheritDoc} 136 */ 137 @Override 138 public CommandResult runTimedCmd(final long timeout, OutputStream stdout, 139 OutputStream stderr, final String... command) { 140 return runTimedCmd(timeout, stdout, stderr, false, command); 141 } 142 143 /** 144 * Helper method to do a runTimeCmd call with or without outputStream specified. 145 * 146 * @return a {@CommandResult} containing results from command 147 */ 148 private CommandResult runTimedCmd(final long timeout, OutputStream stdout, 149 OutputStream stderr, boolean closeStreamAfterRun, final String... command) { 150 final CommandResult result = new CommandResult(); 151 IRunUtil.IRunnableResult osRunnable = 152 createRunnableResult(result, stdout, stderr, closeStreamAfterRun, command); 153 CommandStatus status = runTimed(timeout, osRunnable, true); 154 result.setStatus(status); 155 return result; 156 } 157 158 /** 159 * Create a {@link com.android.tradefed.util.IRunUtil.IRunnableResult} that will run the 160 * command. 161 */ 162 @VisibleForTesting 163 IRunUtil.IRunnableResult createRunnableResult( 164 CommandResult result, 165 OutputStream stdout, 166 OutputStream stderr, 167 boolean closeStreamAfterRun, 168 String... command) { 169 return new RunnableResult( 170 result, null, createProcessBuilder(command), stdout, stderr, closeStreamAfterRun); 171 } 172 173 /** {@inheritDoc} */ 174 @Override 175 public CommandResult runTimedCmdRetry( 176 long timeout, long retryInterval, int attempts, String... command) { 177 CommandResult result = null; 178 int counter = 0; 179 while (counter < attempts) { 180 result = runTimedCmd(timeout, command); 181 if (CommandStatus.SUCCESS.equals(result.getStatus())) { 182 return result; 183 } 184 sleep(retryInterval); 185 counter++; 186 } 187 return result; 188 } 189 190 private synchronized ProcessBuilder createProcessBuilder(String... command) { 191 return createProcessBuilder(Arrays.asList(command)); 192 } 193 194 private synchronized ProcessBuilder createProcessBuilder(List<String> commandList) { 195 ProcessBuilder processBuilder = new ProcessBuilder(); 196 if (mWorkingDir != null) { 197 processBuilder.directory(mWorkingDir); 198 } 199 // By default unset an env. for process has higher priority, but in some case we might want 200 // the 'set' to have priority. 201 if (EnvPriority.UNSET.equals(mEnvVariablePriority)) { 202 if (!mEnvVariables.isEmpty()) { 203 processBuilder.environment().putAll(mEnvVariables); 204 } 205 if (!mUnsetEnvVariables.isEmpty()) { 206 // in this implementation, the unsetEnv's priority is higher than set. 207 processBuilder.environment().keySet().removeAll(mUnsetEnvVariables); 208 } 209 } else { 210 if (!mUnsetEnvVariables.isEmpty()) { 211 processBuilder.environment().keySet().removeAll(mUnsetEnvVariables); 212 } 213 if (!mEnvVariables.isEmpty()) { 214 // in this implementation, the setEnv's priority is higher than set. 215 processBuilder.environment().putAll(mEnvVariables); 216 } 217 } 218 return processBuilder.command(commandList); 219 } 220 221 /** 222 * {@inheritDoc} 223 */ 224 @Override 225 public CommandResult runTimedCmdWithInput(final long timeout, String input, 226 final String... command) { 227 return runTimedCmdWithInput(timeout, input, ArrayUtil.list(command)); 228 } 229 230 /** 231 * {@inheritDoc} 232 */ 233 @Override 234 public CommandResult runTimedCmdWithInput(final long timeout, String input, 235 final List<String> command) { 236 final CommandResult result = new CommandResult(); 237 IRunUtil.IRunnableResult osRunnable = new RunnableResult(result, input, 238 createProcessBuilder(command)); 239 CommandStatus status = runTimed(timeout, osRunnable, true); 240 result.setStatus(status); 241 return result; 242 } 243 244 /** 245 * {@inheritDoc} 246 */ 247 @Override 248 public CommandResult runTimedCmdSilently(final long timeout, final String... command) { 249 final CommandResult result = new CommandResult(); 250 IRunUtil.IRunnableResult osRunnable = new RunnableResult(result, null, 251 createProcessBuilder(command)); 252 CommandStatus status = runTimed(timeout, osRunnable, false); 253 result.setStatus(status); 254 return result; 255 } 256 257 /** 258 * {@inheritDoc} 259 */ 260 @Override 261 public CommandResult runTimedCmdSilentlyRetry(long timeout, long retryInterval, int attempts, 262 String... command) { 263 CommandResult result = null; 264 int counter = 0; 265 while (counter < attempts) { 266 result = runTimedCmdSilently(timeout, command); 267 if (CommandStatus.SUCCESS.equals(result.getStatus())) { 268 return result; 269 } 270 sleep(retryInterval); 271 counter++; 272 } 273 return result; 274 } 275 276 /** 277 * {@inheritDoc} 278 */ 279 @Override 280 public Process runCmdInBackground(final String... command) throws IOException { 281 final String fullCmd = Arrays.toString(command); 282 CLog.v("Running %s", fullCmd); 283 return createProcessBuilder(command).start(); 284 } 285 286 /** 287 * {@inheritDoc} 288 */ 289 @Override 290 public Process runCmdInBackground(final List<String> command) throws IOException { 291 CLog.v("Running %s", command); 292 return createProcessBuilder(command).start(); 293 } 294 295 /** 296 * {@inheritDoc} 297 */ 298 @Override 299 public Process runCmdInBackground(List<String> command, OutputStream output) 300 throws IOException { 301 CLog.v("Running %s", command); 302 Process process = createProcessBuilder(command).start(); 303 inheritIO( 304 process.getInputStream(), 305 output, 306 String.format(INHERITIO_PREFIX + "stdout-%s", command)); 307 inheritIO( 308 process.getErrorStream(), 309 output, 310 String.format(INHERITIO_PREFIX + "stderr-%s", command)); 311 return process; 312 } 313 314 /** 315 * {@inheritDoc} 316 */ 317 @Override 318 public CommandStatus runTimed(long timeout, IRunUtil.IRunnableResult runnable, 319 boolean logErrors) { 320 checkInterrupted(); 321 RunnableNotifier runThread = new RunnableNotifier(runnable, logErrors); 322 if (logErrors) { 323 if (timeout > 0l) { 324 CLog.d("Running command with timeout: %dms", timeout); 325 } else { 326 CLog.d("Running command without timeout."); 327 } 328 } 329 runThread.start(); 330 long startTime = System.currentTimeMillis(); 331 long pollIterval = 0; 332 if (timeout > 0l && timeout < THREAD_JOIN_POLL_INTERVAL) { 333 // only set the pollInterval if we have a timeout 334 pollIterval = timeout; 335 } else { 336 pollIterval = THREAD_JOIN_POLL_INTERVAL; 337 } 338 do { 339 try { 340 runThread.join(pollIterval); 341 } catch (InterruptedException e) { 342 if (mIsInterruptAllowed.get()) { 343 CLog.i("runTimed: interrupted while joining the runnable"); 344 break; 345 } 346 else { 347 CLog.i("runTimed: received an interrupt but uninterruptible mode, ignoring"); 348 } 349 } 350 checkInterrupted(); 351 } while ((timeout == 0l || (System.currentTimeMillis() - startTime) < timeout) 352 && runThread.isAlive()); 353 // Snapshot the status when out of the run loop because thread may terminate and return a 354 // false FAILED instead of TIMED_OUT. 355 CommandStatus status = runThread.getStatus(); 356 if (CommandStatus.TIMED_OUT.equals(status) || CommandStatus.EXCEPTION.equals(status)) { 357 CLog.i("runTimed: Calling interrupt, status is %s", status); 358 runThread.cancel(); 359 } 360 checkInterrupted(); 361 return status; 362 } 363 364 /** 365 * {@inheritDoc} 366 */ 367 @Override 368 public boolean runTimedRetry(long opTimeout, long pollInterval, int attempts, 369 IRunUtil.IRunnableResult runnable) { 370 for (int i = 0; i < attempts; i++) { 371 if (runTimed(opTimeout, runnable, true) == CommandStatus.SUCCESS) { 372 return true; 373 } 374 CLog.d("operation failed, waiting for %d ms", pollInterval); 375 sleep(pollInterval); 376 } 377 return false; 378 } 379 380 /** 381 * {@inheritDoc} 382 */ 383 @Override 384 public boolean runFixedTimedRetry(final long opTimeout, final long pollInterval, 385 final long maxTime, final IRunUtil.IRunnableResult runnable) { 386 final long initialTime = getCurrentTime(); 387 while (getCurrentTime() < (initialTime + maxTime)) { 388 if (runTimed(opTimeout, runnable, true) == CommandStatus.SUCCESS) { 389 return true; 390 } 391 CLog.d("operation failed, waiting for %d ms", pollInterval); 392 sleep(pollInterval); 393 } 394 return false; 395 } 396 397 /** 398 * {@inheritDoc} 399 */ 400 @Override 401 public boolean runEscalatingTimedRetry(final long opTimeout, 402 final long initialPollInterval, final long maxPollInterval, final long maxTime, 403 final IRunUtil.IRunnableResult runnable) { 404 // wait an initial time provided 405 long pollInterval = initialPollInterval; 406 final long initialTime = getCurrentTime(); 407 while (true) { 408 if (runTimed(opTimeout, runnable, true) == CommandStatus.SUCCESS) { 409 return true; 410 } 411 long remainingTime = maxTime - (getCurrentTime() - initialTime); 412 if (remainingTime <= 0) { 413 CLog.d("operation is still failing after retrying for %d ms", maxTime); 414 return false; 415 } else if (remainingTime < pollInterval) { 416 // cap pollInterval to a max of remainingTime 417 pollInterval = remainingTime; 418 } 419 CLog.d("operation failed, waiting for %d ms", pollInterval); 420 sleep(pollInterval); 421 // somewhat arbitrarily, increase the poll time by a factor of 4 for each attempt, 422 // up to the previously decided maximum 423 pollInterval *= POLL_TIME_INCREASE_FACTOR; 424 if (pollInterval > maxPollInterval) { 425 pollInterval = maxPollInterval; 426 } 427 } 428 } 429 430 /** 431 * Retrieves the current system clock time. 432 * <p/> 433 * Exposed so it can be mocked for unit testing 434 */ 435 long getCurrentTime() { 436 return System.currentTimeMillis(); 437 } 438 439 /** 440 * {@inheritDoc} 441 */ 442 @Override 443 public void sleep(long time) { 444 checkInterrupted(); 445 if (time <= 0) { 446 return; 447 } 448 try { 449 Thread.sleep(time); 450 } catch (InterruptedException e) { 451 // ignore 452 CLog.d("sleep interrupted"); 453 } 454 checkInterrupted(); 455 } 456 457 /** 458 * {@inheritDoc} 459 */ 460 @Override 461 public void allowInterrupt(boolean allow) { 462 CLog.d("run interrupt allowed: %s", allow); 463 mIsInterruptAllowed.set(allow); 464 checkInterrupted(); 465 } 466 467 /** 468 * {@inheritDoc} 469 */ 470 @Override 471 public boolean isInterruptAllowed() { 472 return mIsInterruptAllowed.get(); 473 } 474 475 /** 476 * {@inheritDoc} 477 */ 478 @Override 479 public void setInterruptibleInFuture(Thread thread, final long timeMs) { 480 if (mWatchdogInterrupt == null) { 481 mWatchdogInterrupt = new ThreadLocal<Timer>() { 482 @Override 483 protected Timer initialValue() { 484 return new Timer(true); 485 } 486 }; 487 CLog.w("Setting future interruption in %s ms", timeMs); 488 mWatchdogInterrupt.get().schedule(new InterruptTask(thread), timeMs); 489 } else { 490 CLog.w("Future interruptible state already set."); 491 } 492 } 493 494 /** 495 * {@inheritDoc} 496 */ 497 @Override 498 public synchronized void interrupt(Thread thread, String message) { 499 if (message == null) { 500 throw new IllegalArgumentException("message cannot be null."); 501 } 502 mInterruptThreads.put(thread.getId(), message); 503 } 504 505 private synchronized void checkInterrupted() { 506 final long threadId = Thread.currentThread().getId(); 507 if (mInterruptibleGlobal) { 508 // If the global flag is on, meaning everything must terminate. 509 if (!isInterruptAllowed()) { 510 allowInterrupt(true); 511 } 512 } 513 if (isInterruptAllowed()) { 514 final String message = mInterruptThreads.remove(threadId); 515 if (message != null) { 516 Thread.currentThread().interrupt(); 517 throw new RunInterruptedException(message); 518 } 519 } 520 } 521 522 /** 523 * Helper thread that wraps a runnable, and notifies when done. 524 */ 525 private static class RunnableNotifier extends Thread { 526 527 private final IRunUtil.IRunnableResult mRunnable; 528 private CommandStatus mStatus = CommandStatus.TIMED_OUT; 529 private boolean mLogErrors = true; 530 531 RunnableNotifier(IRunUtil.IRunnableResult runnable, boolean logErrors) { 532 // Set this thread to be a daemon so that it does not prevent 533 // TF from shutting down. 534 setName(RUNNABLE_NOTIFIER_NAME); 535 setDaemon(true); 536 mRunnable = runnable; 537 mLogErrors = logErrors; 538 } 539 540 @Override 541 public void run() { 542 CommandStatus status; 543 try { 544 status = mRunnable.run() ? CommandStatus.SUCCESS : CommandStatus.FAILED; 545 } catch (InterruptedException e) { 546 CLog.i("runutil interrupted"); 547 status = CommandStatus.EXCEPTION; 548 } catch (Exception e) { 549 if (mLogErrors) { 550 CLog.e("Exception occurred when executing runnable"); 551 CLog.e(e); 552 } 553 status = CommandStatus.EXCEPTION; 554 } 555 synchronized (this) { 556 mStatus = status; 557 } 558 } 559 560 public void cancel() { 561 mRunnable.cancel(); 562 } 563 564 synchronized CommandStatus getStatus() { 565 return mStatus; 566 } 567 } 568 569 class RunnableResult implements IRunUtil.IRunnableResult { 570 private final ProcessBuilder mProcessBuilder; 571 private final CommandResult mCommandResult; 572 private final String mInput; 573 private Process mProcess = null; 574 private CountDownLatch mCountDown = null; 575 private Thread mExecutionThread; 576 private OutputStream stdOut = null; 577 private OutputStream stdErr = null; 578 private final boolean mCloseStreamAfterRun; 579 private Object mLock = new Object(); 580 private boolean mCancelled = false; 581 582 RunnableResult(final CommandResult result, final String input, 583 final ProcessBuilder processBuilder) { 584 this(result, input, processBuilder, null, null, false); 585 stdOut = new ByteArrayOutputStream(); 586 stdErr = new ByteArrayOutputStream(); 587 } 588 589 /** 590 * Alternative constructor that allows redirecting the output to any Outputstream. 591 * Stdout and stderr can be independently redirected to different Outputstream 592 * implementations. 593 * If streams are null, default behavior of using a buffer will be used. 594 */ 595 RunnableResult(final CommandResult result, final String input, 596 final ProcessBuilder processBuilder, OutputStream stdoutStream, 597 OutputStream stderrStream, boolean closeStreamAfterRun) { 598 mCloseStreamAfterRun = closeStreamAfterRun; 599 mProcessBuilder = processBuilder; 600 mInput = input; 601 mCommandResult = result; 602 // Ensure the outputs are never null 603 mCommandResult.setStdout(""); 604 mCommandResult.setStderr(""); 605 mCountDown = new CountDownLatch(1); 606 // Redirect IO, so that the outputstream for the spawn process does not fill up 607 // and cause deadlock. 608 if (stdoutStream != null) { 609 stdOut = stdoutStream; 610 } else { 611 stdOut = new ByteArrayOutputStream(); 612 } 613 if (stderrStream != null) { 614 stdErr = stderrStream; 615 } else { 616 stdErr = new ByteArrayOutputStream(); 617 } 618 } 619 620 /** Start a {@link Process} based on the {@link ProcessBuilder}. */ 621 @VisibleForTesting 622 Process startProcess() throws IOException { 623 return mProcessBuilder.start(); 624 } 625 626 @Override 627 public boolean run() throws Exception { 628 Thread stdoutThread = null; 629 Thread stderrThread = null; 630 synchronized (mLock) { 631 if (mCancelled == true) { 632 // if cancel() was called before run() took the lock, we do not even attempt 633 // to run. 634 return false; 635 } 636 mExecutionThread = Thread.currentThread(); 637 CLog.d("Running %s", mProcessBuilder.command()); 638 mProcess = startProcess(); 639 if (mInput != null) { 640 BufferedOutputStream processStdin = 641 new BufferedOutputStream(mProcess.getOutputStream()); 642 processStdin.write(mInput.getBytes("UTF-8")); 643 processStdin.flush(); 644 processStdin.close(); 645 } 646 // Log the command for thread tracking purpose. 647 stdoutThread = 648 inheritIO( 649 mProcess.getInputStream(), 650 stdOut, 651 String.format("inheritio-stdout-%s", mProcessBuilder.command())); 652 stderrThread = 653 inheritIO( 654 mProcess.getErrorStream(), 655 stdErr, 656 String.format("inheritio-stderr-%s", mProcessBuilder.command())); 657 } 658 // Wait for process to complete. 659 int rc = Integer.MIN_VALUE; 660 try { 661 try { 662 rc = mProcess.waitFor(); 663 // wait for stdout and stderr to be read 664 stdoutThread.join(IO_THREAD_JOIN_INTERVAL); 665 if (stdoutThread.isAlive()) { 666 CLog.d("stdout read thread %s still alive.", stdoutThread.toString()); 667 } 668 stderrThread.join(IO_THREAD_JOIN_INTERVAL); 669 if (stderrThread.isAlive()) { 670 CLog.d("stderr read thread %s still alive.", stderrThread.toString()); 671 } 672 // close the buffer that holds stdout/err content if default stream 673 // stream specified by caller should be handled by the caller. 674 if (mCloseStreamAfterRun) { 675 stdOut.close(); 676 stdErr.close(); 677 } 678 } finally { 679 // Write out the streams to the result. 680 if (stdOut instanceof ByteArrayOutputStream) { 681 mCommandResult.setStdout(((ByteArrayOutputStream)stdOut).toString("UTF-8")); 682 } else { 683 mCommandResult.setStdout("redirected to " + 684 stdOut.getClass().getSimpleName()); 685 } 686 if (stdErr instanceof ByteArrayOutputStream) { 687 mCommandResult.setStderr(((ByteArrayOutputStream)stdErr).toString("UTF-8")); 688 } else { 689 mCommandResult.setStderr("redirected to " + 690 stdErr.getClass().getSimpleName()); 691 } 692 } 693 } finally { 694 mCountDown.countDown(); 695 } 696 697 if (rc == 0) { 698 return true; 699 } else { 700 CLog.d("%s command failed. return code %d", mProcessBuilder.command(), rc); 701 } 702 return false; 703 } 704 705 @Override 706 public void cancel() { 707 mCancelled = true; 708 synchronized (mLock) { 709 if (mProcess == null) { 710 return; 711 } 712 CLog.i("Cancelling the process execution"); 713 mProcess.destroy(); 714 try { 715 // Only allow to continue if the Stdout has been read 716 // RunnableNotifier#Interrupt is the next call and will terminate the thread 717 if (!mCountDown.await(PROCESS_DESTROY_TIMEOUT_SEC, TimeUnit.SECONDS)) { 718 CLog.i("Process still not terminated, interrupting the execution thread"); 719 mExecutionThread.interrupt(); 720 mCountDown.await(); 721 } 722 } catch (InterruptedException e) { 723 CLog.i("interrupted while waiting for process output to be saved"); 724 } 725 } 726 } 727 728 @Override 729 public String toString() { 730 return "RunnableResult [command=" 731 + ((mProcessBuilder != null) ? mProcessBuilder.command() : null) 732 + "]"; 733 } 734 } 735 736 /** 737 * Helper method to redirect input stream. 738 * 739 * @param src {@link InputStream} to inherit/redirect from 740 * @param dest {@link BufferedOutputStream} to inherit/redirect to 741 * @param name the name of the thread returned. 742 * @return a {@link Thread} started that receives the IO. 743 */ 744 private static Thread inheritIO(final InputStream src, final OutputStream dest, String name) { 745 Thread t = new Thread(new Runnable() { 746 @Override 747 public void run() { 748 try { 749 StreamUtil.copyStreams(src, dest); 750 } catch (IOException e) { 751 CLog.e("Failed to read input stream."); 752 } 753 } 754 }); 755 t.setName(name); 756 t.start(); 757 return t; 758 } 759 760 /** Allow to stop the Timer Thread for the run util instance if started. */ 761 @VisibleForTesting 762 void terminateTimer() { 763 if (mWatchdogInterrupt.get() != null) { 764 mWatchdogInterrupt.get().purge(); 765 mWatchdogInterrupt.get().cancel(); 766 } 767 } 768 769 /** Timer that will execute a interrupt on the Thread registered. */ 770 private class InterruptTask extends TimerTask { 771 772 private Thread mToInterrupt = null; 773 774 public InterruptTask(Thread t) { 775 mToInterrupt = t; 776 } 777 778 @Override 779 public void run() { 780 if (mToInterrupt != null) { 781 CLog.e("Interrupting with TimerTask"); 782 mInterruptibleGlobal = true; 783 mToInterrupt.interrupt(); 784 } 785 } 786 } 787 788 /** 789 * {@inheritDoc} 790 */ 791 @Override 792 public void setEnvVariablePriority(EnvPriority priority) { 793 if (this.equals(sDefaultInstance)) { 794 throw new UnsupportedOperationException("Cannot setWorkingDir on default RunUtil"); 795 } 796 mEnvVariablePriority = priority; 797 } 798 } 799