From 01962754b0a7357e45c8e54dd6b33fc45bfbc567 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 22 Feb 2018 14:54:57 +0000 Subject: [PATCH] Util: Support setSoTimeout() for InternalSockets to keep susimail server connections from hanging too long --- .../net/i2p/util/InternalServerSocket.java | 8 +- .../java/src/net/i2p/util/InternalSocket.java | 17 ++- .../net/i2p/util/TimeoutPipedInputStream.java | 115 ++++++++++++++++++ .../i2p/util/TimeoutPipedOutputStream.java | 34 ++++++ history.txt | 7 ++ .../src/net/i2p/router/RouterVersion.java | 2 +- 6 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 core/java/src/net/i2p/util/TimeoutPipedInputStream.java create mode 100644 core/java/src/net/i2p/util/TimeoutPipedOutputStream.java diff --git a/core/java/src/net/i2p/util/InternalServerSocket.java b/core/java/src/net/i2p/util/InternalServerSocket.java index aa8b94d43e..0c96721ca0 100644 --- a/core/java/src/net/i2p/util/InternalServerSocket.java +++ b/core/java/src/net/i2p/util/InternalServerSocket.java @@ -100,10 +100,10 @@ public class InternalServerSocket extends ServerSocket { InternalServerSocket iss = _sockets.get(Integer.valueOf(port)); if (iss == null) throw new IOException("No server for port: " + port); - PipedInputStream cis = new PipedInputStream(64*1024); - PipedInputStream sis = new PipedInputStream(64*1024); - PipedOutputStream cos = new PipedOutputStream(sis); - PipedOutputStream sos = new PipedOutputStream(cis); + TimeoutPipedInputStream cis = new TimeoutPipedInputStream(64*1024); + TimeoutPipedInputStream sis = new TimeoutPipedInputStream(64*1024); + PipedOutputStream cos = new TimeoutPipedOutputStream(sis); + PipedOutputStream sos = new TimeoutPipedOutputStream(cis); clientSock.setInputStream(cis); clientSock.setOutputStream(cos); iss.queueConnection(new InternalSocket(sis, sos)); diff --git a/core/java/src/net/i2p/util/InternalSocket.java b/core/java/src/net/i2p/util/InternalSocket.java index d681d22703..3bea4d5f5a 100644 --- a/core/java/src/net/i2p/util/InternalSocket.java +++ b/core/java/src/net/i2p/util/InternalSocket.java @@ -98,11 +98,22 @@ public class InternalSocket extends Socket { return ("Internal socket"); } - // ignored stuff - /** warning - unsupported */ + /** + * Supported as of 0.9.34, if constructed with TimeoutPipedInputStream + * and TimeoutPipedOutputStream. Otherwise, does nothing. + * @see TimeoutPipedInputStream + */ @Override - public void setSoTimeout(int timeout) {} + public synchronized void setSoTimeout(int timeout) { + if (_is != null && _is instanceof TimeoutPipedInputStream) + ((TimeoutPipedInputStream) _is).setReadTimeout(timeout); + } + // ignored stuff + + /** + * Always returns 0, even if setSoTimeout() was called. + */ @Override public int getSoTimeout () { return 0; diff --git a/core/java/src/net/i2p/util/TimeoutPipedInputStream.java b/core/java/src/net/i2p/util/TimeoutPipedInputStream.java new file mode 100644 index 0000000000..b2a0e7dd13 --- /dev/null +++ b/core/java/src/net/i2p/util/TimeoutPipedInputStream.java @@ -0,0 +1,115 @@ +package net.i2p.util; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.SocketTimeoutException; + +/** + * Adds setReadTimeout(). + * Must be used with a TimeoutPipedOutputStream. + * + * To support InternalSocket.setSoTimeout(). + * Package private, not a part of the public API, not for general use. + * + * @since 0.9.34 + */ +class TimeoutPipedInputStream extends PipedInputStream { + + private int timeout; + // local version of pkg private in super + private boolean _closedByWriter; + // local version of pkg private in super + private volatile boolean _closedByReader; + + public TimeoutPipedInputStream(int pipeSize) { + super(pipeSize); + } + + /** + * @throws SocketTimeoutException if timeout is reached + */ + @Override + public synchronized int read() throws IOException { + // This is similar to what is done in super, but with a timeout. + // We use local copies of closedByReader and closedByWriter as + // those are package private in super. + // This doesn't add any substantial runtime overhead, + // as we are just doing the 1-second wait and loop here + // instead of in super. + // If a timeout is set, we will always wait here instead of in super. + if (in < 0 && timeout > 0 && !_closedByReader) { + long now = System.currentTimeMillis(); + long end = now + timeout; + while (true) { + if (_closedByWriter) + return -1; + try { + wait(Math.max(1L, Math.min(1000L, end - now))); + } catch (InterruptedException ex) { + throw new InterruptedIOException(); + } + if (in >= 0 || _closedByReader) + break; + now = System.currentTimeMillis(); + if (now >= end) + throw new SocketTimeoutException(); + } + } + return super.read(); + } + + /** + * Must be called before blocking read call. + * @param ms less than or equal to zero means forever + */ + public void setReadTimeout(int ms) { + timeout = Math.max(0, ms); + } + + /** + * To save state. + * We have to do this because can't get to closedByWriter in super. + */ + synchronized void x_receivedLast() { + _closedByWriter = true; + notifyAll(); + } + + /** + * Overridden to save state. + * We have to do this because can't get to closedByReader in super + */ + @Override + public void close() throws IOException { + _closedByReader = true; + super.close(); + } + +/**** + public static void main(String[] args) throws IOException { + TimeoutPipedInputStream in = new TimeoutPipedInputStream(1024); + TimeoutPipedOutputStream out = new TimeoutPipedOutputStream(in); + out.write('a'); + in.setReadTimeout(5555); + long start = System.currentTimeMillis(); + try { + int a = in.read(); + if (a == 'a') + System.out.println("got 1 (pass)"); + else + System.out.println("bad data (fail)"); + in.read(); + System.out.println("got 2 (fail)"); + } catch (IOException ioe) { + ioe.printStackTrace(); + System.out.println("got ioe (pass)"); + } + System.out.println("took " + (System.currentTimeMillis() - start)); + in.setReadTimeout(0); + System.out.println("wait forever"); + in.read(); + } +****/ +} diff --git a/core/java/src/net/i2p/util/TimeoutPipedOutputStream.java b/core/java/src/net/i2p/util/TimeoutPipedOutputStream.java new file mode 100644 index 0000000000..7e8e20cf41 --- /dev/null +++ b/core/java/src/net/i2p/util/TimeoutPipedOutputStream.java @@ -0,0 +1,34 @@ +package net.i2p.util; + +import java.io.IOException; +import java.io.PipedOutputStream; + +/** + * Helper for TimeoutPipedInputStream. + * There isn't any timeout implemented here. + * + * To support InternalSocket.setSoTimeout(). + * Package private, not a part of the public API, not for general use. + * + * @see TimeoutPipedInputStream + * @since 0.9.34 + */ +class TimeoutPipedOutputStream extends PipedOutputStream { + + private final TimeoutPipedInputStream sink; + + public TimeoutPipedOutputStream(TimeoutPipedInputStream snk) throws IOException { + super(snk); + sink = snk; + } + + /** + * Overridden only so we can tell snk. + * We have to do this because TPIS can't get to pkg private receivedLast() in super. + */ + @Override + public void close() throws IOException { + sink.x_receivedLast(); + super.close(); + } +} diff --git a/history.txt b/history.txt index a1808fcf5b..7ba99fd171 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,10 @@ +2018-02-22 zzz + * Util: Support setSoTimeout() for InternalSockets + +2018-02-21 zzz + * Console: Hide options on /configupdate if a package (ticket #2172) + * SusiMail: Add Date header to sent messages + 2018-02-20 zzz * Console: - Redirect to HTTPS if available (ticket #2160) diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index a6204817e3..f2522cd4ed 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 8; + public final static long BUILD = 9; /** for example "-test" */ public final static String EXTRA = "";