Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interrupt BufferedReader#readLine() without closing InputStream

The InputStream of my Process should attach and detach whenever the user wants to see it or not. The attaching works fine, but the detach fails. Default answer to interrupt the readLine() method is always to close the stream, but I cant in this case or the Process will finish or at least not available for future attachments. This is how the stream is read:

BufferedReader reader = new BufferedReader(new InputStreamReader(getProcess().getInputStream()));
String line;

while ((line = reader.readLine()) != null) {
    System.out.println(line);
}

To detach I tried some stuff:

  • Close any of the streams, failed: close method is blocking and waits for the readLine()
  • Implement another stream to send null / abortion value with SequenceInputStream, failed: when one InputStream was waiting for input, the other was not even called
  • Use reflections to unlock the read() method inside any of the streams, failed: not sure why, but did not work. Should we go on with this try? Here is the sourcecode:

    try {
    
        Field modifiers = Field.class.getDeclaredField("modifiers");
        modifiers.setAccessible(true);
    
        Field fdecoder = stream.getClass().getDeclaredField("sd");
        fdecoder.setAccessible(true);
        modifiers.setInt(fdecoder, 1);
        StreamDecoder decoder = (StreamDecoder) fdecoder.get(stream);
    
        Field flock = decoder.getClass().getSuperclass().getDeclaredField("lock");
        flock.setAccessible(true);
        modifiers.setInt(flock, 1);
        Object lock = (Object) flock.get(decoder);
    
        synchronized (lock) {
            lock.notifyAll();
        }
    
    } catch (NoSuchFieldException | IllegalAccessException e) {
        Wrapper.handleException(Thread.currentThread(), e);
    }
    

Not sure how I can fix this. Could you help me interrupting the readLine() method without closing the stream, simple and performant? Thanks.

Edit: What do I mean by "performant"? My application has not much users, but a lot of processes. The answer by @EJP is not wrong - but unperformant in the case of my application. I cannot have hundreds of threads for hundreds of processes, but I can have as many processes as I have users watching. That's why I try to interrupt the process gracefully. Fewer threads, less running/blocked threads. Here is the application described (https://imgur.com/VUcYUfi.png) The Thread that sends the information to the user is the same that reads the input.

like image 845
Felix Gaebler Avatar asked Apr 04 '18 13:04

Felix Gaebler


2 Answers

I didn't expect it to work, but futures are actually cancelable (but why?). After @Tarun Lalwani mentioned the TimeLimiter of Googles Guava library, I inspected the code, tried it in my examples (worked!) and rewrote it a bit - make it not time-based, but method-call-based?!

Here is what I got from my research: A wrapper for the BufferedReader:

public class CancelableReader extends BufferedReader {

    private final ExecutorService executor;
    private Future future;

    public CancelableReader(Reader in) {
        super(in);
        executor = Executors.newSingleThreadExecutor();
    }

    @Override
    public String readLine() {

        future = executor.submit(super::readLine);

        try {
            return (String) future.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } catch (CancellationException e) {
            return null;
        }

        return null;

    }

    public void cancelRead() {
        future.cancel(true);
    }

}

This class allows you to use the BufferedReader#readLine() when you need it and cancel it when you want to continue / interrupt the Thread it is running in. Here is some example code of it in action:

public static void main(String[] args) {

    System.out.println("START");

    CancelableReader reader = new CancelableReader(new InputStreamReader(System.in));
    String line;

    new Thread(() -> {

        try {

            Thread.sleep(10000);
            reader.cancelRead();

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

    }).start();

    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }

    System.out.println("END");

}

And the output of it:

START
> Hello World!
Hello World!
> What's up?
What's up?
END //Exactly after 5 seconds, when the cancel was called
> Hey, you still there?
//No output as expected

And the last thing I wanna say is why this and not closing InputStream or create one Thread per process? In this case the InputStream is the stream of a Process, which means we cannot close it. One way would be to unblock readLine() and return null to finish the while-loop, but this is made with Reflection, which is not as beautiful as our solution now and didn't work for any reason. The application uses many processes but has a limited amount of users - thats why we decide for the amount of threads per user and not per process.

I hope you guys will find this Thread in the future and it is helpful for you. Would be awesome if you leave an upvote, so I can get back my rep of the bounty. Dont forget to upvote the comments either! They helped me alot and brought me to the right solution: Interrupt BufferedReader#readLine() without closing InputStream

like image 156
Felix Gaebler Avatar answered Oct 18 '22 04:10

Felix Gaebler


You're going at this back to front.

You can't stop collecting the process's output, or you will stall the child process.

You want to stop displaying the output when the user doesn't want to see it. Look on it as a user interface issue only.

like image 25
user207421 Avatar answered Oct 18 '22 04:10

user207421