【问题标题】:Capturing stdout when calling Runtime.exec调用 Runtime.exec 时捕获标准输出
【发布时间】:2010-10-27 08:02:25
【问题描述】:

当在客户端机器上遇到网络问题时,我希望能够运行一些命令行并将结果通过电子邮件发送给自己。

我发现 Runtime.exec 将允许我执行任意命令,但将结果收集到字符串中更有趣。

我意识到我可以将输出重定向到一个文件,然后从该文件中读取,但我的感觉告诉我有一种更优雅的方法。

建议?

【问题讨论】:

标签: java shell runtime


【解决方案1】:

您需要同时捕获过程中的 std out 和 std err。然后,您可以将标准输出写入文件/邮件或类似文件。

有关更多信息,请参阅this article,特别注意StreamGobbler 机制,该机制在单独的线程中捕获 stdout/err。这对于防止阻塞至关重要,如果您没有正确执行,则会导致许多错误!

【讨论】:

  • 我注意到,如果命令提供大量输出,则代码流将在 Gobbler 完成输出之前继续。在返回之前需要对它们调用 .join()。
  • 非常有效又简单的方法。需要注意的一点是 Main 方法中的 cmd 数组初始化对于 Windows 7 来说似乎有点过时。添加一个最终的“else”子句,将 cmd 数组初始化为 NT 样式,即使另一个 else if( osName.equals( "Windows NT" )返回错误。
  • SteamGobbler 的解决方案是否仅适用于 Windows 服务器?如果我使用的是 Unix- 它不会发生?
  • 在 Windows 中,它是 command.execmd.exe。如果您使用 Linux,则必须指定您选择的命令解释器,如 bash 等。
  • @Odelya - 我不认为这是特定于操作系统的
【解决方案2】:

使用ProcessBuilder。调用 start() 后,您将获得一个 Process 对象,您可以从中获取 stderr 和 stdout 流。

更新:ProcessBuilder 为您提供更多控制;您不必使用它,但从长远来看,我发现它更容易。尤其是将 stderr 重定向到 stdout 的能力,这意味着您只需吸入一个流。

【讨论】:

  • 我怎样才能从OutputStream获得我的输出?
【解决方案3】:

对于不会产生太多输出的进程,我认为这个利用Apache IOUtils的简单解决方案就足够了:

Process p = Runtime.getRuntime().exec("script");
p.waitFor();
String output = IOUtils.toString(p.getInputStream());
String errorOutput = IOUtils.toString(p.getErrorStream());

警告:但是,如果您的流程生成大量输出,则此方法可能会导致问题,如Process class JavaDoc 中所述:

创建的子进程没有自己的终端或控制台。它的所有标准io(即stdin、stdout、stderr)操作都会通过三个流(getOutputStream()、getInputStream()、getErrorStream())重定向到父进程。父进程使用这些流向子进程提供输入并从子进程获取输出。由于部分原生平台只为标准输入输出流提供有限的缓冲区大小,未能及时写入子进程的输入流或读取输出流可能导致子进程阻塞,甚至死锁。

【讨论】:

    【解决方案4】:

    使用Plexus Utils,Maven使用它来执行所有外部进程。

    Commandline commandLine = new Commandline();
    commandLine.setExecutable(executable.getAbsolutePath());
    
    Collection<String> args = getArguments();
    
    for (String arg : args) {
        Arg _arg = commandLine.createArg();
        _arg.setValue(arg);
    }
    
    WriterStreamConsumer systemOut = new WriterStreamConsumer(console);
    WriterStreamConsumer systemErr = new WriterStreamConsumer(console);
    
    returnCode = CommandLineUtils.executeCommandLine(commandLine, systemOut, systemErr, 10);
    if (returnCode != 0) {
        // bad
    } else {
        // good
    }
    

    【讨论】:

    • 当核心语言有完美的替代方案时,为什么还要使用外部库?
    • 有一些一开始可能看不到的差异。 1. 如果您调用 Process.waitFor() 它将阻塞,这意味着您必须读取进程输出,否则进程将等待输出缓冲区(控制台输出)可用。如果您选择此路径(自己获取输出),则不得使用 waitFor()。 2. 如果您轮询,那么您必须在等待读取输出时添加自己的代码来处理该问题。像 Plexus Utils - 246k- 这样的库的目的是帮助您避免重新发明轮子 :)
    • Ant 做同样的事情,你可以根据需要使用它,有一个核心任务可以被调用(使用适当的初始化 ant 上下文)来执行这个任务,但我更喜欢 Plexus Utils 因为更小(你甚至可以去掉除了 cli 包之外的所有东西,这意味着你将拥有少于 50k 的空间),专用且证明是稳定的(因为包含在 Maven 2 中)
    【解决方案5】:

    这是我多年来一直使用的助手类。一个小班。它有 JavaWorld streamgobbler 类来修复 JVM 资源泄漏。不知道对于 JVM6 和 JVM7 是否仍然有效,但不会受到伤害。 Helper 可以读取输出缓冲区供以后使用。

    import java.io.*;
    
    /**
     * Execute external process and optionally read output buffer.
     */
    public class ShellExec {
        private int exitCode;
        private boolean readOutput, readError;
        private StreamGobbler errorGobbler, outputGobbler;
    
        public ShellExec() { 
            this(false, false);
        }
    
        public ShellExec(boolean readOutput, boolean readError) {
            this.readOutput = readOutput;
            this.readError = readError;
        }
    
        /**
         * Execute a command.
         * @param command   command ("c:/some/folder/script.bat" or "some/folder/script.sh")
         * @param workdir   working directory or NULL to use command folder
         * @param wait  wait for process to end
         * @param args  0..n command line arguments
         * @return  process exit code
         */
        public int execute(String command, String workdir, boolean wait, String...args) throws IOException {
            String[] cmdArr;
            if (args != null && args.length > 0) {
                cmdArr = new String[1+args.length];
                cmdArr[0] = command;
                System.arraycopy(args, 0, cmdArr, 1, args.length);
            } else {
                cmdArr = new String[] { command };
            }
    
            ProcessBuilder pb =  new ProcessBuilder(cmdArr);
            File workingDir = (workdir==null ? new File(command).getParentFile() : new File(workdir) );
            pb.directory(workingDir);
    
            Process process = pb.start();
    
            // Consume streams, older jvm's had a memory leak if streams were not read,
            // some other jvm+OS combinations may block unless streams are consumed.
            errorGobbler  = new StreamGobbler(process.getErrorStream(), readError);
            outputGobbler = new StreamGobbler(process.getInputStream(), readOutput);
            errorGobbler.start();
            outputGobbler.start();
    
            exitCode = 0;
            if (wait) {
                try { 
                    process.waitFor();
                    exitCode = process.exitValue();                 
                } catch (InterruptedException ex) { }
            }
            return exitCode;
        }   
    
        public int getExitCode() {
            return exitCode;
        }
    
        public boolean isOutputCompleted() {
            return (outputGobbler != null ? outputGobbler.isCompleted() : false);
        }
    
        public boolean isErrorCompleted() {
            return (errorGobbler != null ? errorGobbler.isCompleted() : false);
        }
    
        public String getOutput() {
            return (outputGobbler != null ? outputGobbler.getOutput() : null);        
        }
    
        public String getError() {
            return (errorGobbler != null ? errorGobbler.getOutput() : null);        
        }
    
    //********************************************
    //********************************************    
    
        /**
         * StreamGobbler reads inputstream to "gobble" it.
         * This is used by Executor class when running 
         * a commandline applications. Gobblers must read/purge
         * INSTR and ERRSTR process streams.
         * http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html?page=4
         */
        private class StreamGobbler extends Thread {
            private InputStream is;
            private StringBuilder output;
            private volatile boolean completed; // mark volatile to guarantee a thread safety
    
            public StreamGobbler(InputStream is, boolean readStream) {
                this.is = is;
                this.output = (readStream ? new StringBuilder(256) : null);
            }
    
            public void run() {
                completed = false;
                try {
                    String NL = System.getProperty("line.separator", "\r\n");
    
                    InputStreamReader isr = new InputStreamReader(is);
                    BufferedReader br = new BufferedReader(isr);
                    String line;
                    while ( (line = br.readLine()) != null) {
                        if (output != null)
                            output.append(line + NL); 
                    }
                } catch (IOException ex) {
                    // ex.printStackTrace();
                }
                completed = true;
            }
    
            /**
             * Get inputstream buffer or null if stream
             * was not consumed.
             * @return
             */
            public String getOutput() {
                return (output != null ? output.toString() : null);
            }
    
            /**
             * Is input stream completed.
             * @return
             */
            public boolean isCompleted() {
                return completed;
            }
    
        }
    
    }
    

    这是一个从 .vbs 脚本读取输出的示例,但对于 linux sh 脚本也有类似的工作。

       ShellExec exec = new ShellExec(true, false);
       exec.execute("cscript.exe", null, true,
          "//Nologo",
          "//B",            // batch mode, no prompts
          "//T:320",        // timeout seconds
          "c:/my/script/test1.vbs",  // unix path delim works for script.exe
          "script arg 1",
          "script arg 2",
       );
       System.out.println(exec.getOutput());
    

    【讨论】:

      【解决方案6】:

      来自jcabi-logVerboseProcess 实用程序类可以帮助您:

      String output = new VerboseProcess(
        new ProcessBuilder("executable with output")
      ).stdout();
      

      你需要的唯一依赖:

      <dependency>
        <groupId>com.jcabi</groupId>
        <artifactId>jcabi-log</artifactId>
        <version>0.7.5</version>
      </dependency>
      

      【讨论】:

        【解决方案7】:

        Runtime.exec() 返回一个 Process 对象,您可以从中提取您运行的任何命令的输出。

        【讨论】:

          【解决方案8】:

          使用 Runtime.exec 为您提供了一个流程。您可以使用getInputStream 获取此过程的标准输出,并将此输入流放入一个字符串,例如通过一个StringBuffer。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2021-05-21
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-07-09
            • 1970-01-01
            • 1970-01-01
            • 2013-05-10
            相关资源
            最近更新 更多