How to interrupt reading on System.in?
Asked Answered
S

2

4

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?

Surfperch answered 27/3, 2018 at 19:4 Comment(13)
System.in.close()Scriabin
@JBNizet do you really think I haven't tried this?Surfperch
it would be th.close(); no?Stratfordonavon
@Surfperch yes, I do think so. Have you? What's the result?Scriabin
Didn't work, sadly.Surfperch
The correct way to wait for a thread to finish is via join() and not Thread.sleep(). Also calling th.stop() is deprecated and inherently unsafe.Homosporous
It does here. Edit your question, post the code you tried, and the output you got.Scriabin
I guess you have tried that, but otherwise the correct method would be to use th.interrupt(). That should interrupt the IO operation and ideally the Scanner should not catch the exception and the thread should die. If it doesn't work, try closing the scanner, but I'm skeptical about that.Fauver
Strongly related: How to interrupt java.util.Scanner nextLine call.Taunyataupe
Hah. Closing the stream works fine in the IDE (probably because it sets System.in to something else than the default System.in, but it indeed doesn't work in the terminal.Scriabin
@Taunyataupe I edited the question so that it shouldn't be a duplicate anymore.Surfperch
There is no guarantee about whether InputStream operations are interruptible. However, Channels do provide such a guarantee, so wrapping your InputStream may be sufficient.Ahead
Java: how to abort a thread reading from System.in looks like a dup, but doesn't provide a good answer.Laidlaw
S
0

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.

Surfperch answered 28/3, 2018 at 16:19 Comment(4)
Good answer, but you shouldn't swallow the IOException in read(byte b[], int off, int len).Laidlaw
This may spin on EOF. The contract for available() specified in InputStream is 0 when it reaches the end of the input stream.Laidlaw
The swallowed exception code is copied from InputStream.read() and is intentional. But the available() is indeed a problem, thanks for noticing.Surfperch
@Laidlaw nice catch. but there's no way of detecting the EOF prior to read, am I right? this appears to be another Java API fail.Shadbush
S
4

You should design the run method so that it can determine for itself when to terminate. Calling stop() or similar methods upon the thread would be inherently unsafe.

However, there still remains the question of how to avoid blocking inside System.in.read? To do that you could poll System.in.available until it returns > 0 prior to reading.

Example code:

    Thread th = new Thread(() -> {
        try {
            while(System.in.available() < 1) {
                Thread.sleep(200);
            }
            System.in.read();
        } catch (InterruptedException e) {
            // sleep interrupted
        } catch (IOException e) {
            e.printStackTrace();
        }
    });

Of course, it is generally considered favorable to use a blocking IO method rather than polling. But polling does have its uses; in your situation, it allows this thread to exit cleanly.

A Better Approach:

A better approach that avoids polling would be to restructure the code so that any Thread you intend to kill is not allowed direct access to System.in. This is because System.in is an InputStream that should not be closed. Instead the main thread or another dedicated thread will read from System.in (blocking) then write any contents into a buffer. That buffer, in turn, would be monitored by the Thread you intend to kill.

Example code:

public static void main(String[] args) throws InterruptedException, IOException {
    PipedOutputStream stagingPipe = new PipedOutputStream();
    PipedInputStream releasingPipe = new PipedInputStream(stagingPipe);
    Thread stagingThread = new Thread(() -> {
        try {
            while(true) {
                stagingPipe.write(System.in.read());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    });     
    stagingThread.setDaemon(true);
    stagingThread.start();
    Thread th = new Thread(() -> {
        try {
            releasingPipe.read();
        } catch (InterruptedIOException e) {
            // read interrupted
        } catch (IOException e) {
            e.printStackTrace();
        }
    });
    th.start();
    Thread.sleep(1000);
    Thread.sleep(1000);
    th.interrupt();
    Thread.sleep(1000);
    Thread.sleep(1000);
    System.out.println(th.isAlive()); // Outputs false
}       

But Wait! (Another Java API Fail)

Unfortunately, as pointed out by user Motowski, there exists a "Won't Fix" bug in the Java API implementation of PipedInputSteam. So if you use the unmodified library version of PipedInputSteam as shown above, it will sometimes trigger a long sleep via wait(1000). To work around the bug, Developers must make their own FastPipedInputStream subclass as described here.

Shadbush answered 28/3, 2018 at 9:7 Comment(4)
I am trying to make a wrapper InputStream using the System.in.available(), but it doesn't seem to work with the Scanner.Surfperch
Make the thread a daemon instead of killing the application with System.exit(), it should work the sameSurfperch
PipedInputStream is implemeted with polling. They use wait(1000) loops. I don't know why, just checked the JDK 1.8 source.Hearthstone
@MostowskiCollapse it seems you are correct. Another Java API fail! Developers must make their own FastPipedInputStream subclass. #28617675Shadbush
S
0

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.

Surfperch answered 28/3, 2018 at 16:19 Comment(4)
Good answer, but you shouldn't swallow the IOException in read(byte b[], int off, int len).Laidlaw
This may spin on EOF. The contract for available() specified in InputStream is 0 when it reaches the end of the input stream.Laidlaw
The swallowed exception code is copied from InputStream.read() and is intentional. But the available() is indeed a problem, thanks for noticing.Surfperch
@Laidlaw nice catch. but there's no way of detecting the EOF prior to read, am I right? this appears to be another Java API fail.Shadbush

© 2022 - 2024 — McMap. All rights reserved.