Search code examples
javamultithreadinginputstreamsystem.in

How to interrupt reading on System.in?


If I start reading from System.in, it will block the thread until it gets data. There is no way to stop it. Here are all the ways that I've tried:

  • Interrupting the thread
  • Stopping the thread
  • Closing System.in
  • Calling System.exit(0) does indeed stop the thread, but it also kills my application so not ideal.
  • Entering a char into the console makes the method return, but I can't rely on user input.

Sample code that does not work:

public static void main(String[] args) throws InterruptedException {
    Thread th = new Thread(() -> {
        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
    });
    th.start();
    Thread.sleep(1000);
    System.in.close();
    Thread.sleep(1000);
    th.interrupt();
    Thread.sleep(1000);
    th.stop();
    Thread.sleep(1000);
    System.out.println(th.isAlive()); // Outputs true
}

When I run this code, it will output true and run forever.

How do I read from System.in in an interruptible way?


Solution

  • I've written a wrapper InputStream class that allows to be interrupted:

    package de.piegames.voicepi.stt;
    import java.io.IOException;
    import java.io.InputStream;
    
    public class InterruptibleInputStream extends InputStream {
    
        protected final InputStream in;
    
        public InterruptibleInputStream(InputStream in) {
            this.in = in;
        }
    
        /**
         * This will read one byte, blocking if needed. If the thread is interrupted while reading, it will stop and throw
         * an {@link IOException}.
         */     
        @Override
        public int read() throws IOException {
            while (!Thread.interrupted())
                if (in.available() > 0)
                    return in.read();
                else
                    Thread.yield();
            throw new IOException("Thread interrupted while reading");
        }
    
        /**
         * This will read multiple bytes into a buffer. While reading the first byte it will block and wait in an
         * interruptable way until one is available. For the remaining bytes, it will stop reading when none are available
         * anymore. If the thread is interrupted, it will return -1.
         */
        @Override
        public int read(byte b[], int off, int len) throws IOException {
            if (b == null) {
                throw new NullPointerException();
            } else if (off < 0 || len < 0 || len > b.length - off) {
                throw new IndexOutOfBoundsException();
            } else if (len == 0) {
                return 0;
            }
            int c = -1;
            while (!Thread.interrupted())
                if (in.available() > 0) {
                    c = in.read();
                    break;
                } else
                    Thread.yield();
            if (c == -1) {
                return -1;
            }
            b[off] = (byte) c;
    
            int i = 1;
            try {
                for (; i < len; i++) {
                    c = -1;
                    if (in.available() > 0)
                        c = in.read();
                    if (c == -1) {
                        break;
                    }
                    b[off + i] = (byte) c;
                }
            } catch (IOException ee) {
            }
            return i;
        }
    
        @Override
        public int available() throws IOException {
            return in.available();
        }
    
        @Override
        public void close() throws IOException {
            in.close();
        }
    
        @Override
        public synchronized void mark(int readlimit) {
            in.mark(readlimit);
        }
    
        @Override
        public synchronized void reset() throws IOException {
            in.reset();
        }
    
        @Override
        public boolean markSupported() {
            return in.markSupported();
        }
    }
    

    Adjust the Thread.yield() to sleep as long as the maximum latency you can accept and prepare for some exceptions when interrupting, but apart from that it should work fine.