首页 > 解决方案 > 控制台重定向在应用程序关闭之前不起作用(Java)

问题描述

首先,我这样做只是为了好玩,无论如何我都不是专业人士。因此,如果我的代码有点草率,我不会感到惊讶!

我正在尝试在 Java 11 中为控制台应用程序编写 GUI 包装器。我的计划是使用 BufferedReader 从进程中捕获 stdOut 和 stdErr 并将其显示在 JTextArea 中。在使用命令行参数填充 ArrayList 后,我​​正在从我的主 GUI 线程运行此线程。它在 Ubuntu 或 Fedora 上完美运行,但我无法在 Windows 上正确运行。当我尝试运行控制台应用程序的交叉编译 Windows 版本时,我的应用程序仅在控制台应用程序关闭后才显示其输出。我还尝试用 C 替换一个非常简单的 Hello World 应用程序(通常显示 Hello,等待 5 秒,然后显示 World),这也是同样的事情。但是,如果我将 ArrayList 更改为运行 ping.exe -t 8.8.8.8,则可以正常工作。

我怀疑正在发生的是while循环阻塞了线程,但我不明白它在Linux上是如何工作的,以及我是否在Windows上使用ping.exe。我还尝试了Java 中的 Redirect stdin 和 stdout以及 ProcessBuilder中提到的 inheritIO 中的代码:Forwarding stdout and stderr of started processes without blocking the main thread,但我也遇到了同样的问题。有任何想法吗?

    public class RunThread extends Thread {
    @Override
    public void run(){
        // Create process with the ArrayList we populated above
        ProcessBuilder pb = new ProcessBuilder(allArgs);
        pb.redirectErrorStream(true);

        // Clear the console
        txtConsoleOutput.setText("");

        // Try to start the process
        try {
            Process p = pb.start();                

            // Get the PID of the process we just started
            pid = p.pid(); 

            // Capture the output
            String cmdOutput;
            BufferedReader inputStream = new BufferedReader(new InputStreamReader(p.getInputStream()));

            // Get stdOut/stdErr of the process and display in the console
            while ((cmdOutput = inputStream.readLine()) != null) {
                txtConsoleOutput.append(cmdOutput + "\n");
            }
            inputStream.close();
        }
        catch (IOException ex) {
            JOptionPane.showMessageDialog(null,
                    "An error (" + ex + ") occurred while attempting to run.", AppName, JOptionPane.ERROR_MESSAGE);
        }

        // Clear the ArrayList so we can run again with a fresh set
        allArgs.clear();

    }
}

更新基于@ControlAltDel 提供的代码和@Holger 的建议,我将其重写为线程安全的(希望如此!),但最终结果是相同的。

        SwingWorker <Void, String> RunTV = new SwingWorker <Void, String> () {
        @Override
        protected Void doInBackground() {
            // Create process with the ArrayList we populated above
            ProcessBuilder pb = new ProcessBuilder(allArgs);
            pb.directory(new File(hacktvDirectory));
            pb.redirectErrorStream(true); 
            // Try to start the process
            try {
                Process p = pb.start();                
                // Get the PID of the process we just started
                pid = p.pid();
                // Capture the output
                DataFetcher df = new DataFetcher(p.getInputStream(), new byte[1024], 0);
                FetcherListener fl = new FetcherListener() {
                    @Override
                    public void fetchedAll(byte[] bytes) {}
                    @Override
                    public void fetchedMore(byte[] bytes, int start, int end) {
                        publish(new String (bytes, start, end-start));
                    }
                };
                df.addFetcherListener(fl);
                new Thread(df).start();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            return null;
        } // End doInBackground

        // Update the GUI from this method.
        @Override
        protected void done() {
            // Revert button to say Run instead of Stop
            changeStopToRun();
            // Clear the ArrayList so we can run again with a fresh set
            allArgs.clear();                
        }
        // Update the GUI from this method.
        @Override
        protected void process(List<String> chunks) {
            // Here we receive the values from publish() and display
            // them in the console
            for (String o : chunks) {
                txtConsoleOutput.append(o);
                txtConsoleOutput.repaint();
            }
        }
    };
    RunTV.execute();
}

2020 年 10 月 11 日更新在 kriegaex 的帖子之后,我又看了一遍。不幸的是,示例代码做了同样的事情,但他们的评论“例如,如果您的示例程序使用 System.out.print() 而不是 println(),您将永远不会在控制台上看到任何内容,因为输出将被缓冲。” 和我一起敲钟。

我可以访问我正在包装的程序的源代码,它是用 C 语言编写的。它具有以下代码来将视频分辨率打印到控制台:

void vid_info(vid_t *s)
{
        fprintf(stderr, "Video: %dx%d %.2f fps (full frame %dx%d)\n",
                s->active_width, s->conf.active_lines,
                (double) s->conf.frame_rate_num / s->conf.frame_rate_den,
                s->width, s->conf.lines
        );

        fprintf(stderr, "Sample rate: %d\n", s->sample_rate);
}

如果我添加fflush(stderr); 在第二个 fprintf 语句下方,我在控制台上看到了这些行,而没有在我自己的代码中修改任何东西。我仍然不明白为什么它在没有这个的情况下在 Linux 中工作,但至少我知道答案。

标签: javastdoutjava-11

解决方案


我自己的评论的相关信息:

另一个想法:您是否尝试过在不使用 Swing 的情况下复制它并将从流中读取的内容转储到程序的文本控制台上?也许它适用ping于其他测试程序但不适用于其他测试程序的问题是后者只是写入一个缓冲流,该流只会偶尔刷新一次(例如退出时),因此您自己的程序没有什么可读取的。我想将“Hello”+“world”写入缓冲区明显大于那些短字符串的流可能会导致这种行为。ping但是可能会直接写入和刷新。

例如,如果您的示例程序使用System.out.print()而不是println(),您将永远不会在控制台上看到任何内容,因为输出将被缓冲。只有在您插入println()(暗示调用BufferedWriter.flushBuffer())或直接刷新写入器的缓冲区之后,从第一个进程的控制台读取的其他程序才能读取某些内容。

目标应用程序,写入控制台:

import java.util.Random;

public class TargetApp {
  public static void main(String[] args) throws InterruptedException {
    System.out.print("Hello ");
    Thread.sleep(1500);
    System.out.println("world!");
    Random random = new Random();
    for (int i = 0; i < 250; i++) {
      System.out.print("#");
      if (random.nextInt(20) == 0)
        System.out.println();
      Thread.sleep(50);
    }
  }
}

控制器应用程序,读取目标应用程序的控制台输出:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;

public class ControllerApp extends Thread {
  List<String> allArgs = Arrays.asList(
    //"ping", "-n", "3", "google.de"
    "java", "-cp", "bin", "TargetApp"
  );

  @Override
  public void run() {
    try (
      BufferedReader inputStream = new BufferedReader(
        new InputStreamReader(
          new ProcessBuilder(allArgs)
            .redirectErrorStream(true)
            .start()
            .getInputStream()
        )
      )
    ) {
      String cmdOutput;
      while ((cmdOutput = inputStream.readLine()) != null) {
        System.out.println(cmdOutput);
      }
    }
    catch (IOException ex) {
      throw new RuntimeException(ex);
    }
  }

  public static void main(String[] args) throws InterruptedException {
    new ControllerApp().start();
  }
}

如果你运行ControllerApp,你会看到它没有读取“Hello”和“world!”。分开但同时。“#”字符串也是如此。它根据其他程序的缓冲区刷新行为以块的形式读取它们。您还会注意到它以走走停停的方式编写以下协议,而不是每 50 毫秒连续的“#”字符流。

Hello world!
#####
#################
############
####
#############
##########################################
###############
################
#################
######
##########
######
#######
############################################
#########
#################
##########

因此,如果这是您的问题,那么它是 inTargetApp而不是 in ControllerApp。那么它也将与 Swing 无关。


更新:TargetApp我忘了提到,您可以通过注释掉这两行来模拟仅在退出后看到最后一个输出的行为:

//      if (random.nextInt(20) == 0)
//        System.out.println();

TargetApp然后控制台日志看起来像这样,只有在终止时才打印长的“#”行:

Hello world!
##########################################################################################################################################################################################################################################################

推荐阅读