草庐IT

具有并发输入/输出流的 Java 进程

coder 2024-03-17 原文

我正在尝试创建一种控制台/终端,允许用户输入一个字符串,然后将其制作成一个进程并打印出结果。就像普通控制台一样。但是我在管理输入/输出流时遇到了麻烦。我查看了 this thread ,但遗憾的是该解决方案不适用于我的问题。

除了像“ipconfig”和“cmd.exe”这样的标准命令,如果脚本要求输入,我需要能够运行脚本并使用相同的输入流来传递一些参数。

例如,在运行脚本“python pyScript.py”后,我应该能够将进一步的输入传递给脚本(例如:raw_input),同时还打印脚本的输出。您期望从终端获得的基本行为。

到目前为止我所得到的:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;

public class Console extends JFrame{

    JTextPane inPane, outPane;
    InputStream inStream, inErrStream;
    OutputStream outStream;

    public Console(){
        super("Console");
        setPreferredSize(new Dimension(500, 600));
        setLocationByPlatform(true);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        // GUI
        outPane = new JTextPane();
        outPane.setEditable(false);
        outPane.setBackground(new Color(20, 20, 20));
        outPane.setForeground(Color.white);
        inPane = new JTextPane();
        inPane.setBackground(new Color(40, 40, 40));
        inPane.setForeground(Color.white);
        inPane.setCaretColor(Color.white);

        JPanel panel = new JPanel(new BorderLayout());
        panel.add(outPane, BorderLayout.CENTER);
        panel.add(inPane, BorderLayout.SOUTH);

        JScrollPane scrollPanel = new JScrollPane(panel);

        getContentPane().add(scrollPanel);

        // LISTENER
        inPane.addKeyListener(new KeyListener(){
            @Override
            public void keyPressed(KeyEvent e){
              if(e.getKeyCode() == KeyEvent.VK_ENTER){
                    e.consume();
                    read(inPane.getText());
                }
            }
            @Override
            public void keyTyped(KeyEvent e) {}

            @Override
            public void keyReleased(KeyEvent e) {}
        });


        pack();
        setVisible(true);
    }

    private void read(String command){
        println(command);

        // Write to Process
        if (outStream != null) {
            System.out.println("Outstream again");
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outStream));
            try {
                writer.write(command);
                //writer.flush();
                //writer.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }

        // Execute Command
        try {
            exec(command);
        } catch (IOException e) {}

        inPane.setText("");
    }

    private void exec(String command) throws IOException{
        Process pro = Runtime.getRuntime().exec(command, null);

        inStream = pro.getInputStream();
        inErrStream = pro.getErrorStream();
        outStream = pro.getOutputStream();

        Thread t1 = new Thread(new Runnable() {
            public void run() {
                try {
                    String line = null;
                    while(true){
                        BufferedReader in = new BufferedReader(new InputStreamReader(inStream));
                        while ((line = in.readLine()) != null) {
                            println(line);
                        }
                        BufferedReader inErr = new BufferedReader(new InputStreamReader(inErrStream));
                        while ((line = inErr.readLine()) != null) {
                            println(line);
                        }
                        Thread.sleep(1000);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
    }

    public void println(String line) {
        Document doc = outPane.getDocument();
        try {
            doc.insertString(doc.getLength(), line + "\n", null);
        } catch (BadLocationException e) {}
    }

    public static void main(String[] args){
        new Console();
    }
}

我不使用提到的 ProcessBuilder ,因为我确实喜欢区分错误和正常流。

2016 年 8 月 29 日更新

在@ArcticLord 的帮助下,我们已经实现了原始问题中的要求。
现在只需解决诸如非终止进程之类的任何奇怪行为。控制台有一个“停止”按钮,只需调用 pro.destroy()。但出于某种原因,这不适用于无限运行的进程,即垃圾邮件输出。

控制台:http://pastebin.com/vyxfPEXC

输入流线缓冲区:http://pastebin.com/TzFamwZ1

执行 的示例代码不是 停止:
public class Infinity{
    public static void main(String[] args){ 
        while(true){
            System.out.println(".");
        }
    }
}

停止的示例代码:
import java.util.concurrent.TimeUnit;

public class InfinitySlow{
    public static void main(String[] args){ 
        while(true){
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(".");
        }
    }
}

最佳答案

你的代码是正确的。只有一些小事你错过了。
让我们从您的 read 开始方法:

private void read(String command){
    [...]
    // Write to Process
    if (outStream != null) {
        [...]
        try {
            writer.write(command + "\n");  // add newline so your input will get proceed
            writer.flush();  // flush your input to your process
        } catch (IOException e1) {
            e1.printStackTrace();
        }
    }
    // ELSE!! - if no outputstream is available
    // Execute Command
    else {
        try {
            exec(command);
        } catch (IOException e) {
            // Handle the exception here. Mostly this means
            // that the command could not get executed
            // because command was not found.
            println("Command not found: " + command);
        }
    }
    inPane.setText("");
}

现在让我们修复您的 exec方法。您应该使用单独的线程来读取正常进程输出和错误输出。此外,我引入了第三个线程,它等待进程结束并关闭 outputStream,因此下一个用户输入不是用于进程而是一个新命令。
private void exec(String command) throws IOException{
    Process pro = Runtime.getRuntime().exec(command, null);

    inStream = pro.getInputStream();
    inErrStream = pro.getErrorStream();
    outStream = pro.getOutputStream();

    // Thread that reads process output
    Thread outStreamReader = new Thread(new Runnable() {
        public void run() {
            try {
                String line = null;
                BufferedReader in = new BufferedReader(new InputStreamReader(inStream));                        
                while ((line = in.readLine()) != null) {
                    println(line);                       
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("Exit reading process output");
        }
    });
    outStreamReader.start();

    // Thread that reads process error output
    Thread errStreamReader = new Thread(new Runnable() {
        public void run() {
            try {
                String line = null;           
                BufferedReader inErr = new BufferedReader(new InputStreamReader(inErrStream));
                while ((line = inErr.readLine()) != null) {
                    println(line);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("Exit reading error stream");
        }
    });
    errStreamReader.start();

    // Thread that waits for process to end
    Thread exitWaiter = new Thread(new Runnable() {
        public void run() {
            try {
                int retValue = pro.waitFor();
                println("Command exit with return value " + retValue);
                // close outStream
                outStream.close();
                outStream = null;
            } catch (InterruptedException e) {
                e.printStackTrace(); 
            } catch (IOException e) {
                e.printStackTrace();
            } 
        }
    });
    exitWaiter.start();
}

现在这应该有效。
如果您输入 ipconfig它打印命令输出,关闭输出流并准备好接收新命令。
如果您输入 cmd它打印输出并让您输入更多 cmd 命令,例如 dircd依此类推,直到您输入 exit .然后它关闭输出流并准备好接收新命令。

您可能会在执行 python 脚本时遇到问题,因为如果它们没有刷新到系统管道中,那么使用 Java 读取 Process InputStreams 就会出现问题。
请参阅此示例 python 脚本
print "Input something!"
str = raw_input()
print "Received input is : ", str

您可以使用 Java 程序运行它并输入输入,但在脚本完成之前您将看不到脚本输出。
我能找到的唯一解决方法是手动刷新脚本中的输出。
import sys
print "Input something!"
sys.stdout.flush()
str = raw_input()
print "Received input is : ", str
sys.stdout.flush()

运行此脚本将如您所愿。

您可以在以下位置阅读有关此问题的更多信息
  • Java: is there a way to run a system command and print the output during execution?
  • Why does reading from Process' InputStream block altough data is available
  • Java: can't get stdout data from Process unless its manually flushed

  • 编辑:我刚刚为 stdout.flush() 找到了另一个非常简单的解决方案Python 脚本的问题。从 python -u script.py 开始并且您无需手动冲洗。这应该可以解决您的问题。

    EDIT2:我们在评论中讨论了这个解决方案的输出和错误流将被混淆,因为它们在不同的线程中运行。这里的问题是当错误流线程出现时,我们无法区分输出写入是否完成。否则带锁的经典线程调度可以处理这种情况。但是无论数据是否流动,我们都有一个连续的流,直到过程完成。因此,我们需要一种机制来记录自从每个流中读取最后一行以来耗时。
    为此,我将介绍一个获取 InputStream 并启动一个线程以读取传入数据的类。该线程将每一行存储在一个队列中,并在流结束时停止。此外,它保存最后一行被读取并添加到队列的时间。
    public class InputStreamLineBuffer{
        private InputStream inputStream;
        private ConcurrentLinkedQueue<String> lines;
        private long lastTimeModified;
        private Thread inputCatcher;
        private boolean isAlive;
    
        public InputStreamLineBuffer(InputStream is){
            inputStream = is;
            lines = new ConcurrentLinkedQueue<String>();
            lastTimeModified = System.currentTimeMillis();
            isAlive = false;
            inputCatcher = new Thread(new Runnable(){
                @Override
                public void run() {
                    StringBuilder sb = new StringBuilder(100);
                    int b;
                    try{
                        while ((b = inputStream.read()) != -1){  
                            // read one char
                            if((char)b == '\n'){
                                // new Line -> add to queue
                                lines.offer(sb.toString());
                                sb.setLength(0); // reset StringBuilder
                                lastTimeModified = System.currentTimeMillis();
                            }
                            else sb.append((char)b); // append char to stringbuilder
                        }
                    } catch (IOException e){
                        e.printStackTrace();
                    } finally {
                        isAlive = false;
                    }
                }});
        }
        // is the input reader thread alive
        public boolean isAlive(){
            return isAlive;
        }
        // start the input reader thread
        public void start(){
            isAlive = true;
            inputCatcher.start();
        }
        // has Queue some lines
        public boolean hasNext(){
            return lines.size() > 0;
        }
        // get next line from Queue
        public String getNext(){
            return lines.poll();
        }
        // how much time has elapsed since last line was read
        public long timeElapsed(){
            return (System.currentTimeMillis() - lastTimeModified);
        }
    }
    

    通过这个类,我们可以将输出和错误读取线程合二为一。当输入读取缓冲区线程存在并且没有消耗数据时,它就存在。在每次运行中,它会检查自上次读取输出以来是否已经过去了一段时间,如果是,则一次打印所有未打印的行。与错误输出相同。然后它会 hibernate 一些毫秒,以免浪费 CPU 时间。
    private void exec(String command) throws IOException{
        Process pro = Runtime.getRuntime().exec(command, null);
    
        inStream = pro.getInputStream();
        inErrStream = pro.getErrorStream();
        outStream = pro.getOutputStream();
    
        InputStreamLineBuffer outBuff = new InputStreamLineBuffer(inStream);
        InputStreamLineBuffer errBuff = new InputStreamLineBuffer(inErrStream);
    
        Thread streamReader = new Thread(new Runnable() {       
            public void run() {
                // start the input reader buffer threads
                outBuff.start();
                errBuff.start();
    
                // while an input reader buffer thread is alive
                // or there are unconsumed data left
                while(outBuff.isAlive() || outBuff.hasNext() ||
                    errBuff.isAlive() || errBuff.hasNext()){
    
                    // get the normal output if at least 50 millis have passed
                    if(outBuff.timeElapsed() > 50)
                        while(outBuff.hasNext())
                            println(outBuff.getNext());
                    // get the error output if at least 50 millis have passed
                    if(errBuff.timeElapsed() > 50)
                        while(errBuff.hasNext())
                            println(errBuff.getNext());
                    // sleep a bit bofore next run
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }                 
                }
                System.out.println("Finish reading error and output stream");
            }          
        });
        streamReader.start();
    
        // remove outStreamReader and errStreamReader Thread
        [...]
    }
    

    也许这不是一个完美的解决方案,但它应该处理这里的情况。

    编辑 (31.8.2016)
    我们在评论中讨论了在实现一个停止按钮时代码仍然存在问题,该按钮会杀死已启动的
    使用 Process#destroy() 处理.产生大量输出的过程,例如在无限循环中
    立即调用 destroy() 销毁.但由于它已经产生了大量必须消耗的产出
    通过我们的 streamReader我们无法恢复正常的程序行为。
    所以我们需要在这里做一些小改动:
    我们将介绍一个 destroy()方法到 InputStreamLineBuffer停止输出读取并清除队列。
    更改将如下所示:
    public class InputStreamLineBuffer{
        private boolean emergencyBrake = false;
        [...]
        public InputStreamLineBuffer(InputStream is){
            [...]
                    while ((b = inputStream.read()) != -1 && !emergencyBrake){
                        [...]
                    }
        }
        [...]
    
        // exits immediately and clears line buffer
        public void destroy(){
            emergencyBrake = true;
            lines.clear();
        }
    }
    

    以及主程序的一些小改动
    public class ExeConsole extends JFrame{
        [...]
        // The line buffers must be declared outside the method
        InputStreamLineBuffer outBuff, errBuff; 
        public ExeConsole{
            [...]
            btnStop.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                     if(pro != null){
                          pro.destroy();
                          outBuff.destroy();
                          errBuff.destroy();
                     }
            }});
        }
        [...]
        private void exec(String command) throws IOException{
            [...]
            //InputStreamLineBuffer outBuff = new InputStreamLineBuffer(inStream);
            //InputStreamLineBuffer errBuff = new InputStreamLineBuffer(inErrStream);        
            outBuff = new InputStreamLineBuffer(inStream);
            errBuff = new InputStreamLineBuffer(inErrStream);    
            [...]
        }
    }
    

    现在它甚至应该能够破坏一些输出垃圾邮件进程。
    注:我发现 Process#destroy()无法销毁子进程。所以如果你开始 cmd在 window 上
    并从那里启动一个 java 程序,你最终会破坏 cmd在 java 程序仍在运行时处理。
    您将在任务管理器中看到它。这个问题不能用java本身解决。它需要
    一些操作系统依赖于外部工具来获取这些进程的 pid 并手动杀死它们。

    关于具有并发输入/输出流的 Java 进程,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33418502/

    有关具有并发输入/输出流的 Java 进程的更多相关文章

    1. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

      我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

    2. ruby - 检查 "command"的输出应该包含 NilClass 的意外崩溃 - 2

      为了将Cucumber用于命令行脚本,我按照提供的说明安装了arubagem。它在我的Gemfile中,我可以验证是否安装了正确的版本并且我已经包含了require'aruba/cucumber'在'features/env.rb'中为了确保它能正常工作,我写了以下场景:@announceScenario:Testingcucumber/arubaGivenablankslateThentheoutputfrom"ls-la"shouldcontain"drw"假设事情应该失败。它确实失败了,但失败的原因是错误的:@announceScenario:Testingcucumber/ar

    3. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

      在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

    4. ruby - 通过 erb 模板输出 ruby​​ 数组 - 2

      我正在使用puppet为ruby​​程序提供一组常量。我需要提供一组主机名,我的程序将对其进行迭代。在我之前使用的bash脚本中,我只是将它作为一个puppet变量hosts=>"host1,host2"我将其提供给bash脚本作为HOSTS=显然这对ruby​​不太适用——我需要它的格式hosts=["host1","host2"]自从phosts和putsmy_array.inspect提供输出["host1","host2"]我希望使用其中之一。不幸的是,我终其一生都无法弄清楚如何让它发挥作用。我尝试了以下各项:我发现某处他们指出我需要在函数调用前放置“function_”……这

    5. ruby - 通过 ruby​​ 进程共享变量 - 2

      我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是

    6. java - 等价于 Java 中的 Ruby Hash - 2

      我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

    7. ruby - 如何进行排列以有效地定制输出 - 2

      这是一道面试题,我没有答对,但还是很好奇怎么解。你有N个人的大家庭,分别是1,2,3,...,N岁。你想给你的大家庭拍张照片。所有的家庭成员都排成一排。“我是家里的friend,建议家庭成员安排如下:”1岁的家庭成员坐在这一排的最左边。每两个坐在一起的家庭成员的年龄相差不得超过2岁。输入:整数N,1≤N≤55。输出:摄影师可以拍摄的照片数量。示例->输入:4,输出:4符合条件的数组:[1,2,3,4][1,2,4,3][1,3,2,4][1,3,4,2]另一个例子:输入:5输出:6符合条件的数组:[1,2,3,4,5][1,2,3,5,4][1,2,4,3,5][1,2,4,5,3][

    8. java - 从 JRuby 调用 Java 类的问题 - 2

      我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

    9. ruby-on-rails - Rails 3.1 中具有相同形式的多个模型? - 2

      我正在使用Rails3.1并在一个论坛上工作。我有一个名为Topic的模型,每个模型都有许多Post。当用户创建新主题时,他们也应该创建第一个Post。但是,我不确定如何以相同的形式执行此操作。这是我的代码:classTopic:destroyaccepts_nested_attributes_for:postsvalidates_presence_of:titleendclassPost...但这似乎不起作用。有什么想法吗?谢谢! 最佳答案 @Pablo的回答似乎有你需要的一切。但更具体地说...首先改变你View中的这一行对此#

    10. java - 我的模型类或其他类中应该有逻辑吗 - 2

      我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

    随机推荐