I spent days looking for a way of reading the output of a console app in realtime. Every way I came across doing it was almost identical to all others, but somehow didn't seem to work. Whenever I ran the code, my console command would not output any text for several minutes, leading me to assume that it was actually outputting data at the end of it's execution rather than during. However, now I know that there is some kind of bottleneck and I don't believe it's with my code.
It's difficult to hand over a more minimal reproducible example, because I believe it would make it more difficult to comprehend, so please excuse the verbosity. I'm not even sure if this issue can be reproduced at all, since it appears to be a problem with the environment.
I'm running Windows 7 (I know, I'm uncool).
I'm using IntelliJ IDEA IDE (which almost out-weighs the uncoolness of running Windows 7).
Here's the output of java --version:
Java 11.0.3 2019-04-16 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.3+12-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.3+12-LTS, mixed mode)
To illustrate the problem, I have the following code:
public class Main {
private static Timer timer;
public static void main(String[] args) throws IOException {
java.lang.String[] commands = {"cmd", "/c", "sfc", "/scannow"};
ProcessBuilder pBuilder = new ProcessBuilder(commands);
pBuilder.redirectErrorStream();
timer = new Timer();
try {
dbg("Starting process...");
Process p = pBuilder.start();
dbg("Process started.");
InputStream in = p.getInputStream();
final Scanner scanner = new Scanner(in);
new Thread(() -> {
dbg("Thread started.");
int i = 1;
while (scanner.hasNextLine()) {
String nextLine = scanner.nextLine();
dbg("Line " + i + " - " + nextLine);
i++;
}
scanner.close();
dbg("Closing scanner.");
}).start();
dbg("Waiting for exit status result...");
int exitStatus = p.waitFor();
dbg("exit status: " + exitStatus);
p.destroy();
} catch (NullPointerException | InterruptedException | IOException e) {
e.printStackTrace();
}
}
private static void dbg(String messageToAppend) {
System.out.println(timer.getTimePassedAsStr(messageToAppend));
}
}
I've made a simple timer class to help me debug the code, since it's taking a lot longer to start running than I'd expect. It works fine, so I won't post the code. However, here is the output of running that I get:
[000:00.000] Starting process...
[000:00.149] Process started.
[000:00.150] Input stream created.
[000:00.204] Scanner created.
[000:00.205] Waiting for exit status result...
[000:00.205] Thread started.
[005:51.762] Line 1 -
[005:51.763] Line 2 -
[005:51.763] Line 3 - Beginning system scan. This process will take some time.
[005:51.763] Line 4 -
[005:51.763] Line 5 -
[005:51.764] Line 6 -
[005:51.764] Line 7 - Beginning verification phase of system scan.
[005:51.764] Line 8 -
[012:42.484] Line 9 - Verification 100% complete.
[012:42.485] Line 10 - Windows Resource Protection found corrupt files but was unable to fix some of them.
[012:42.486] Line 11 -
[012:42.486] Line 12 - Details are included in the CBS.Log windir\Logs\CBS\CBS.log. For example
[012:42.487] Line 13 -
[012:42.487] Line 14 - C:\Windows\Logs\CBS\CBS.log
[012:42.487] Line 15 -
[012:42.488] Closing scanner.
[012:42.491] exit status: 0
Usually, when I run the sfc /scannow directly from the console, it starts up almost instantly, but when I run it from within my Java app, there's almost a 6 minute delay before I get any output.
I have also tried executing it through the CMD interpreter like so:
java.lang.String[] commands = {"cmd", "/c", "sfc", "/scannow"};
But I get the same result. I've also tried running it as a single thread, like so:
public static void runSfc() throws IOException {
timer = new Timer();
Runtime rt = Runtime.getRuntime();
String[] commands = {"sfc", "/scannow"};
dbg("Running command...");
Process proc = rt.exec(commands);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream()));
BufferedReader stdError = new BufferedReader(new InputStreamReader(proc.getErrorStream()));
String s;
int i = 1;
dbg("Here is the standard output of the command:");
while ((s = stdInput.readLine()) != null) {
dbg("Line " + i + " - " + s);
i++;
}
i = 1;
dbg("Here is the standard error of the command (if any):");
while ((s = stdError.readLine()) != null) {
dbg("Line " + i + " - " + s);
i++;
}
}
It appears that each subsequent run, be it single-threaded or multi-threaded, takes either roughly the same amount of time or even longer and inexplicably subsequently shorter again. The latest run stands at 15 minutes before I saw the first line of output in the console window.
The app needs to be run as Administrator, if that makes any difference?
It makes no difference whether I run the code from my IDE, or compile it and run it from the command line.
I have tried reinstalling Java - no difference.
I tried using a version ? instead - no difference.
Curiously, when the scan is unable to run because it's already running elsewhere, it started it's initial output after about 25 seconds (down by over 5 minutes). It's the same situation when run within a thread too.
[000:00.000] Running command...
[000:00.203] Here is the standard output of the command:
[000:25.508] Line 1 -
[000:25.509] Line 2 -
[000:25.509] Line 3 - Beginning system scan. This process will take some time.
[000:25.509] Line 4 -
[000:25.509] Line 5 -
[000:25.509] Line 6 - Another servicing or repair operation is currently running.
[000:25.509] Line 7 -
[000:25.509] Line 8 - Wait for this to finish and run sfc again.
[000:25.509] Line 9 -
[000:25.510] Here is the standard error of the command (if any):
[000:25.510] End of execution
Question 1: Why is it taking more than 5 minutes for the process to output anything, when running it from the command line yields output almost instantly? Does anyone else on a Window machine experience this?
Question 2: I wanted to receive progress updates as the scan progressed. I've not realised, that since the progress updates are preceded with a \r and don't end with a \n until the process gets to 100%, that I won't see any of update messages until the scan has finished, since it never actually registers as having received a line of output. Obviously, I need to read bytes instead of lines. Does anyone know a way of reading this kind of text (that starts with a \r and doesn't end with a \n until the end of the process) without my having to reinvent the wheel?
EDIT:
Mean average execution time when executing directly via CLI
13m 53s (5 runs)
Mean average execution time when executing via the Java app
12m 32s (5 runs)
So it looks like execution time within the Java app is roughly the same as executing it directly in the console, and that the execution time isn't slow at all. It's just that it takes a good while before the first line of output is displayed.
I can only conclude that this must indeed be a buffer-flushing issue as #Andreas suggested. As it stands, I see no way around the problem.
Related
I'm parsing the result of executing this composite command
ntpq -c peers | awk ' $0 ~ /^*/ {print $9}'
in order to obtain the offset of the active ntp server.
This is the java code used and executed periodically
public Double getClockOffset() {
Double localClockOffset = null;
try {
String[] cmd = {"/bin/sh",
"-c",
"ntpq -c peers | awk \' $0 ~ /^\\*/ {print $9}\'"};
Process p = Runtime.getRuntime().exec(cmd);
p.waitFor();
BufferedReader buf = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = buf.readLine();
if (!StringUtils.isEmpty(line)) {
localClockOffset = Double.parseDouble(line.trim());
} else {
// Log "NTP -> Empty line - No active servers - Unsynchronized"
}
} catch (Exception e) {
// Log exception
}
return localClockOffset;
}
ntpq result example
> remote refid st t when poll reach delay offset jitter
> ==============================================================================
> *server001s1 .LOCL. 1 u 33 64 377 0.111 -0.017 0.011
> +server002s1 10.30.10.6 2 u 42 64 377 0.106 -0.006 0.027
> +server003s1 10.30.10.6 2 u 13 64 377 0.120 -0.009 0.016
Notice that awk searchs the first line beginnig with '*' and extracts its ninth column. In the example: -0.017
The problem is that sometimes I'm obtaining the no-active-servers log message - intended to appear when there is no server with '*'- while the execution of the command through the console returns a number.
I know that I'm not closing the BufferedReader in that code but is that the reason of this behaviour? A new instance is being created (and left open until garbage collecting) in each method invocation but I think that it shouldn't be the cause of this problem.
Runtime.exec() simply invokes the ProcessBuilder inside it, like that:
public Process More ...exec(String[] cmdarray, String[] envp, File dir)
throws IOException {
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}
see OpenJDK Runtime.java
So there is nothing wrong with using it instead of the ProcessBuilder as is.
The problem is that you invoke:
p.waitFor();
before you obtained the InputStream.
Which means that the process will be already terminated, by the time you obtain the InputStream, and the output stream data might be or might not be available to you, depending on the OS buffering implementation nuances and precise timing of the operations.
So, if you move the waitFor() to the bottom, your code should start working more reliably.
Under Linux however you should normally be able to read the remaining data from the PIPE buffer, even after the writing process has ended.
And the UNIXProcess implementation in OpenJDK, actually makes an explicit use of that, and tries to drain the remaining data, once the process has exited, so that file descriptor can be reclaimed:
/** Called by the process reaper thread when the process exits. */
synchronized void processExited() {
synchronized (closeLock) {
try {
InputStream in = this.in;
// this stream is closed if and only if: in == null
if (in != null) {
byte[] stragglers = drainInputStream(in);
in.close();
this.in = (stragglers == null) ?
ProcessBuilder.NullInputStream.INSTANCE :
new ByteArrayInputStream(stragglers);
}
} catch (IOException ignored) {}
}
}
And this seems to work reliable enough, at least in my tests, so it would be nice to know which specific version of Linux|Unix and JRE your are running.
Have you also considered the possibility of an application-level problem ?
I.e. ntpq is not really guaranteed to always return a * row.
So, it would be nice to remove the awk part from your pipe, to see if there will be some output at all the times.
Another thing to note is that if one of your shell pipeline steps fails (e.g. the ntpq itself), you will also get an empty output, so you will have to track the STDERR as well (e.g. by merging it with STDOUT via the ProcessBuilder).
Sidenote
Doing waitFor before you start consuming the data, is a bad idea in any case, as if your external process will produce enough output to fill the pipe buffer, it will just hang waiting for someone to read it, which will never happen, as your Java process will be locked in waitFor at the same time.
As pointed by Andrew Thompson, you shall try ProcessBuilder instead.
String[] cmd = {"/bin/sh",
"-c",
"ntpq -c peers | awk \' $0 ~ /^\\*/ {print $9}\'"};
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true);
Process proc = pb.start();
BufferedReader buf = new BufferedReader(new
InputStreamReader(proc.getInputStream()));
String line = null;
while ((line = buf.readLine()) != null) {
localClockOffset = Double.parseDouble(line.trim());
break;
}
proc.destroy();
Ref ProcessBuilder
Finally we have found the real problem.
I'm not gonna change the accepted anwser, I think that it's useful too but maybe someone can learn from our experience.
My java program is launched with a shell script. When we execute the script manually, ntpq command is found and invoked successfully. The problem arises when the software is fully deployed. In the final environment we've got a cron scheduled demon that keeps our program alive but PATH established by cron is different from the PATH that our profile has got assigned.
PATH used by cron:
.:/usr/bin:/bin
PATH that we got login for launching the script manually:
/usr/sbin:/usr/bin:/bin:/sbin:/usr/lib:/usr/lib64:/local/users/nor:
/usr/local/bin:/usr/local/lib:.
Usually ntpq is in
/usr/sbin/ntpq
After we found the key of our problem, I search StackOverflow and got this relevant question where the problem is better explained and solved.
How to get CRON to call in the correct PATHs
I have a java restful service method which executes a myscript.sh using processBuilder. My script takes one input (example - myscript.sh /path/to-a/folder).
Inside the script something like this
-> execute a command which is multithreaded i.e parallel processing
-> echo "my message"
Now when call my script from a linux command line it executes fine. First all the threads running finishes and then some text output from threaded command execution shown on terminal and then echo my message is shown.
But when I call the same script from java using processBuilder, the last echo message comes immidiately and execution ends.
Following the way I call my script from java
ProcessBuilder processBuilder = new ProcessBuilder("/bin/bash","/path/to/myscript.sh","/path/to/folder/data");
Process proc = processBuilder.start();
StringBuffer output = new StringBuffer();
BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));
String line = "";
while((line = reader.readLine()) != null){
output.append(line + "\n");
}
System.out.println("### " + output);
I don't know whats happening, how to debug also.
Can someone enlighten me on how to get the same behaviour from shell script when run from terminal or from java processBuilder?
Use ProcessBuilder.redirectErrorStream(boolean redirectErrorStream) with argument true to merge the errors into output. Alternatively, you could also use the shell command syntax cmd 2>&1 to merge the error with output.
These are some of the cases why you may be immediately getting the output of the last echo statement (instead of the script taking time to run and return proper results):
Missing environment variables
The launched bash needs to source .bashrc or some such recource file
The launched bash may not be running in right directory (you can set this in ProcessBuilder)
The launched bash may not be finding some script/executable in its PATH
The launched bash may not be finding proper libraries in the path for any of the executables
Once you merge error, you would be able to debug and see the errors for yourself.
In your context, separate processes may be spawned in two ways:
1) Bash
/path/to/executables/executable &
This will spawn a new executable executable and you need to wait for it to finish. Here's an answer that will help you.
2) Java
Process exec = Runtime.getRuntime().exec(command);
status = exec.waitFor();
Essentially, you need to wait for the process to end before you start reading its std/err streams.
If I understand the problem correctly, adding just this line to your code should suffice: status = exec.waitFor() (Before you obtain the streams)
Here's the JavaDoc for Process.waitFor() :
Causes the current thread to wait, if necessary, until the process represented by this Process object has terminated. This method returns immediately if the subprocess has already terminated. If the subprocess has not yet terminated, the calling thread will be blocked until the subprocess exits.
Returns:
the exit value of the subprocess represented by this Process object. By convention, the value 0 indicates normal termination.
Throws:
InterruptedException - if the current thread is interrupted by another thread while it is waiting, then the wait is ended and an InterruptedException is thrown
I need to kill an external process on windows (WindowsXP 32bit) from my integration test. I thought I'd just use 'taskkill.exe' but I cannot seem to get it working. Basically, every time I kick off a 'taskkill.exe' process from java it returns exit value -1073741515, nothing is printed to std error/output.
To reproduce the problem I wrote this simple application:
public static void main(String[] args) throws Exception {
ProcessBuilder builder = new ProcessBuilder();
//In my real code, I kill process by its pid. However below also shows the problem:
builder.command("taskkill.exe", "/?");
builder.redirectErrorStream(true);
Process p = builder.start();
BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = r.readLine();
System.out.println("out:");
while(line != null) {
System.out.println(line);
line = r.readLine();
}
System.out.println(p.waitFor());
}
More data points:
-1073741515 apparently means "The application failed to initialize properly". Not very helpful for me though ;)
I've tried bunch of combinations of taskkill.exe parameters; I've tried prefixing the command with 'cmd', '/c'. Symptoms are exactly the same
I tried executing other windows programs that live under windows\system32 and I also get -10737...
Executing things like 'dir' or 'echo' works ok.
Any hints on what might be the problem?
Have you tried executing your application as a different user? If you're running your app with a plain batch file in windows, right click and select Run as administrator and see the results. It's likely the account you're running under doesn't have enough rights to execute native apps.
Under Windows, I am unable to reliably manipulate my child process' I/O when my program has been started from the command line. It's frustrating as it is standard for servers to use a console for I/O. GUIs are nice, but I'd really prefer to stick to the command line and keep things simple. I've noticed that child process I/O is just fine when I'm executing my server from the Eclipse IDE, but it's a whole different story being ran from the command line. I can't read or write to the child process, but the process would still be running. I've written some test code below that demonstrates this problem, and I'm hoping the problem could be reproduced on another machine, and then hopefully get a solution out of it. When executed from Eclipse, inherited I/O works as expected. However, when executed from the Windows command prompt, nothing can be read or written to the child process. In both cases, redirecting child process output to a file always succeeds, but input still can't be passed to the child. If there is already a solution to this problem then please link the page.
JRE/JDK Implementation:
>java -version
java version "1.7.0_01"
Java(TM) SE Runtime Environment (build 1.7.0_01-b08)
Java HotSpot(TM) 64-Bit Server VM (build 21.1-b02, mixed mode)
Consider the following code:
package com.comp8nerd4u2.io.test;
/*
* These tests attempt to confirm what I'm experiencing under my build environment
*/
import java.io.File;
import java.io.IOException;
public final class PIOTest {
/** The command to run as a child process. The command itself isn't the test, but what you use to run this Java program is the test. */
private static final String[] COMMAND = {"cmd.exe", "/c", "echo This is a test. Feel free to change this."}; // Change this to just {"cmd.exe"} or some other program that accepts input and you'll see how frustrating this is
/** Controls how the test process is built */
private static final ProcessBuilder PB = new ProcessBuilder(COMMAND);
/** How long to allow the process to run before forcibly terminating it. */
private static final long PROCESS_TIMEOUT = 10000L;
private static final Runnable R = new TimedInterruptWorker(PROCESS_TIMEOUT);
private static int n = 0;
static {
PB.redirectErrorStream(true);
}
private PIOTest() {}
public static void main(String[] args) {
// ----- Begin Tests -----
/*
* Test #1: Let's test putting our command's output onto our standard I/O streams
* Goal condition: Child process outputs expected output, and exits before the timeout. If child process expects input, it should accept entered input.
* Known success factors: Parent process' standard I/O is piped to Eclipse. Tests would probably succeed with Netbeans as well
* Known fail factors: Parent process' standard I/O is piped to Windows Command Prompt
* Result under fail condition: Child process hangs if it fills up its output buffer or requests input, but exits on its own otherwise, unless it took longer than the timeout.
*/
PB.inheritIO();
doTest();
// Test #2: Let's test putting our command's output into a file
PB.redirectOutput(new File("piotest.txt"));
doTest();
}
/**
* Performs the I/O test.
*/
private static void doTest() {
n++;
Process p = null;
try {
p = PB.start();
} catch (IOException e) {
e.printStackTrace();
return;
}
try {
Thread t = new Thread(R);
t.setDaemon(true);
t.start();
System.out.format("[Test #%d] Child exited with status code %d\n", n, p.waitFor());
t.interrupt();
} catch (InterruptedException e) {
p.destroy();
System.out.format("[Test #%d] Child took longer than the timeout.\n", n);
}
}
/**
* Useful for sending interrupts after a certain amount of time has passed.
*
* #author comp8nerd4u2
*/
private static final class TimedInterruptWorker implements Runnable {
private long timeout = 0;
private Thread target = null;
public TimedInterruptWorker(long timeout) {
this(timeout, Thread.currentThread());
}
public TimedInterruptWorker(long timeout, Thread target) {
this.timeout = timeout;
this.target = target;
}
#Override
public void run() {
try {
Thread.sleep(timeout);
} catch (InterruptedException e) {
return;
}
target.interrupt();
}
}
}
UPDATE: I modified the test to accept any command at runtime, and uploaded it to my linux vps server. I ran it from a ssh session and all child processes' I/O can be read and written to with ease. There was one thing that I have noticed. When I opened a interactive bash shell as a child process, and then redirect it's output to a file, CentOS stopped my program, I think. That or my program crashed.
[admin#comp8nerd4u2 piotest]$ java -jar piotest.jar
Enter command to run : bash
[admin#comp8nerd4u2 piotest]$ [Test #1] Child took longer than the timeout.
[1]+ Stopped java -jar piotest.jar
[admin#comp8nerd4u2 piotest]$
First line is my typing in the command. Second line is the bash shell that was spawned but i never typed anything into it so my program kills it after the timeout. It gets ready for the second test, creates the "piotest.txt" file, and then either crashes or is stopped by the OS. The actual test itself was unchanged, except that the test now allows you to enter what command to run at runtime. This works fine in linux, but not in windows. I'm hoping that someone who knows the Win32 API can somehow explain why this test fails in windows.
Have you seen this article? http://www.javaworld.com/jw-12-2000/jw-1229-traps.html?page=1
It sounds to me like you need to service the input/output streams on Windows. The article is about Runtime.exec, but I bet the native code for ProcessBuilder is very similar and has the same type of issues on Windows.
My guess as to why this works on Eclipse on Windows is that Eclipse is servicing the streams on your behalf in order to display things in the Console view.
I know I'm late in answering, but I came across this question before coming across the answer, and wanted to save anybody else in the same boat some searching.
This is actually a known bug for Windows: https://bugs.openjdk.java.net/browse/JDK-8023130
You can get around it by redirecting the streams yourself:
Process p = pb.start();
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
p.waitFor();
br.close();
In my program, closing a java.util.RandomAccessFile sometimes takes exactly 45 seconds (well, almost exactly: between 44.998 and 45.003 seconds). The program creates and closes lots of small files. Usually closing the file is very quick (between 0 and 0.1 seconds). If I debug the program, it's stuck in the native method RandomAccessFile.close0.
The same problem also occurs when using FileOutputStream instead of RandomAccessFile (in which case the program is blocked in the native method FileOutputStream.close0).
Has somebody an idea what that could be? Can you reproduce the problem on your system (I can reproduce it only on a Mac, not on Windows XP; I didn't test yet on Linux)?
Update 2:
This only seems to happend on Mac OS X. I use JDK 1.6.0_22-b04. It happens on both 32-bit and 64-bit. On Windows XP it doesn't seem to occur.
My test case is:
import java.io.File;
import java.io.RandomAccessFile;
public class TestFileClose {
public static void main(String... args) throws Exception {
for (int i = 0; i < 100000; i++) {
String name = "test" + i;
RandomAccessFile r = new RandomAccessFile(name, "rw");
r.write(0);
long t = System.currentTimeMillis();
r.close();
long close = System.currentTimeMillis() - t;
if (close > 200) {
System.out.println("closing " + name +
" took " + close + " ms!");
}
if (i % 2000 == 0) {
System.out.println("test " + i + "/100000");
}
new File(name).delete();
}
}
}
Example output on my machine:
test 0/100000
test 2000/100000
test 4000/100000
test 6000/100000
test 8000/100000
test 10000/100000
closing test10030 took 44998 ms!
test 12000/100000
test 14000/100000
test 16000/100000
closing test16930 took 44998 ms!
test 18000/100000
test 20000/100000
In my case, it turned out to be McAfee antivirus installed on my machine. I had to install it (company policy)...
The problem also showed up if I disabled the on-access scan.
It could be garbage collection activity, triggered by opening/closing a large number of RandomAccessFile objects; there may be nothing magic about 45 seconds - it could just be the time it takes the JVM on your machine to traverse the heap scavenging for objects to free. Having said that, 45 seconds is an awfully long GC pause; One application I worked on recently always suffered full GC's of about 11 seconds.
Try monitoring your program using JConsole or JVisualVM, or when you start the program try adding the following options:
-verbose:gc -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
Then have a look in the gc.log file produced to see what the application stop times are; if you instrument your code to print timestamps you may be able to tie the close() behaviour to specific GC activity:
...
if (close > 200) {
System.out.println(new Date());
System.out.println("closing " + name +
" took " + close + " ms!");
}
...
If it is GC related, in the gc.log file, you'd be looking for Full garbage collections and/or application stop times at around the timestamps your program outputs files.
Tinkering with the heap settings (-Xmx=... and XX:MaxPermSize=...) may then give you a completely different profile.
On a side note, if it is a temporary file, try using File file = File.createTempFile(prefix, suffix) and pass that into the RandomAccessFile - this may create files in /var/tmp (or whatever its called) on OS X, thus using an in-memory file system instead of a disk-based file system.