javasocketstcpinstall4jjavaagents

TCP socket communication between parent and child process blocks after a few seconds (a few hundred bytes)


For test automation, my program starts the installer (= child process):

Runtime.getRuntime().exec(new String[] { "installer.exe", "-J-javaagent:myagent.jar" });

The installer starts, the agent successfuly initializes and starts a ServerSocket.

java.net.Socket socket = new java.net.ServerSocket(44444).accept();

Then my program successfully connects to the agent:

java.net.Socket socket = new java.net.Socket("localhost", 44444);

After 1-3 request/response-cycles, the agent blocks during a socket.write call, and my program blocks during a socket.read call.

However, if I start the installer with cmd /c start (= independent process), everything works as expected:

Runtime.getRuntime().exec(new String[] { "cmd", "/c", "start", "installer.exe", "-J-javaagent:myagent.jar" });

So, the TCP socket breaks down after a few bytes if there is a parent/child process relationship, but works if the processes are independent. Does anyone have a clue what could be going on here?

Maybe related: C++ TCP Socket communication - Connection is working as expected, fails after a couple of seconds, no new data is received and read() and recv() block


Solution

  • When using Runtime.exec or ProcessBuilder to launch a sub-process, care must be taken to consume the stdout/stderr. If the sub-process writes to stream which is not consumed, it will freeze (similar to the effect of Ctrl-S in some terminals). Unfortunately a large proportion of StackOverflow answers show incorrect handling.

    The issue is apparently fixed when adding "cmd.exe","/c", "start" as the CMD kicks off the sub-process and hides the stdout/stderr from Java.

    The simplest way to deal with this is to consume the streams by reading from the Process as redirect to File, reading stdout+err with different threads, or merge stderr -> stdout and read stdout only, or use inheritIO() to share the streams with the caller stdout/stderr.

    ProcessBuilder is better way to deal with the streams:

    Process p = new ProcessBuilder("installer.exe", "-J-javaagent:myagent.jar")
                   // Choose one:
                   .inheritIO()
                   // or redirectError(stderrFile).redirectOutput(stdoutFile)
                   // or redirectError(stderrFile) and consume p.getInputStream()
                   // or redirectErrorStream(true) and consume p.getInputStream()
                   // or redirectErrorStream(true).redirectOutput(stdoutFile)
                   // or consume p.getInputStream() and p.getErrorStream() in different threads
                   .start();
    

    Example code to consume stream when not using redirection to File or inheritIO() as above:

    try(var stdout = p.getInputStream()) {
            stdout.transferTo(System.out);
    }
    

    Don't forget to check the exit code:

    int rc = p.waitFor();