1

I'm trying to write a java program to launch multiple processes and redirect stdin to the main program to both those processes, while simultaneously reading the output from those processes to stdout.

At the moment I'm trying to launch vlc processes and later on plan to be able to control them both separately using different keyboard keys, but I would like to understand how to get this to work for any process, for example like a command line or custom telnet client, so you could spawn two command lines and send the same command inputs to both of them, or programmatically substitute parts of the commands going to each process.

The problem I'm having is that it's unpredictable which end will be sending when, so I need essentially a bidirectional pipe, or both pipes to run simultaneously. I haven't been able to work out how to do that, everything I've tried makes it hang waiting for input from either way.

I can launch the two vlc processes, and even read both their output, but I can't send stdin to them without the processes hanging waiting for input.

Here's what I have already:

The parent class:

    package multiVLC;
    
    import java.nio.file.Path;
    import java.nio.file.Paths;
    
    public class multiVLC {
        public static void main(String[] args) {
            Path video1 = Paths.get("C:\\path\\to\\video1.mkv");
            Path video2 = Paths.get("C:\\path\\to\\video2.mkv");
            new VLCProcess(video1).start();
            new VLCProcess(video2).start();
        }
    }

And the process:

package multiVLC;

import java.io.*;
import java.nio.file.Path;
import java.util.Scanner;

public class VLCProcess extends Thread {

    private Path videofile;

    public VLCProcess(Path videofile) {
        this.videofile = videofile;
    }

    public void run() {
        try {
            String[] cmd = {"C:\\Program Files\\VideoLAN\\VLC\\vlc.exe", videofile.toRealPath().toString()};
            ProcessBuilder ps = new ProcessBuilder(cmd);
            ps.redirectErrorStream(true); // combine stdErr and stdOut
            Process pr = ps.start();

            BufferedReader in = new BufferedReader(new InputStreamReader(pr.getInputStream()));
            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(pr.getOutputStream()));
            BufferedReader sysin = new BufferedReader(new InputStreamReader(System.in));

            String line;

            Scanner scanin = new Scanner(in);
            Scanner scansysin = new Scanner(sysin);

            while (true) {  // THIS HANGS
                if (scanin.hasNextLine()) {
                    System.out.println(this.getName() + ":" + scanin.nextLine());
                }
                if (scansysin.hasNextLine()) {
                    out.write(scansysin.nextLine());
                }
                System.out.println("still in loop"); // NEVER PRINTS
            }

//            while ((line = in.readLine()) != null) { // THIS WORKED TO RUN TWO VLC PROCESSES, BUT DOESN'T REDIRECT STDIN
//                System.out.println(this.getName() + ":" + line);
//            }
//            pr.waitFor(); // not sure if this is needed

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

How can I correctly handle the pipes both ways?

localhost
  • 1,253
  • 4
  • 18
  • 29
  • A few notes: • Re `class mulitVLC`: class names start with uppercase by convention. • [Paths.get()](https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/nio/file/Paths.html#get(java.lang.String,java.lang.String...)) "_simply invokes `Path.of(String, String...)`_" and "_It is recommended to obtain a Path via the `Path.of` methods instead of via the `get` methods defined in this class as this class may be deprecated in a future release._". • You can define paths as "C:/path/to/any.file". Java knows how to handle this OS-dependant. – Gerold Broser Jan 01 '21 at 14:26
  • Does this answer your question? [java: how to both read and write to & from process thru pipe (stdin/stdout)](https://stackoverflow.com/questions/4112470/java-how-to-both-read-and-write-to-from-process-thru-pipe-stdin-stdout) – Gerold Broser Jan 01 '21 at 16:45
  • Maybe you can use the `tee` command like on [this](https://unix.stackexchange.com/questions/28503/how-can-i-send-stdout-to-multiple-commands) or [this](https://stackoverflow.com/questions/26555251/windows-cmd-standard-output-multiple-redirection) post? – gthanop Jan 01 '21 at 18:12
  • @GeroldBroser No, the solution for that is assuming that the data is synchronous, ie sending a command and blocking until the response arrives. For my application I need it to be able to either send or receive at any time. From more investigation I think I need to have a thread to wait for the input Scanner that blocks, and poll for those responses in a loop. I haven't been able to achieve it yet however. – localhost Jan 02 '21 at 04:46
  • @gthanop I would much rather find a solution in java than muck around with non-portable shell solutions. – localhost Jan 02 '21 at 04:52
  • @GeroldBroser I did a bit of googling and could literally find zero examples of how to use `Path.of`. All I could find was `Paths.get`. How can I create a path using that from `String filepath = "C:\\folder\\filename.ext"`? My IDE barfed at `Path.of(filepath)` and all other combinations I could think of. – localhost Jan 02 '21 at 13:31
  • 1
    Path.of is only available from JDK11, and is called by Paths.get – DuncG Jan 02 '21 at 13:52

1 Answers1

1

To redirect the input of your program to the output of multiple processes you may try the following code, which will hopefully meet your requirements:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Objects;
import java.util.Scanner;

public class Main {
    
    private static final Charset CHARSET = StandardCharsets.UTF_8; //Make sure you use the proper charset.
    
    public static class StartedProcess implements Runnable {
        
        private final BufferedWriter out;
        private final BufferedReader in;
        private final Process pro;

        public StartedProcess(final Process pro) {
            this.pro = Objects.requireNonNull(pro);
            out = new BufferedWriter(new OutputStreamWriter(pro.getOutputStream(), CHARSET));
            in = new BufferedReader(new InputStreamReader(pro.getInputStream(), CHARSET));
            //err = new BufferedReader(new InputStreamReader(pro.getErrorStream(), CHARSET));
        }
        
        @Override
        public void run() {
            final String n = Thread.currentThread().getName();
            try {
                for (String line = in.readLine(); line != null; line = in.readLine())
                    System.out.println(n + ": " + line);
                System.out.println(n + " stream closed.");
            }
            catch (final IOException iox) {
                System.out.println(n + " stream error: " + iox);
            }
        }
        
        public Process getProcess() {
            return pro;
        }
        
        public BufferedWriter getOutput() {
            return out;
        }
        
        //public BufferedReader getError() {
        //    return err;
        //}
    }
    
    private static StartedProcess startProcess(final String... command) throws IOException {
        final ProcessBuilder pb = new ProcessBuilder(command);
        pb.redirectErrorStream(true);
        final StartedProcess sp = new StartedProcess(pb.start());
        new Thread(sp).start(); //Probably you could better utilize an ExecutorService here.
        return sp;
    }
    
    private static String pathToString(final String pathFirst, final String... pathMore) {
        return Paths.get(pathFirst, pathMore).toString();
    }
    
    public static void main(final String[] args) throws IOException { //You should handle this exception appropriately.
        final String vlc = pathToString("path1", "to", "vlc");
        final String mv1 = pathToString("path2", "to", "movie1.mkv");
        final String mv2 = pathToString("path3", "to", "movie2.mkv");
        
        System.out.println("Starting processes...");
        
        final ArrayList<StartedProcess> processes = new ArrayList<>(Arrays.asList(
                startProcess(vlc, mv1),
                startProcess(vlc, mv2)
        ));
        
        final Scanner scan = new Scanner(System.in, CHARSET.name());
        
        while (!processes.isEmpty()) {
            System.out.println("Waiting for your input...");
            final String line = scan.nextLine();
            final Iterator<StartedProcess> procit = processes.iterator();
            while (procit.hasNext()) {
                try {
                    final BufferedWriter out = procit.next().getOutput();
                    out.write(line);
                    out.newLine();
                    out.flush();
                }
                catch (final IOException iox) {
                    procit.remove();
                }
            }
        }
        
        System.out.println("No processes left.");
    }
}

Basically, the main thread of the program is resposible for getting user input and then a simple loop to forward to all stored processes' output the user's input.

The input of each spawned process is handled in its own thread, so you should make sure that you synchronize them in case you are sharing resources between them.

System.out is indeed shared between the processes because in this code I am just printing all processes' inputs to it, but don't worry because the PrintStream's methods are synchronized already so you won't get bytes interleaved, but you are going to get lines interleaved.

You shouldn't need to export the InputStream of each StartedProcess and I would advise you not to do so, because it is handled inside the Runnable itself, which is its purpose.

You can code this to use ExecutorService for the Runnables, and to handle Exceptions appropriately.

Edit 1:

I forgot to mention that you are probably going to get your input interleaved with the processes' output and that's because as you type in your CLI the processes are going to write to it at the same time.

gthanop
  • 3,035
  • 2
  • 10
  • 27
  • Thank you so much for the amazing answer! I tried it out last night and I'm still processing the intricacies of how it was constructed and is different to my code. Once again thank you so much for your effort. – localhost Jan 05 '21 at 03:17
  • Glad it helped. :) You may use a GUI to show different outputs (and input) on different GUI components so that you won't get them interleaved. I mean instead of outputing everything to `System.out`. – gthanop Jan 05 '21 at 13:49
  • GUIs is the next step in my java learning. The java GUI ecosystem seems like a mess to me as a beginner but I'm planning to have a go at javaFX soon as ih seems to be the best looking, most supported one. – localhost Jan 06 '21 at 03:41