| // Copyright 2015 Google Inc. All rights reserved. |
| // |
| // 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.google.devtools.build.lib.shell; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.devtools.build.lib.shell.TestUtil.assertArrayEquals; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import com.google.devtools.build.lib.testutil.BlazeTestUtils; |
| import com.google.devtools.build.lib.testutil.TestConstants; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.Collections; |
| import java.util.Map; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| /** |
| * Unit tests for {@link Command}. This test will only succeed on Linux, |
| * currently, because of its non-portable nature. |
| */ |
| @RunWith(JUnit4.class) |
| public class CommandTest { |
| |
| private static final long LONG_TIME = 10000; |
| private static final long SHORT_TIME = 250; |
| |
| // Platform-independent tests ---------------------------------------------- |
| |
| @Before |
| public void setUp() throws Exception { |
| |
| // enable all log statements to ensure there are no problems with |
| // logging code |
| Logger.getLogger("com.google.devtools.build.lib.shell.Command").setLevel(Level.FINEST); |
| } |
| |
| @Test |
| public void testIllegalArgs() throws Exception { |
| |
| try { |
| new Command((String[]) null); |
| fail("Should have thrown IllegalArgumentException"); |
| } catch (IllegalArgumentException iae) { |
| // good |
| } |
| |
| try { |
| new Command(new String[] {"/bin/true", null}).execute(); |
| fail("Should have thrown NullPointerException"); |
| } catch (NullPointerException npe) { |
| // good |
| } |
| |
| try { |
| new Command(new String[] {"foo"}).execute(null); |
| fail("Should have thrown NullPointerException"); |
| } catch (NullPointerException npe) { |
| // good |
| } |
| |
| } |
| |
| @Test |
| public void testProcessBuilderConstructor() throws Exception { |
| String helloWorld = "Hello, world"; |
| ProcessBuilder builder = new ProcessBuilder("/bin/echo", helloWorld); |
| byte[] stdout = new Command(builder).execute().getStdout(); |
| assertEquals(helloWorld + '\n', new String(stdout, "UTF-8")); |
| } |
| |
| @Test |
| public void testGetters() { |
| final File workingDir = new File("."); |
| final Map<String,String> env = Collections.singletonMap("foo", "bar"); |
| final String[] commandArgs = new String[] { "command" }; |
| final Command command = new Command(commandArgs, env, workingDir); |
| assertArrayEquals(commandArgs, command.getCommandLineElements()); |
| for (final String key : env.keySet()) { |
| assertThat(command.getEnvironmentVariables()).containsEntry(key, env.get(key)); |
| } |
| assertEquals(workingDir, command.getWorkingDirectory()); |
| } |
| |
| // Platform-dependent tests ------------------------------------------------ |
| |
| @Test |
| public void testSimpleCommand() throws Exception { |
| final Command command = new Command(new String[] {"ls"}); |
| final CommandResult result = command.execute(); |
| assertTrue(result.getTerminationStatus().success()); |
| assertEquals(0, result.getStderr().length); |
| assertThat(result.getStdout().length).isGreaterThan(0); |
| } |
| |
| @Test |
| public void testArguments() throws Exception { |
| final Command command = new Command(new String[] {"echo", "foo"}); |
| checkSuccess(command.execute(), "foo\n"); |
| } |
| |
| @Test |
| public void testEnvironment() throws Exception { |
| final Map<String,String> env = Collections.singletonMap("FOO", "BAR"); |
| final Command command = new Command(new String[] {"/bin/sh", "-c", "echo $FOO"}, env, |
| null); |
| checkSuccess(command.execute(), "BAR\n"); |
| } |
| |
| @Test |
| public void testWorkingDir() throws Exception { |
| final Command command = new Command(new String[] {"pwd"}, null, new File("/")); |
| checkSuccess(command.execute(), "/\n"); |
| } |
| |
| @Test |
| public void testStdin() throws Exception { |
| final Command command = new Command(new String[] {"grep", "bar"}); |
| checkSuccess(command.execute("foobarbaz".getBytes()), "foobarbaz\n"); |
| } |
| |
| @Test |
| public void testRawCommand() throws Exception { |
| final Command command = new Command(new String[] { "perl", |
| "-e", |
| "print 'a'x100000" }); |
| final CommandResult result = command.execute(); |
| assertTrue(result.getTerminationStatus().success()); |
| assertEquals(0, result.getStderr().length); |
| assertThat(result.getStdout().length).isGreaterThan(0); |
| } |
| |
| @Test |
| public void testRawCommandWithDir() throws Exception { |
| final Command command = new Command(new String[] { "pwd" }, |
| null, |
| new File("/")); |
| final CommandResult result = command.execute(); |
| checkSuccess(result, "/\n"); |
| } |
| |
| @Test |
| public void testHugeOutput() throws Exception { |
| final Command command = new Command(new String[] {"perl", "-e", "print 'a'x100000"}); |
| final CommandResult result = command.execute(); |
| assertTrue(result.getTerminationStatus().success()); |
| assertEquals(0, result.getStderr().length); |
| assertEquals(100000, result.getStdout().length); |
| } |
| |
| @Test |
| public void testIgnoreOutput() throws Exception { |
| final Command command = new Command(new String[] {"perl", "-e", "print 'a'x100000"}); |
| final CommandResult result = command.execute(Command.NO_INPUT, null, true); |
| assertTrue(result.getTerminationStatus().success()); |
| try { |
| result.getStdout(); |
| fail("Should have thrown IllegalStateException"); |
| } catch (IllegalStateException ise) { |
| // good |
| } |
| try { |
| result.getStderr(); |
| fail("Should have thrown IllegalStateException"); |
| } catch (IllegalStateException ise) { |
| // good |
| } |
| } |
| |
| @Test |
| public void testNoStreamingInputForCat() throws Exception { |
| final Command command = new Command(new String[]{"/bin/cat"}); |
| ByteArrayInputStream emptyInput = new ByteArrayInputStream(new byte[0]); |
| ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| ByteArrayOutputStream err = new ByteArrayOutputStream(); |
| CommandResult result = command.execute(emptyInput, |
| Command.NO_OBSERVER, out, err); |
| assertTrue(result.getTerminationStatus().success()); |
| assertThat(out.toString("UTF-8")).isEmpty(); |
| assertThat(err.toString("UTF-8")).isEmpty(); |
| } |
| |
| @Test |
| public void testNoInputForCat() throws Exception { |
| final Command command = new Command(new String[]{"/bin/cat"}); |
| CommandResult result = command.execute(); |
| assertTrue(result.getTerminationStatus().success()); |
| assertThat(new String(result.getStdout(), "UTF-8")).isEmpty(); |
| assertThat(new String(result.getStderr(), "UTF-8")).isEmpty(); |
| } |
| |
| @Test |
| public void testProvidedOutputStreamCapturesHelloWorld() throws Exception { |
| String helloWorld = "Hello, world."; |
| final Command command = new Command(new String[]{"/bin/echo", helloWorld}); |
| ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); |
| ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); |
| command.execute(Command.NO_INPUT, Command.NO_OBSERVER, stdOut, stdErr); |
| assertEquals(helloWorld + "\n", stdOut.toString("UTF-8")); |
| assertEquals(0, stdErr.toByteArray().length); |
| } |
| |
| @Test |
| public void testAsynchronous() throws Exception { |
| final File tempFile = File.createTempFile("googlecron-test", "tmp"); |
| tempFile.delete(); |
| final Command command = new Command(new String[] {"touch", tempFile.getAbsolutePath()}); |
| // Shouldn't throw any exceptions: |
| FutureCommandResult result = |
| command.executeAsynchronously(Command.NO_INPUT); |
| result.get(); |
| assertTrue(tempFile.exists()); |
| assertTrue(result.isDone()); |
| tempFile.delete(); |
| } |
| |
| @Test |
| public void testAsynchronousWithKillable() throws Exception { |
| final Command command = new Command(new String[] {"sleep", "5"}); |
| final SimpleKillableObserver observer = new SimpleKillableObserver(); |
| FutureCommandResult result = |
| command.executeAsynchronously(Command.NO_INPUT, observer); |
| assertFalse(result.isDone()); |
| observer.kill(); |
| try { |
| result.get(); |
| } catch (AbnormalTerminationException e) { |
| // Expects, but does not insist on termination with a signal. |
| } |
| assertTrue(result.isDone()); |
| } |
| |
| @Test |
| public void testAsynchronousWithOutputStreams() throws Exception { |
| |
| final String helloWorld = "Hello, world."; |
| final Command command = new Command(new String[]{"/bin/echo", helloWorld}); |
| final ByteArrayInputStream emptyInput = |
| new ByteArrayInputStream(new byte[0]); |
| final ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); |
| final ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); |
| FutureCommandResult result = command.executeAsynchronously(emptyInput, |
| Command.NO_OBSERVER, |
| stdOut, |
| stdErr); |
| result.get(); // Make sure the process actually finished |
| assertEquals(helloWorld + "\n", stdOut.toString("UTF-8")); |
| assertEquals(0, stdErr.toByteArray().length); |
| } |
| |
| @Test |
| public void testSimpleKillableObserver() throws Exception { |
| final Command command = new Command(new String[] {"sleep", "5"}); |
| final SimpleKillableObserver observer = new SimpleKillableObserver(); |
| new Thread() { |
| @Override |
| public void run() { |
| try { |
| command.execute(Command.NO_INPUT, observer, true); |
| fail(); |
| } catch (CommandException e) { |
| // Good. |
| checkCommandElements(e, "sleep", "5"); |
| } |
| } |
| }.start(); |
| // We're racing against the actual startup of the other command. Wait for 10ms so it can start. |
| Thread.sleep(10); |
| observer.kill(); |
| } |
| |
| @Test |
| public void testTimeout() throws Exception { |
| // Sleep for 3 seconds, |
| final Command command = new Command(new String[] {"sleep", "3"}); |
| try { |
| // but timeout after 1 second |
| command.execute(Command.NO_INPUT, 1000L, false); |
| fail("Should have thrown AbnormalTerminationException"); |
| } catch (AbnormalTerminationException ate) { |
| // good |
| checkCommandElements(ate, "sleep", "3"); |
| checkATE(ate); |
| } |
| } |
| |
| @Test |
| public void testTimeoutDoesntFire() throws Exception { |
| final Command command = new Command(new String[] {"cat"}); |
| command.execute(new byte[]{'H', 'i', '!'}, 2000L, false); |
| } |
| |
| @Test |
| public void testCommandDoesNotExist() throws Exception { |
| final Command command = new Command(new String[]{"thisisnotreal"}); |
| try { |
| command.execute(); |
| fail(); |
| } catch (ExecFailedException e){ |
| // Good. |
| checkCommandElements(e, "thisisnotreal"); |
| } |
| } |
| |
| @Test |
| public void testNoSuchCommand() throws Exception { |
| final Command command = new Command(new String[] {"thisisnotreal"}); |
| try { |
| command.execute(); |
| fail("Should have thrown ExecFailedException"); |
| } catch (ExecFailedException expected) { |
| // good |
| } |
| } |
| |
| @Test |
| public void testExitCodes() throws Exception { |
| // 0 => success |
| { |
| String args[] = { "/bin/sh", "-c", "exit 0" }; |
| CommandResult result = new Command(args).execute(); |
| TerminationStatus status = result.getTerminationStatus(); |
| assertTrue(status.success()); |
| assertTrue(status.exited()); |
| assertEquals(0, status.getExitCode()); |
| } |
| |
| // Every exit value in range [1-255] is reported as such (except [129-191], |
| // which map to signals). |
| for (int exit : new int[] { 1, 2, 3, 127, 128, 192, 255 }) { |
| try { |
| String args[] = { "/bin/sh", "-c", "exit " + exit }; |
| new Command(args).execute(); |
| fail("Should have exited with status " + exit); |
| } catch (BadExitStatusException e) { |
| assertThat(e).hasMessage("Process exited with status " + exit); |
| checkCommandElements(e, "/bin/sh", "-c", "exit " + exit); |
| TerminationStatus status = e.getResult().getTerminationStatus(); |
| assertFalse(status.success()); |
| assertTrue(status.exited()); |
| assertEquals(exit, status.getExitCode()); |
| assertEquals("Exit " + exit , status.toShortString()); |
| } |
| } |
| |
| // negative exit values are modulo 256: |
| for (int exit : new int[] { -1, -2, -3 }) { |
| int expected = 256 + exit; |
| try { |
| String args[] = { "/bin/bash", "-c", "exit " + exit }; |
| new Command(args).execute(); |
| fail("Should have exited with status " + expected); |
| } catch (BadExitStatusException e) { |
| assertThat(e).hasMessage("Process exited with status " + expected); |
| checkCommandElements(e, "/bin/bash", "-c", "exit " + exit); |
| TerminationStatus status = e.getResult().getTerminationStatus(); |
| assertFalse(status.success()); |
| assertTrue(status.exited()); |
| assertEquals(expected, status.getExitCode()); |
| assertEquals("Exit " + expected, status.toShortString()); |
| } |
| } |
| } |
| |
| @Test |
| public void testFailedWithSignal() throws Exception { |
| // SIGHUP, SIGINT, SIGKILL, SIGTERM |
| for (int signal : new int[] { 1, 2, 9, 15 }) { |
| // Invoke a C++ program (killmyself.cc) that will die |
| // with the specified signal. |
| String killmyself = BlazeTestUtils.runfilesDir() + "/" |
| + TestConstants.JAVATESTS_ROOT |
| + "/com/google/devtools/build/lib/shell/killmyself"; |
| try { |
| String args[] = { killmyself, "" + signal }; |
| new Command(args).execute(); |
| fail("Expected signal " + signal); |
| } catch (AbnormalTerminationException e) { |
| assertThat(e).hasMessage("Process terminated by signal " + signal); |
| checkCommandElements(e, killmyself, "" + signal); |
| TerminationStatus status = e.getResult().getTerminationStatus(); |
| assertFalse(status.success()); |
| assertFalse(status.exited()); |
| assertEquals(signal, status.getTerminatingSignal()); |
| |
| switch (signal) { |
| case 1: assertEquals("Hangup", status.toShortString()); break; |
| case 2: assertEquals("Interrupt", status.toShortString()); break; |
| case 9: assertEquals("Killed", status.toShortString()); break; |
| case 15: assertEquals("Terminated", status.toShortString()); break; |
| } |
| } |
| } |
| } |
| |
| @Test |
| public void testDestroy() throws Exception { |
| |
| // Sleep for 10 seconds, |
| final Command command = new Command(new String[] {"sleep", "10"}); |
| |
| // but kill it after 1 |
| final KillableObserver killer = new KillableObserver() { |
| @Override |
| public void startObserving(final Killable killable) { |
| final Thread t = new Thread() { |
| @Override |
| public void run() { |
| try { |
| Thread.sleep(1000L); |
| } catch (InterruptedException ie) { |
| // continue |
| } |
| killable.kill(); |
| } |
| }; |
| t.start(); |
| } |
| @Override |
| public void stopObserving(final Killable killable) { |
| // do nothing |
| } |
| }; |
| |
| try { |
| command.execute(Command.NO_INPUT, killer, false); |
| fail("Should have thrown AbnormalTerminationException"); |
| } catch (AbnormalTerminationException ate) { |
| // Good. |
| checkCommandElements(ate, "sleep", "10"); |
| checkATE(ate); |
| } |
| } |
| |
| @Test |
| public void testOnlyReadsPartialInput() throws Exception { |
| // -c == --bytes, but -c also works on Darwin. |
| Command command = new Command(new String[] {"head", "-c", "500"}); |
| OutputStream out = new ByteArrayOutputStream(); |
| InputStream in = new InputStream() { |
| |
| @Override |
| public int read() { |
| return 0; // write an unbounded amount |
| } |
| |
| }; |
| |
| CommandResult result = command.execute(in, Command.NO_OBSERVER, out, out); |
| TerminationStatus status = result.getTerminationStatus(); |
| assertTrue(status.success()); |
| } |
| |
| @Test |
| public void testFlushing() throws Exception { |
| final Command command = new Command( |
| // On darwin, /bin/sh does not support -n for the echo builtin. |
| new String[] {"/bin/bash", "-c", "echo -n Foo; sleep 0.1; echo Bar"}); |
| // We run this command, passing in a special output stream |
| // that records when each flush() occurs. |
| // We test that a flush occurs after writing "Foo" |
| // and that another flush occurs after writing "Bar\n". |
| final boolean[] flushed = new boolean[8]; |
| OutputStream out = new OutputStream() { |
| private int count = 0; |
| @Override |
| public void write(int b) throws IOException { |
| count++; |
| } |
| @Override |
| public void flush() throws IOException { |
| flushed[count] = true; |
| } |
| }; |
| command.execute(Command.NO_INPUT, Command.NO_OBSERVER, out, System.err); |
| assertFalse(flushed[0]); |
| assertFalse(flushed[1]); // 'F' |
| assertFalse(flushed[2]); // 'o' |
| assertTrue(flushed[3]); // 'o' <- expect flush here. |
| assertFalse(flushed[4]); // 'B' |
| assertFalse(flushed[5]); // 'a' |
| assertFalse(flushed[6]); // 'r' |
| assertTrue(flushed[7]); // '\n' |
| } |
| |
| // See also InterruptibleTest. |
| @Test |
| public void testInterrupt() throws Exception { |
| |
| // Sleep for 10 seconds, |
| final Command command = new Command(new String[] {"sleep", "10"}); |
| // Easy but hacky way to let this thread "return" a result to this method |
| final CommandResult[] resultContainer = new CommandResult[1]; |
| final Thread commandThread = new Thread() { |
| @Override |
| public void run() { |
| try { |
| resultContainer[0] = command.execute(); |
| } catch (CommandException ce) { |
| fail(ce.toString()); |
| } |
| } |
| }; |
| commandThread.start(); |
| |
| Thread.sleep(1000L); |
| |
| // but interrupt it after 1 |
| commandThread.interrupt(); |
| |
| // should continue to wait and exit normally |
| commandThread.join(); |
| |
| final CommandResult result = resultContainer[0]; |
| assertTrue(result.getTerminationStatus().success()); |
| assertEquals(0, result.getStderr().length); |
| assertEquals(0, result.getStdout().length); |
| } |
| |
| @Test |
| public void testOutputStreamThrowsException() throws Exception { |
| OutputStream out = new OutputStream () { |
| @Override |
| public void write(int b) throws IOException { |
| throw new IOException(); |
| } |
| }; |
| Command command = new Command(new String[] {"/bin/echo", "foo"}); |
| try { |
| command.execute(Command.NO_INPUT, Command.NO_OBSERVER, out, out); |
| fail(); |
| } catch (AbnormalTerminationException e) { |
| // Good. |
| checkCommandElements(e, "/bin/echo", "foo"); |
| assertThat(e).hasMessage("java.io.IOException"); |
| } |
| } |
| |
| @Test |
| public void testOutputStreamThrowsExceptionAndCommandFails() |
| throws Exception { |
| OutputStream out = new OutputStream () { |
| @Override |
| public void write(int b) throws IOException { |
| throw new IOException(); |
| } |
| }; |
| Command command = new Command(new String[] {"cat", "/dev/thisisnotreal"}); |
| try { |
| command.execute(Command.NO_INPUT, Command.NO_OBSERVER, out, out); |
| fail(); |
| } catch (AbnormalTerminationException e) { |
| checkCommandElements(e, "cat", "/dev/thisisnotreal"); |
| TerminationStatus status = e.getResult().getTerminationStatus(); |
| // Subprocess either gets a SIGPIPE trying to write to our output stream, |
| // or it exits with failure. Both are observed, nondetermistically. |
| assertTrue(status.exited() |
| ? status.getExitCode() == 1 |
| : status.getTerminatingSignal() == 13); |
| assertTrue(e.getMessage(), |
| e.getMessage().endsWith("also encountered an error while attempting " |
| + "to retrieve output")); |
| } |
| } |
| |
| /** |
| * Helper to test KillableObserver classes. |
| */ |
| private class KillableTester implements Killable { |
| private boolean isKilled = false; |
| private boolean timedOut = false; |
| @Override |
| public synchronized void kill() { |
| isKilled = true; |
| notifyAll(); |
| } |
| public synchronized boolean getIsKilled() { |
| return isKilled; |
| } |
| /** |
| * Wait for a specified time or until the {@link #kill()} is called. |
| */ |
| public synchronized void sleepUntilKilled(final long timeoutMS) { |
| long nowTime = System.currentTimeMillis(); |
| long endTime = nowTime + timeoutMS; |
| while (!isKilled && !timedOut) { |
| long waitTime = endTime - nowTime; |
| if (waitTime <= 0) { |
| // Process has timed out, needs killing. |
| timedOut = true; |
| break; |
| } |
| try { |
| wait(waitTime); // Suffers "spurious wakeup", hence the while() loop. |
| nowTime = System.currentTimeMillis(); |
| } catch (InterruptedException exception) { |
| break; |
| } |
| } |
| } |
| } |
| |
| @Test |
| public void testTimeOutKillableObserverNoKill() throws Exception { |
| KillableTester killable = new KillableTester(); |
| TimeoutKillableObserver observer = new TimeoutKillableObserver(LONG_TIME); |
| observer.startObserving(killable); |
| observer.stopObserving(killable); |
| assertFalse(observer.hasTimedOut()); |
| assertFalse(killable.getIsKilled()); |
| } |
| |
| @Test |
| public void testTimeOutKillableObserverNoKillWithDelay() throws Exception { |
| KillableTester killable = new KillableTester(); |
| TimeoutKillableObserver observer = new TimeoutKillableObserver(LONG_TIME); |
| observer.startObserving(killable); |
| killable.sleepUntilKilled(SHORT_TIME); |
| observer.stopObserving(killable); |
| assertFalse(observer.hasTimedOut()); |
| assertFalse(killable.getIsKilled()); |
| } |
| |
| @Test |
| public void testTimeOutKillableObserverWithKill() throws Exception { |
| KillableTester killable = new KillableTester(); |
| TimeoutKillableObserver observer = new TimeoutKillableObserver(SHORT_TIME); |
| observer.startObserving(killable); |
| killable.sleepUntilKilled(LONG_TIME); |
| observer.stopObserving(killable); |
| assertTrue(observer.hasTimedOut()); |
| assertTrue(killable.getIsKilled()); |
| } |
| |
| @Test |
| public void testTimeOutKillableObserverWithKillZeroMillis() throws Exception { |
| KillableTester killable = new KillableTester(); |
| TimeoutKillableObserver observer = new TimeoutKillableObserver(0); |
| observer.startObserving(killable); |
| killable.sleepUntilKilled(LONG_TIME); |
| observer.stopObserving(killable); |
| assertTrue(observer.hasTimedOut()); |
| assertTrue(killable.getIsKilled()); |
| } |
| |
| private static void checkCommandElements(CommandException e, |
| String... expected) { |
| assertArrayEquals(expected, e.getCommand().getCommandLineElements()); |
| } |
| |
| private static void checkATE(final AbnormalTerminationException ate) { |
| final CommandResult result = ate.getResult(); |
| assertFalse(result.getTerminationStatus().success()); |
| } |
| |
| private static void checkSuccess(final CommandResult result, |
| final String expectedOutput) { |
| assertTrue(result.getTerminationStatus().success()); |
| assertEquals(0, result.getStderr().length); |
| assertEquals(expectedOutput, new String(result.getStdout())); |
| } |
| } |