2015-10-29 26 views
7

Sto provando a creare una sorta di console/terminale che consente all'utente di immettere una stringa, che viene quindi trasformata in un processo e i risultati vengono stampati. Proprio come una normale console. Ma ho problemi a gestire i flussi di input/output. Ho esaminato this thread, ma quella soluzione purtroppo non si applica al mio problema.Processo Java con flussi di input/output simultanei

Insieme ai comandi standard come "ipconfig" e "cmd.exe", devo essere in grado di eseguire uno script e utilizzare lo stesso inputstream per passare alcuni argomenti, se lo script richiede input.

Ad esempio, dopo aver eseguito uno script "python pyScript.py", dovrei essere in grado di passare ulteriori input allo script se lo richiede (esempio: raw_input), mentre si stampa anche l'output dallo script. Il comportamento di base che ti aspetteresti da un terminale.

Quello che ho finora:

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(); 
    } 
} 

io non utilizzare il citato ProcessBuilder, dal momento che mi piace di distinguere tra errore e flusso normale.

UPDATE 29.08.2016

Con l'aiuto di @ArcticLord che abbiamo realizzato quello che è stato chiesto alla domanda iniziale. Ora è solo una questione di stirare qualsiasi comportamento strano come il processo di non terminazione. La console ha un pulsante "stop" che chiama semplicemente pro.destroy(). Ma per qualche ragione questo non funziona per processi infinitamente in esecuzione, cioè output di spamming.

Console: http://pastebin.com/vyxfPEXC

InputStreamLineBuffer: http://pastebin.com/TzFamwZ1

codice

Esempio che fa non fermata:

public class Infinity{ 
    public static void main(String[] args){ 
     while(true){ 
      System.out.println("."); 
     } 
    } 
} 

codice di esempio che non ferma:

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("."); 
     } 
    } 
} 

risposta

10

Sei sulla strada giusta con il tuo codice. Ci sono solo alcune piccole cose che ti sei perso.
Iniziamo con il metodo di 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(""); 
} 

Ora lascia risolvere il tuo metodo di exec. È necessario utilizzare thread separati per leggere il normale output di processo e l'output degli errori. Inoltre, introduco un terzo thread che aspetta che il processo termini e chiude l'outputStream in modo che il prossimo input dell'utente non sia pensato per il processo ma sia un nuovo comando.

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(); 
} 

Ora questo dovrebbe funzionare.
Se si immette ipconfig, stampa l'output del comando, chiude il flusso di output ed è pronto per un nuovo comando.
Se si immette cmd, stampa l'output e consente di immettere ulteriori comandi cmd come dir o cd e così via fino a quando non si immette exit. Quindi chiude il flusso di output ed è pronto per un nuovo comando.

È possibile che si verifichino dei problemi con l'esecuzione degli script Python perché vi sono problemi con la lettura di Process InputStreams con Java se non vengono scaricati nella pipeline del sistema.
vedere questo esempio di script python

print "Input something!" 
str = raw_input() 
print "Received input is : ", str 

È possibile eseguire questo con il vostro programma Java e anche inserire l'ingresso, ma non vedrà l'output dello script finché lo script è terminato.
L'unica correzione che ho trovato è quella di svuotare manualmente l'output nello script.

import sys 
print "Input something!" 
sys.stdout.flush() 
str = raw_input() 
print "Received input is : ", str 
sys.stdout.flush() 

L'esecuzione di questo script funzionerà come previsto.
Si può leggere di più su questo problema a

EDIT: ho appena trovato un'altra soluzione molto facile per il stdout.flush() problema con gli script Python. Avviali con python -u script.py e non è necessario svuotare manualmente. Questo dovrebbe risolvere il tuo problema.

EDIT2: Abbiamo discusso nei commenti che con questa soluzione l'output e l'errore Stream verranno confusi poiché vengono eseguiti in thread diversi. Il problema qui è che non possiamo distinguere se la scrittura in uscita è terminata quando viene visualizzato il thread del flusso di errori. In caso contrario, la classica programmazione dei thread con blocchi potrebbe gestire questa situazione. Ma abbiamo un flusso continuo fino al completamento del processo, indipendentemente dal fatto che i flussi di dati siano o meno. Quindi abbiamo bisogno di un meccanismo che registri quanto tempo è trascorso dall'ultima riga è stata letta da ogni flusso.

Per questo introdurrò una classe che ottiene un InputStream e avvia una discussione per leggere i dati in arrivo. Questa discussione memorizza ogni riga in una coda e si interrompe quando arriva la fine del flusso. Inoltre, contiene il tempo in cui l'ultima riga è stata letta e aggiunta alla coda.

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); 
    } 
} 

Con questa classe è possibile combinare il thread di lettura dell'output e dell'errore in uno. Che vive mentre i thread del buffer di lettura dell'ingresso sono in diretta e non hanno portato dati. In ogni esecuzione controlla se è trascorso un po 'di tempo dall'ultima lettura dell'output e in tal caso stampa tutte le linee non stampate in un colpo solo. Lo stesso con l'output dell'errore. Quindi dorme per alcuni millis per non sprecare tempo in 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 
    [...] 
} 

Forse questa non è una soluzione perfetta ma dovrebbe gestire la situazione qui.


EDIT (31.8.2016)
abbiamo discusso nei commenti che c'è ancora un problema con il codice durante l'implementazione di un pulsante di arresto che uccide il processo iniziato a utilizzare Process#destroy(). Un processo che produce molto output, ad es. in un ciclo infinito lo sarà immediatamente distrutto chiamando lo destroy(). Ma dal momento che ha già prodotto un sacco di output che deve essere consumato dal nostro streamReader non possiamo tornare al normale comportamento del programma.
Quindi abbiamo bisogno di alcune piccole modifiche qui:

Introduciamo un metodo destroy() per InputStreamLineBuffer che interrompe la lettura dell'output e cancella la coda.
I cambiamenti sarà simile a questa:

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(); 
    } 
} 

E alcuni piccoli cambiamenti nel programma principale

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);  
     [...] 
    } 
} 

Ora dovrebbe essere in grado di distruggere anche alcuni processi di output di spamming.

Nota: Ho scoperto che Process#destroy() non è in grado di distruggere i processi figlio. Quindi se avvii cmd su windows e avvii un programma java da lì finirai per distruggere il processo cmd mentre il programma java è ancora in esecuzione. Lo vedrai nel task manager. Questo problema non può essere risolto con java stesso. sarà necessario disporre di alcuni strumenti operativi esterni per ottenere i pid di questi processi e ucciderli manualmente.

+0

Grazie mille! In realtà ho trovato una soluzione simile ieri. Ma c'è un piccolo problema con l'errore e il flusso in uscita. Poiché sono in due thread separati, spesso confondono il loro output. Ad esempio: "cmd", "dir", "foo", "bar". dato che foo in un input non valido, cmd restituisce 2 righe (comando e una nuova riga) con outstream e 2 righe (descrizione dell'errore) con errortream. Ma il risultato è spesso: out, err, out, err. – Haeri

+0

Esatto, ma non è possibile scoprire se la stampa dell'output del comando 'dir' è terminata quando l'errortream inizia a stampare che' foo' non è un comando. Quindi penso che tu possa usare solo 'ProcessBuilder' con 'redirectErrorStream' e ignorare il secondo thread qui. Questo risolverebbe questo problema. – ArcticLord

+0

Stavo sperimentando con redirectErrorStream, ma non sembrava che stampasse effettivamente il flusso degli errori. Solo outStream. E anche non appena avrebbe stampato un errorStream, ha appena terminato il processo. (PS: Mi piace anche avere flussi separati, perché posso contrassegnare l'errortream con il colore rosso). Se solo ci fosse un modo per portarli nel giusto ordine ... – Haeri

2

Sebbene la soluzione di @ArticLord sia carina e ordinata, recentemente ho affrontato lo stesso tipo di problema e ho trovato una soluzione concettualmente equivalente, ma leggermente diversa nella sua implementazione.

Il concetto è lo stesso, vale a dire "letture di massa": quando un thread di lettore acquisisce il proprio turno, consuma tutto il flusso che gestisce e passa la mano solo quando viene eseguito.
Ciò garantisce l'ordine di stampa out/err.

Ma invece di usare un incarico sua volta basato sul timer, io uso un blocco a base di non-blocking simulazione leggere:

// main method for testability: replace with private void exec(String command) 
public static void main(String[] args) throws Exception 
{ 
    // create a lock that will be shared between reader threads 
    // the lock is fair to minimize starvation possibilities 
    ReentrantLock lock = new ReentrantLock(true); 

    // exec the command: I use nslookup for testing on windows 
    // because it is interactive and prints to stderr too 
    Process p = Runtime.getRuntime().exec("nslookup"); 

    // create a thread to handle output from process (uses a test consumer) 
    Thread outThread = createThread(p.getInputStream(), lock, System.out::print); 
    outThread.setName("outThread"); 
    outThread.start(); 

    // create a thread to handle error from process (test consumer, again) 
    Thread errThread = createThread(p.getErrorStream(), lock, System.err::print); 
    errThread.setName("errThread"); 
    errThread.start(); 

    // create a thread to handle input to process (read from stdin for testing purpose) 
    PrintWriter writer = new PrintWriter(p.getOutputStream()); 
    Thread inThread = createThread(System.in, null, str -> 
    { 
     writer.print(str); 
     writer.flush(); 
    }); 
    inThread.setName("inThread"); 
    inThread.start(); 

    // create a thread to handle termination gracefully. Not really needed in this simple 
    // scenario, but on a real application we don't want to block the UI until process dies 
    Thread endThread = new Thread(() -> 
    { 
     try 
     { 
      // wait until process is done 
      p.waitFor(); 
      logger.debug("process exit"); 

      // signal threads to exit 
      outThread.interrupt(); 
      errThread.interrupt(); 
      inThread.interrupt(); 

      // close process streams 
      p.getOutputStream().close(); 
      p.getInputStream().close(); 
      p.getErrorStream().close(); 

      // wait for threads to exit 
      outThread.join(); 
      errThread.join(); 
      inThread.join(); 

      logger.debug("exit"); 
     } 
     catch(Exception e) 
     { 
      throw new RuntimeException(e.getMessage(), e); 
     } 
    }); 
    endThread.setName("endThread"); 
    endThread.start(); 

    // wait for full termination (process and related threads by cascade joins) 
    endThread.join(); 

    logger.debug("END"); 
} 

// convenience method to create a specific reader thread with exclusion by lock behavior 
private static Thread createThread(InputStream input, ReentrantLock lock, Consumer<String> consumer) 
{ 
    return new Thread(() -> 
    { 
     // wrap input to be buffered (enables ready()) and to read chars 
     // using explicit encoding may be relevant in some case 
     BufferedReader reader = new BufferedReader(new InputStreamReader(input)); 

     // create a char buffer for reading 
     char[] buffer = new char[8192]; 

     try 
     { 
      // repeat until EOF or interruption 
      while(true) 
      { 
       try 
       { 
        // wait for your turn to bulk read 
        if(lock != null && !lock.isHeldByCurrentThread()) 
        { 
         lock.lockInterruptibly(); 
        } 

        // when there's nothing to read, pass the hand (bulk read ended) 
        if(!reader.ready()) 
        { 
         if(lock != null) 
         { 
          lock.unlock(); 
         } 

         // this enables a soft busy-waiting loop, that simultates non-blocking reads 
         Thread.sleep(100); 
         continue; 
        } 

        // perform the read, as we are sure it will not block (input is "ready") 
        int len = reader.read(buffer); 
        if(len == -1) 
        { 
         return; 
        } 

        // transform to string an let consumer consume it 
        String str = new String(buffer, 0, len); 
        consumer.accept(str); 
       } 
       catch(InterruptedException e) 
       { 
        // catch interruptions either when sleeping and waiting for lock 
        // and restore interrupted flag (not necessary in this case, however it's a best practice) 
        Thread.currentThread().interrupt(); 
        return; 
       } 
       catch(IOException e) 
       { 
        throw new RuntimeException(e.getMessage(), e); 
       } 
      } 
     } 
     finally 
     { 
      // protect the lock against unhandled exceptions 
      if(lock != null && lock.isHeldByCurrentThread()) 
      { 
       lock.unlock(); 
      } 

      logger.debug("exit"); 
     } 
    }); 
} 

Si noti che entrambe le soluzioni, @ ArticLord di e la mia, non sono del tutto la fame-safe e le probabilità (davvero poche) sono inversamente proporzionali alla velocità dei consumatori.

Buon 2016! ;)

+0

How faccio creare thread per ottenere input e output da un singolo builder di processi e utilizzare un componente jtext –