- All Implemented Interfaces:
Runnable, LimitOutputStream.DoneCallback
- Direct Known Subclasses:
I2PTunnelDCCClient.Runner, I2PTunnelHTTPClientRunner
I2PTunnelRunner implements the core bidirectional data forwarding between I2P and TCP network connections. Operates two StreamForwarders: the I2P to TCP direction runs inline and the TCP to I2P direction runs concurrently via a shared executor pool.
Connection Flow:
- Runner is created with connected I2PSocket and TCP Socket
- Initial data may be sent immediately via initialI2PData/initialSocketData
- Two StreamForwarders run for bidirectional streaming; TCP to I2P via executor pool
- Runner monitors both connections for errors or disconnection
- On completion or failure, callbacks may be invoked and sockets are closed
Keep-Alive Support: When keep-alive is enabled for either connection, the runner may skip spawning one direction of forwarding if no data is expected. This optimization is used for simple GET requests that don't require responses.
Thread Safety: This class uses locks (slock) to coordinate socket access and prevent concurrent writes from multiple threads. The finishLock ensures thread-safe state transitions.
- See Also:
Nested Class Summary
Modifier and TypeClassDescriptionprotected static final classSnapshot of body-delivery progress used byshouldResumeIncompleteBody(long, long, boolean, boolean, boolean).static interfaceFor use in new constructor.private final classPeriodic tick enforcingINITIAL_RESPONSE_TIMEOUT_MS.(package private) static final classWinner of a dual-race empty retry: the socket that produced the first response byte, with a pushback stream that still holds that byte so the forwarder never loses the head of the response.static interfaceCallback for reconnecting via a second route when the first attempt completes with zero upstream bytes, or dies mid-body with an incomplete Content-Length response (Range resume).(package private) static final classProgress-based budget for body-resume cycles.private classForward data in one direction between two streams.static interfaceCallback interface for successful tunnel operation completion.Nested classes/interfaces inherited from class I2PThread
I2PThread.OOMEventListenerModifier and TypeClassDescriptionstatic interfaceListener for out-of-memory events.Nested classes/interfaces inherited from class Thread
Thread.Builder, Thread.State, Thread.UncaughtExceptionHandlerField Summary
FieldsModifier and TypeFieldDescriptionprivate static final AtomicLongCounter numbering the tunnel runners.private longEpoch-ms of the first response byte, 0 until one arrives; retires the initial-response watchdog permanently.private booleanOne-shot latch per request attempt so a fired deadline does not warn and close again every watchdog tick; cleared when the anchor slides or a re-send starts a new attempt.private SimpleTimer2.TimedEventWatchdog for the initial connection, kept for cancelling.private booleanWatchdog lifecycle: set on schedule, cleared on cancel, read by the timer thread so a tick racing run()'s finally cannot re-arm.protected booleanKeep I2P socket alive after data transfer.protected booleanKeep local socket alive after data transfer.private longSliding anchor: epoch-ms of the most recent request-side write upstream.protected final LogThe runner's logger, from the global context's log manager.private booleanPrevent the no-data failure callback from firing more than once across the synchronous completion block and the exception/finally paths.private final I2PTunnelRunner.FailCallbackCallback run when the session fails.private I2PTunnelRunner.SuccessCallbackCallback run when the session succeeds.Optional reconnect callback for the empty-response retry; null = never retry.private ExecutorExecutor for submitting tasks; null = fallback to new Thread.private final longThis runner's id, for the logs.(package private) static final longNo-bytes read timeout (ms) once a response body has started streaming.(package private) static final longBase delay before empty-retry cycle N+1 (ms).(package private) static final longCap for empty-retry inter-cycle delay (ms).(package private) static final longOverall wait for a race winner's first response byte (ms).private booleanTrue once the session has finished.private final ObjectLock for the finish state.private I2PTunnelRunner.StreamForwarderForwarder copying data from I2P.private static final byte[]GET request prefix for proxy mode.private static final byte[]HEAD request prefix for proxy mode.private I2PSocketI2P socket (the tunnel connection).(package private) static final longDeadline for the very first response byte after a request is written upstream (ms).(package private) static final longWatchdog poll interval (ms); SimpleTimer2 rejects periods under 5s.private final byte[]Data to send over I2P before normal forwarding starts.(package private) static longEffective first-response deadline (ms), read per watchdog tick.private final byte[]Data to send over TCP before normal forwarding starts.(package private) static final intMax number of consecutive no-progress cycles (stalls) the outer empty-response / body-resume reconnect loop may invoke the callback for.(package private) static final intMax bytes streamed in a packet - smaller ones might be filled up to this size.(package private) static final intAbsolute safety cap on total body-resume cycles regardless of progress.(package private) static final intHard cap on the size-scaled total cycle budget (SCALED_CAP_MULTIPLIERtimes baseline).(package private) static final intHard cap on the size-scaled stall budget (SCALED_CAP_MULTIPLIERtimes baseline), bounding worst-case browser wait when the destination is genuinely stuck.(package private) static final intNetwork buffer size for buffered streams.(package private) static final longSentinel for "no wall-clock deadline", matchingI2PTunnelClientBase.NO_DEADLINE.private final RunnableIf we die before receiving any data, run this job.private static final byte[]POST request prefix for proxy mode.private static final byte[]PUT request prefix for proxy mode.(package private) static final intBudget cost of one dual-race open (two SYNs).(package private) static final intToken refill rate: tokens per second, keyed per remote dest.(package private) static final longStagger between the two race connects (ms) to avoid same-tick double SYN.(package private) static final intPer-dest race token bucket capacity (SYNs worth of budget).(package private) static final longWall-clock ceiling on one entity's resume/reconnect sequence (ms), independent of Content-Length.(package private) static final longLargest Content-Length that may enlarge the retry ramp (bytes).(package private) static final longEntity-size unit for retry-budget ramping: size-scaled limits gain one cycle (or connect attempt) per this many bytes of Content-Length.private final SocketPlain TCP socket (local or remote endpoint).(package private) static final intMultiplier applied to a baseline budget to cap the size-scaled ramps.(package private) static final intBudget cost of one single-socket open on the empty path.private final ObjectSynchronization lock for socket access.The I2P sockets this session opened.private final longWhen runner started up.private I2PTunnelRunner.StreamForwarderForwarder copying data to I2P.private longTotal bytes received from I2P.private longTotal bytes sent to I2P.Fields inherited from class Thread
MAX_PRIORITY, MIN_PRIORITY, NORM_PRIORITYConstructor Summary
ModifierConstructorDescriptionprivateI2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List<I2PSocket> sockList, Runnable onTimeout, I2PTunnelRunner.FailCallback onFail, boolean keepAliveI2P, boolean keepAliveSocket, boolean shouldStart) Base constructor with keepAlive args.I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List<I2PSocket> sockList, I2PTunnelRunner.FailCallback onFail) Recommended new constructor.privateI2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List<I2PSocket> sockList, I2PTunnelRunner.FailCallback onFail, boolean shouldStart) Base constructor.I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List<I2PSocket> sockList, I2PTunnelRunner.FailCallback onFail, boolean keepAliveI2P, boolean keepAliveSocket) With keepAlive args.Method Summary
Modifier and TypeMethodDescriptionprivate voidDisarm the initial-response watchdog; called from run()'s finally so a completed runner never leaves a timer event behind.(package private) booleancheckInitialResponseDeadline(long nowMs) Enforce the first-response deadline: close the I2P socket if no response byte arrived withininitialResponseTimeoutMsof the last request-side write.protected voidclose(OutputStream out, InputStream in, OutputStream i2pout, InputStream i2pin, Socket s, I2PSocket i2ps, Thread t1, Thread t2) Warning - overridden inI2PTunnelHTTPClientRunner.private static voidcloseQuietly(I2PSocket sock) Closes a socket, ignoring any error.(package private) static longemptyCycleDelayMs(int cycle) Delay before empty-retry cyclecycle(1-based, after the cycle counter has been incremented).protected I2PTunnelRunner.BodyProgressEntity-body progress for a mid-body Range resume.(package private) booleanChecks if the I2P socket should be kept open after data transfer.(package private) booleanChecks if the local socket should be kept open after data transfer.(package private) ExecutorThe tunnel's runner executor (set bysetExecutor(Executor)), or null when the runner will spawn a dedicated thread instead.protected InputStreamGets the TCP socket input stream.protected OutputStreamGets the TCP socket output stream.longReturns the timestamp when this runner started.protected booleanWhether a transient status (408/502/503/504) is currently latched on the response stream — observed without clearing, unlikewasTransientResumeFailure().private static intindexOfHeaderEnd(byte[] data) Index of the first\r\n\r\n(or\n\n) header terminator.(package private) static booleaninitialResponseExpired(long lastRequestWriteMs, long firstByteMs, long nowMs, long timeoutMs) Whether the first-response deadline has elapsed for the current request attempt (pure predicate).private booleanWhether an incomplete body is still eligible for resume — used by the forwarder finally-block to keep the browser stream open.private static booleanisRangeHeaderLine(byte[] data, int start, int end) Whether[start, end)is aRange:header line (not the request line).(package private) static booleanisRetryableRequest(byte[] initialData) Whether the buffered initial request is safe to re-send on a fresh connection in the "empty response" retry.(package private) voidRecord the first response byte upstream: permanently retires the initial-response watchdog from here on (BODY_STALL_READ_TIMEOUT_MScovers further stalling).(package private) voidRecord a request-side write upstream: start (or slide, or restart) the first-response deadline for the current attempt.(package private) voidnoteRequestWritten(long nowMs) noteRequestWritten()with an explicit timestamp (test hook).protected voidInvoke the no-data failure callback when a transfer completes (or aborts) without delivering any bytes from the I2P peer.protected voidInvoke the no-data failure callback with an optional cause.(package private) static InputStreampickRedriveStream(InputStream supplied, I2PSocket sock) Choose the stream a re-driven receive forwarder reads from.protected voidPrepare the HTTP response stream to swallow the next header block (mid-body Range resume).(package private) I2PTunnelRunner.RaceWinraceEmptyPair(I2PSocket[] pair, byte[] request) Race two freshly connected sockets for an empty-response retry: write the buffered request to both, then the first socket to deliver a non-EOF byte wins.private voidredriveReceiveForwarder(OutputStream out, InputStream i2pin) Re-drive the I2P→browser forwarder inline after a reconnect, waiting up to the standard 180s for completion (same policy as the initial run).(package private) static long[]refillRaceBudget(long[] state, long now) Refill a per-dest race token bucket in place.private voidRemove this runner's I2PSocket from the shared socket list.(package private) static booleanresumeDeadlineExpired(long deadlineMs, long nowMs) Whether a resume sequence has run past its wall-clock deadline.private voidResume an incomplete Content-Length body on a fresh I2P connection.voidrun()Serves one connection: writes the buffered request, starts the forwarders and keeps them going, reconnecting if the upstream returns nothing.private static intscaledCycleLimit(long contentLength, int baseline, int maxCap) Shared size-scaled cycle cap.private voidArm the first-response watchdog after the request has been written.voidsetExecutor(Executor exec) Set the executor for submitting forwarder tasks.voidSet the reconnect callback used for the empty-response retry.voidSets a callback to be invoked on successful data transfer.(package private) static booleanshouldFallbackFullBody(long bodyReceived, long dataExpected) Whether a non-Range full re-request is safe after a transient status (408/502/503/504) aborted a Range resume attempt.(package private) static booleanshouldFireNoDataFailure(long totalReceived, boolean handled) Whether a transfer with the given upstream byte count and handling state should trigger the no-data failure callback.(package private) static booleanshouldReconnectEmptyResponse(long totalReceived, boolean hasReconnectCallback, boolean retryableRequest) Whether an empty-upstream transfer should be retried on a fresh I2P connection.(package private) static booleanshouldResumeIncompleteBody(long bodyReceived, long dataExpected, boolean hasReconnectCallback, boolean retryableRequest, boolean canRangeResume) Whether an incomplete Content-Length body should be resumed via HTTP Range on a fresh I2P connection (new tunnel).private static booleansleepQuietly(long delayMs) Sleeps, reporting whether interrupted.(package private) static intstallCycleLimit(long contentLength) Size-scaled cap on consecutive no-progress resume cycles.private voidstartRaceLeg(String label, I2PSocket sock, PushbackInputStream in, AtomicReference<I2PTunnelRunner.RaceWin> winner, AtomicInteger failures, CountDownLatch settled) One race leg: read a single first byte, unread it into a pushback stream, and publish the win.private static booleanstartsWithIgnoreCase(byte[] data, byte[] prefix) Case-insensitive ASCII prefix match ofprefixagainstdata.voidThe DoneCallback for the I2P socket.private static byte[]stripRangeHeader(byte[] request, int headerEnd) Return a copy ofrequestwith anyRange:header line removed.(package private) static inttotalCycleLimit(long contentLength) Size-scaled cap on total body-resume cycles regardless of progress.protected booleanWhether this runner type arms the first-response deadline watchdog.(package private) static booleantryConsumeRaceBudget(long[] state, long now, int costTokens) Try to consumecostTokensfrom the race budget, refilling first.protected booleanWhether the last body-resume attempt failed with a transient status (408/502/503/504) rather than a real stall.(package private) static byte[]withoutRangeHeader(byte[] request) Returnrequestwith everyRange:header line removed — the non-Range fallback form re-requested after a transient status (408/502/503/504) aborts a Range resume.(package private) static byte[]withRangeHeader(byte[] request, long start) Rewrite a buffered GET/HEAD request to resume atstartvia a Range header, replacing any Range the browser already sent.Methods inherited from class I2PAppThread
addOOMEventThreadListener, fireOOM, removeOOMEventThreadListenerModifier and TypeMethodDescriptionvoidRegister a new component that wants notification of OOM events.protected voidfireOOM(OutOfMemoryError oom) Notify listeners that an OutOfMemoryError occurred.voidUnregister a component that wants notification of OOM events.Methods inherited from class I2PThread
addOOMEventListener, removeOOMEventListener, startModifier and TypeMethodDescriptionstatic voidRegister a new component that wants notification of OOM events.static voidUnregister a component that wants notification of OOM events.voidstart()Overridden to provide useful info to users on OOM, and to prevent shutting down the whole JVM for what is most likely not a heap issue.Methods inherited from class Thread
activeCount, clone, currentThread, dumpStack, enumerate, getAllStackTraces, getContextClassLoader, getDefaultUncaughtExceptionHandler, getName, getPriority, getStackTrace, getState, getThreadGroup, getUncaughtExceptionHandler, holdsLock, interrupt, interrupted, isAlive, isDaemon, isInterrupted, isVirtual, join, join, join, join, ofPlatform, ofVirtual, onSpinWait, setContextClassLoader, setDaemon, setDefaultUncaughtExceptionHandler, setName, setPriority, setUncaughtExceptionHandler, sleep, sleep, sleep, startVirtualThread, threadId, toString, yield
Field Details
_log
The runner's logger, from the global context's log manager.__runnerId
Counter numbering the tunnel runners._runnerId
private final long _runnerIdThis runner's id, for the logs.MAX_PACKET_SIZE
static final int MAX_PACKET_SIZEMax bytes streamed in a packet - smaller ones might be filled up to this size. Larger ones are not split (at least not on Sun's impl of BufferedOutputStream), but that is the streaming api's job...- See Also:
NETWORK_BUFFER_SIZE
static final int NETWORK_BUFFER_SIZENetwork buffer size for buffered streams.- See Also:
MAX_EMPTY_RECONNECT_CYCLES
static final int MAX_EMPTY_RECONNECT_CYCLESMax number of consecutive no-progress cycles (stalls) the outer empty-response / body-resume reconnect loop may invoke the callback for. The callback's own for-loop handles connect-failure backoff, but when the destination is reachable yet always sends zero bytes, each cycle succeeds on attempt 1 and the outer loop would spin forever. This is the baseline for entities underRETRY_RAMP_UNIT_BYTES;stallCycleLimit(long)ramps the cap by one cycle per 1MB of Content-Length so a large file can outlive proportionally more tunnel-pool churn. A cycle that makes any forward progress on the entity body resets this budget (seeMAX_RESUME_CYCLES).- See Also:
MAX_RESUME_CYCLES
static final int MAX_RESUME_CYCLESAbsolute safety cap on total body-resume cycles regardless of progress. Progress resets the stall budget (MAX_EMPTY_RECONNECT_CYCLES) so a slowly-advancing transfer is never abandoned mid-body for lack of progress, but a pathological trickle (one byte per cycle forever) must still terminate. Well above any legitimate need: a healthy resume completes in 1–2 cycles.- See Also:
RETRY_RAMP_UNIT_BYTES
static final long RETRY_RAMP_UNIT_BYTESEntity-size unit for retry-budget ramping: size-scaled limits gain one cycle (or connect attempt) per this many bytes of Content-Length. Entities smaller than this keep the baseline caps untouched.- See Also:
SCALED_CAP_MULTIPLIER
static final int SCALED_CAP_MULTIPLIERMultiplier applied to a baseline budget to cap the size-scaled ramps. The historical caps were 8x baseline under a 4MB ramp unit, i.e. 7x baseline of headroom. The 1MB unit is 4x finer, so preserving the same saturation sizes (112MB for the stall budget, 896MB for the total budget) needs 4 * 7 = 28x baseline of headroom, 29x baseline in total.- See Also:
MAX_SCALED_STALL_CYCLES
static final int MAX_SCALED_STALL_CYCLESHard cap on the size-scaled stall budget (SCALED_CAP_MULTIPLIERtimes baseline), bounding worst-case browser wait when the destination is genuinely stuck.- See Also:
MAX_SCALED_RESUME_CYCLES
static final int MAX_SCALED_RESUME_CYCLESHard cap on the size-scaled total cycle budget (SCALED_CAP_MULTIPLIERtimes baseline).- See Also:
NO_DEADLINE
static final long NO_DEADLINESentinel for "no wall-clock deadline", matchingI2PTunnelClientBase.NO_DEADLINE.- See Also:
RESUME_DEADLINE_MS
static final long RESUME_DEADLINE_MSWall-clock ceiling on one entity's resume/reconnect sequence (ms), independent of Content-Length.The cycle caps in
stallCycleLimit(long)andtotalCycleLimit(long)bound attempt count, but each attempt may block for a full connect timeout, so a large entity's 29x-scaled budget can keep a runner busy for hours against a destination that is answering but never finishing. This deadline is the only bound expressed in the unit the browser actually waits in.Set well above what the baseline caps can consume (4 empty-reconnect cycles, each bounded by a connect timeout) so ordinary small-entity reconnects are never cut short; it only bounds the size-scaled ramp.
- See Also:
RETRY_RAMP_MAX_BYTES
static final long RETRY_RAMP_MAX_BYTESLargest Content-Length that may enlarge the retry ramp (bytes).The ramp exists because a genuinely long transfer survives proportionally more tunnel-pool churn than a small file. A Content-Length beyond this is not a long transfer, it is an upstream claim no honest peer makes, and it must not be able to buy extra retry budget: the cap is already saturated long before this size, so clamping the input costs nothing real and denies a hostile or broken destination the largest rung of the ladder.
- See Also:
EMPTY_CYCLE_BASE_DELAY_MS
static final long EMPTY_CYCLE_BASE_DELAY_MSBase delay before empty-retry cycle N+1 (ms). Combined with exponential growth this keeps successful-connect-but-empty retries from spinning back-to-back and stampeding the remote SYN-burst gate.- See Also:
EMPTY_CYCLE_MAX_DELAY_MS
static final long EMPTY_CYCLE_MAX_DELAY_MSCap for empty-retry inter-cycle delay (ms).- See Also:
RACE_TOKENS_MAX
static final int RACE_TOKENS_MAXPer-dest race token bucket capacity (SYNs worth of budget).- See Also:
RACE_REFILL_PER_SEC
static final int RACE_REFILL_PER_SECToken refill rate: tokens per second, keyed per remote dest.- See Also:
RACE_COST_TOKENS
static final int RACE_COST_TOKENSBudget cost of one dual-race open (two SYNs).- See Also:
SINGLE_COST_TOKENS
static final int SINGLE_COST_TOKENSBudget cost of one single-socket open on the empty path.- See Also:
RACE_STAGGER_MS
static final long RACE_STAGGER_MSStagger between the two race connects (ms) to avoid same-tick double SYN.- See Also:
EMPTY_RACE_TIMEOUT_MS
static final long EMPTY_RACE_TIMEOUT_MSOverall wait for a race winner's first response byte (ms).- See Also:
BODY_STALL_READ_TIMEOUT_MS
static final long BODY_STALL_READ_TIMEOUT_MSNo-bytes read timeout (ms) once a response body has started streaming. Armed after the first body byte so a black-holed tunnel fails over to Range resume instead of pinning the runner for the full browser window.- See Also:
INITIAL_RESPONSE_TIMEOUT_MS
static final long INITIAL_RESPONSE_TIMEOUT_MSDeadline for the very first response byte after a request is written upstream (ms). Without it, a peer that accepts the connection but never sends a byte pins the runner forever: the body stall timeout is only armed after the first byte and streaming inactivity is disabled for HTTP tunnels, so nothing else bounds the wait. The anchor slides forward on every request-side write, so a slow POST upload never trips it; it is re-armed on each empty-response reconnect. 120s matches streaming's default inactivity window and stays well inside browser patience (~300s) across the empty-retry budget. Superseded permanently by the first response byte, which hands off toBODY_STALL_READ_TIMEOUT_MS.- See Also:
INITIAL_WATCHDOG_POLL_MS
static final long INITIAL_WATCHDOG_POLL_MSWatchdog poll interval (ms); SimpleTimer2 rejects periods under 5s.- See Also:
initialResponseTimeoutMs
static volatile long initialResponseTimeoutMsEffective first-response deadline (ms), read per watchdog tick. Non-positive disables the watchdog entirely. Test hook; do not modify in production.s
Plain TCP socket (local or remote endpoint).i2ps
I2P socket (the tunnel connection). Non-final so an "empty response" reconnect may replace it with a fresh connection while the local browser socket stays open. Only ever reassigned withinrun()when aI2PTunnelRunner.ReconnectCallbackis installed and yields a new connection. Volatile so the initial-response watchdog on the timer thread sees the swap when it fires.slock
Synchronization lock for socket access.finishLock
Lock for the finish state.finished
private volatile boolean finishedTrue once the session has finished.initialI2PData
private final byte[] initialI2PDataData to send over I2P before normal forwarding starts.initialSocketData
private final byte[] initialSocketDataData to send over TCP before normal forwarding starts.startedOn
private final long startedOnWhen runner started up.sockList
onTimeout
If we die before receiving any data, run this job._onFail
Callback run when the session fails._onSuccess
Callback run when the session succeeds._reconnectCallback
Optional reconnect callback for the empty-response retry; null = never retry.totalSent
private volatile long totalSentTotal bytes sent to I2P.totalReceived
private volatile long totalReceivedTotal bytes received from I2P._noDataHandled
private boolean _noDataHandledPrevent the no-data failure callback from firing more than once across the synchronous completion block and the exception/finally paths._lastRequestWriteMs
private volatile long _lastRequestWriteMsSliding anchor: epoch-ms of the most recent request-side write upstream. Refreshed by the toI2P forwarder so a slow upload never trips the initial-response deadline; reset on each empty-response re-send._firstByteMs
private volatile long _firstByteMsEpoch-ms of the first response byte, 0 until one arrives; retires the initial-response watchdog permanently._initialDeadlineFired
private volatile boolean _initialDeadlineFiredOne-shot latch per request attempt so a fired deadline does not warn and close again every watchdog tick; cleared when the anchor slides or a re-send starts a new attempt._initialWatchdogOn
private volatile boolean _initialWatchdogOnWatchdog lifecycle: set on schedule, cleared on cancel, read by the timer thread so a tick racing run()'s finally cannot re-arm._initialWatchdog
Watchdog for the initial connection, kept for cancelling._keepAliveI2P
protected volatile boolean _keepAliveI2PKeep I2P socket alive after data transfer._keepAliveSocket
protected volatile boolean _keepAliveSocketKeep local socket alive after data transfer._runnerExecutor
Executor for submitting tasks; null = fallback to new Thread.toI2P
Forwarder copying data to I2P.fromI2P
Forwarder copying data from I2P.GET
private static final byte[] GETGET request prefix for proxy mode.HEAD
private static final byte[] HEADHEAD request prefix for proxy mode.POST
private static final byte[] POSTPOST request prefix for proxy mode.PUT
private static final byte[] PUTPUT request prefix for proxy mode.
Constructor Details
I2PTunnelRunner
public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List<I2PSocket> sockList, I2PTunnelRunner.FailCallback onFail) Recommended new constructor. Does NOT start itself. Caller must call start().- Parameters:
s- the local socket, non-nulli2ps- the I2P socket carrying the tunnel, non-nullslock- the socket lock, non-nullinitialI2PData- may be nullinitialSocketData- may be nullsockList- may be null. Caller must add i2ps to the list! It will be removed here on completion. Will synchronize on slock when removing.onFail- May be null. If non-null and no data (except initial data) was received, it will be run before closing s.
I2PTunnelRunner
public I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List<I2PSocket> sockList, I2PTunnelRunner.FailCallback onFail, boolean keepAliveI2P, boolean keepAliveSocket) With keepAlive args. Does NOT start itself. Caller must call start().- Parameters:
s- the local socket, non-nulli2ps- the I2P socket carrying the tunnel, non-nullslock- the socket lock, non-nullinitialI2PData- may be nullinitialSocketData- may be nullsockList- may be null. Caller must add i2ps to the list! It will be removed here on completion. Will synchronize on slock when removing.onFail- May be null. If non-null and no data (except initial data) was received, it will be run before closing s.keepAliveI2P- Do not close the I2P socket when done.keepAliveSocket- Do not close the local socket when done. For client side only; must be false for server side. NO data will be forwarded from the socket to the i2psocket other than initialI2PData if this is true.
I2PTunnelRunner
private I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List<I2PSocket> sockList, I2PTunnelRunner.FailCallback onFail, boolean shouldStart) Base constructor.- Parameters:
s- the local socket, non-nulli2ps- the I2P socket carrying the tunnel, non-nullslock- the socket lock, non-nullinitialI2PData- may be nullinitialSocketData- may be nullsockList- may be null. Caller must add i2ps to the list! It will be removed here on completion. Will synchronize on slock when removing.onFail- run when the transfer ends early; takes precedence over onTimeoutshouldStart- should thread be started in constructor (bad, false recommended)
I2PTunnelRunner
private I2PTunnelRunner(Socket s, I2PSocket i2ps, Object slock, byte[] initialI2PData, byte[] initialSocketData, List<I2PSocket> sockList, Runnable onTimeout, I2PTunnelRunner.FailCallback onFail, boolean keepAliveI2P, boolean keepAliveSocket, boolean shouldStart) Base constructor with keepAlive args.- Parameters:
s- the local socket, non-nulli2ps- the I2P socket carrying the tunnel, non-nullslock- the socket lock, non-nullinitialI2PData- may be nullinitialSocketData- may be nullsockList- may be null. Caller must add i2ps to the list! It will be removed here on completion. Will synchronize on slock when removing.onTimeout- May be null. If non-null and no data (except initial data) was received, it will be run before closing s.onFail- run when the transfer ends early; takes precedence over onTimeoutkeepAliveI2P- Do not close the I2P socket when done.keepAliveSocket- Do not close the local socket when done. For client side only; must be false for server side. NO data will be forwarded from the socket to the i2psocket other than initialI2PData if this is true.shouldStart- should thread be started in constructor (bad, false recommended)
Method Details
emptyCycleDelayMs
static long emptyCycleDelayMs(int cycle) Delay before empty-retry cyclecycle(1-based, after the cycle counter has been incremented). Cycle 1 races immediately (the prior attempt already failed). Later cycles back off exponentially so a reachable-but-empty dest cannot spin at full rate.- Parameters:
cycle- 1-based empty-retry cycle number- Returns:
- delay in ms before invoking the reconnect callback
initialResponseExpired
static boolean initialResponseExpired(long lastRequestWriteMs, long firstByteMs, long nowMs, long timeoutMs) Whether the first-response deadline has elapsed for the current request attempt (pure predicate).The deadline only ever arms before the first response byte: once any upstream byte has arrived the watchdog retires permanently and
BODY_STALL_READ_TIMEOUT_MStakes over. An unset anchor means no request has been written (server-side runners, or a runner that never started), so nothing can be late.- Parameters:
lastRequestWriteMs- epoch-ms of the most recent request-side write upstream, 0 if nonefirstByteMs- epoch-ms of the first response byte, 0 if none yetnowMs- current epoch-ms, comparable with lastRequestWriteMstimeoutMs- deadline window in ms; non-positive disables it- Returns:
- true if the deadline expired with no response byte
refillRaceBudget
static long[] refillRaceBudget(long[] state, long now) Refill a per-dest race token bucket in place. State is{tokensMilli, lastRefillMs}; tokens are held in milli-units so a whole-second refill can be applied from a millisecond delta without truncation to zero.- Parameters:
state- non-nulllong[2]bucket, updated in placenow- current epoch-ms, the base for the refill delta- Returns:
- the same state array
tryConsumeRaceBudget
static boolean tryConsumeRaceBudget(long[] state, long now, int costTokens) Try to consumecostTokensfrom the race budget, refilling first. Insufficient balance leaves the bucket unchanged and returns false.- Parameters:
state- non-nulllong[2]bucket, updated in place when consumednow- current epoch-ms, forwarded to the refillcostTokens- whole tokens to consume (1 = single open, 2 = dual-race)- Returns:
- true if the budget allowed the open
stallCycleLimit
static int stallCycleLimit(long contentLength) Size-scaled cap on consecutive no-progress resume cycles.A long transfer lives through proportionally more tunnel-pool churn than a small file, so the stall budget ramps by one cycle per
RETRY_RAMP_UNIT_BYTESof the entity's verified Content-Length. Unknown or sub-1MB entities keep theMAX_EMPTY_RECONNECT_CYCLESbaseline, and the result is capped atMAX_SCALED_STALL_CYCLESso a stuck destination still fails within a bounded browser wait.- Parameters:
contentLength- entity Content-Length in bytes, or -1 if unknown- Returns:
- the stall-cycle cap for this entity; never below the baseline
totalCycleLimit
static int totalCycleLimit(long contentLength) Size-scaled cap on total body-resume cycles regardless of progress.Progress resets the stall counter, but every resume attempt counts against this absolute cap, so it must grow with the entity the same way
stallCycleLimit(long)does: one cycle perRETRY_RAMP_UNIT_BYTESof verified Content-Length, baseline below 1MB, capped atMAX_SCALED_RESUME_CYCLESso a pathological trickle still terminates.- Parameters:
contentLength- entity Content-Length in bytes, or -1 if unknown- Returns:
- the total-cycle cap for this entity; never below the baseline
resumeDeadlineExpired
static boolean resumeDeadlineExpired(long deadlineMs, long nowMs) Whether a resume sequence has run past its wall-clock deadline. Pure decision so the bound can be pinned without a live transfer.The cycle caps in
stallCycleLimit(long)andtotalCycleLimit(long)bound attempt count, not elapsed time: every attempt may block for a full connect timeout, so a large entity's 29x-scaled budget can hold a runner busy for hours against a destination that keeps answering but never finishes. This is the only bound expressed in the unit the browser actually waits in, and it must hold even while progress is being made and cycle budget remains.- Parameters:
deadlineMs- absolute deadline in ms, orNO_DEADLINEnowMs- current time in ms, ignored when no deadline applies- Returns:
- true if the deadline applies and has been reached
scaledCycleLimit
private static int scaledCycleLimit(long contentLength, int baseline, int maxCap) Shared size-scaled cycle cap. Pure so both limits stay pinned by one set of tests and neither can drift back to accepting an unclamped Content-Length.- Parameters:
contentLength- entity Content-Length in bytes, or -1 if unknownbaseline- cap for entities at or below the ramp unitmaxCap- hard ceiling on the ramp- Returns:
- the cap for this entity, never below the baseline
getStartedOn
public long getStartedOn()Returns the timestamp when this runner started.This value is set at construction time and represents when the runner was created, not when it started executing.
- Returns:
- the timestamp in milliseconds since epoch when this runner was created
setSuccessCallback
Sets a callback to be invoked on successful data transfer.The callback is invoked after the first byte of data is received from the destination, not when the entire transfer completes. Only one of SuccessCallback, onTimeout, or onFail will be called.
- Parameters:
sc- the callback to invoke on success, may be null
setReconnectCallback
Set the reconnect callback used for the empty-response retry.When set, and this is a no-request-body transfer (GET/HEAD, i.e. the
toI2Pforwarder was never started) that completes with zero upstream bytes,run()will call the callback to obtain a fresh I2P socket and re-drive the request on it, keeping the local browser socket open. The same callback is used for mid-body Range resume when a partial Content-Length body was already delivered. Only theonNoDataFailure(Exception)path triggers an empty reconnect; a mid-body death withtotalReceived > 0triggersresumeIncompleteBody(OutputStream)instead. A genuine non-empty completion (full body, or no Content-Length to resume) never reconnects.- Parameters:
rc- the callback, or null to disable reconnects
setExecutor
Set the executor for submitting forwarder tasks. When null (default), forwarders use a fallback thread.- Parameters:
exec- the executor to run forwarders on, or null for the fallback thread
getRunnerExecutor
Executor getRunnerExecutor()The tunnel's runner executor (set bysetExecutor(Executor)), or null when the runner will spawn a dedicated thread instead.- Returns:
- the executor, or null
getSocketIn
Gets the TCP socket input stream.This method is protected to allow subclasses to override socket access for testing or special handling (e.g., SSL unwrapping).
- Returns:
- the TCP socket's input stream
- Throws:
IOException- if the socket is closed
getSocketOut
Gets the TCP socket output stream.- Returns:
- the TCP socket's output stream
- Throws:
IOException- if the socket is closed
getKeepAliveI2P
boolean getKeepAliveI2P()Checks if the I2P socket should be kept open after data transfer.On the client side, this is true only if the browser and server both support HTTP keep-alive. On the server side, it's true only if the client supports keep-alive.
- Returns:
- true if the I2P socket should remain open for reuse
getKeepAliveSocket
boolean getKeepAliveSocket()Checks if the local socket should be kept open after data transfer.Usually true for client-side connections (browser to proxy). Always false for server-side connections (I2P to local service).
- Returns:
- true if the local socket should remain open for reuse
streamDone
public void streamDone()The DoneCallback for the I2P socket.- Specified by:
streamDonein interfaceLimitOutputStream.DoneCallback
onNoDataFailure
protected void onNoDataFailure()Invoke the no-data failure callback when a transfer completes (or aborts) without delivering any bytes from the I2P peer.This is the single choke point behind the "empty response" class of bugs: without it, a connection that is established to the proxy but never yields an upstream byte (server unresponsive, reset before first data, or the executor rejecting the forwarder) would end by closing the local socket with nothing written, which the browser reports as
NS_ERROR_NET_EMPTY_RESPONSE. The HTTP client runner wires itsI2PTunnelRunner.FailCallbackto a handler that writes a well-formed HTTP error page to the browser socket before closing; this is what turns a silent empty close into a surfaced 5xx. The base implementation simply dispatches to the configuredI2PTunnelRunner.FailCallbackoronTimeout.Safe to call from any completion or exception path;
_noDataHandledguarantees the callback runs at most once even when several paths race. Run even whentotalSent > 0(post body) — the absence of a response is still a failure. Never run when any upstream bytes were received.onNoDataFailure
Invoke the no-data failure callback with an optional cause.- Parameters:
e- the failure cause, ornullfor a clean empty transfer
shouldFireNoDataFailure
static boolean shouldFireNoDataFailure(long totalReceived, boolean handled) Whether a transfer with the given upstream byte count and handling state should trigger the no-data failure callback.This is the decision behind the "empty response" fix: the failure callback must fire iff no upstream bytes were received (an empty transfer is still a failure even when a POST body was sent upstream) and the callback has not already fired for this transfer. Extracted as a pure static predicate so the empty-response contract is unit-testable without a router or live socket.
- Parameters:
totalReceived- upstream bytes received from the I2P peerhandled- whether the no-data failure has already been signalled- Returns:
- true if the failure callback should fire
shouldReconnectEmptyResponse
static boolean shouldReconnectEmptyResponse(long totalReceived, boolean hasReconnectCallback, boolean retryableRequest) Whether an empty-upstream transfer should be retried on a fresh I2P connection.The HTTP client proxy re-sends an idempotent (GET/HEAD) request against a fresh I2P connection after a transfer that produced no upstream bytes, instead of closing the browser socket with nothing (which the browser reports as
NS_ERROR_NET_EMPTY_RESPONSE). Reconnecting is only justified when the transfer was genuinely empty: once the peer has reported a complete HTTP response (even a definitive error such as a 5xx),totalReceivedis positive and a reconnect is both pointless and harmful - it re-drives a request the outproxy already answered, contributing to the very congestion behind slow/failed streams. Guarding on upstream bytes actually received (rather than bytes written to the browser) is essential: a response whose browser write threw (e.g.Pipe closed) still counts as received, so it must terminate, not retry.- Parameters:
totalReceived- upstream bytes received from the I2P peer since reconnect resethasReconnectCallback- whether a reconnect callback is installedretryableRequest- whether the buffered request is an idempotent, body-less GET/HEAD- Returns:
- true if the transfer should be retried on a fresh connection
shouldResumeIncompleteBody
static boolean shouldResumeIncompleteBody(long bodyReceived, long dataExpected, boolean hasReconnectCallback, boolean retryableRequest, boolean canRangeResume) Whether an incomplete Content-Length body should be resumed via HTTP Range on a fresh I2P connection (new tunnel).Used when the browser has already been sent response headers with a definite Content-Length, but the upstream stream died (tunnel failure, write timeout, stall) after only part of the entity arrived. I2P delays are expected; the download must not be abandoned. Reconnecting with
Range: bytes=N-fetches only the remainder so the browser-visible byte stream stays contiguous. Only GET/HEAD qualify (idempotent, no request body). Resume is refused when the body cannot be spliced safely (unknown length, chunked, transparent gzip decode, or headers not yet emitted — the latter is the empty-response case handled instead).- Parameters:
bodyReceived- entity-body bytes already delivered to the browserdataExpected- original response Content-Length, or -1 if unknownhasReconnectCallback- whether a reconnect callback is installedretryableRequest- whether the buffered request is an idempotent GET/HEADcanRangeResume- whether the response stream can safely splice (seeHTTPResponseOutputStream.canRangeResume())- Returns:
- true if the transfer should resume on a fresh connection
shouldFallbackFullBody
static boolean shouldFallbackFullBody(long bodyReceived, long dataExpected) Whether a non-Range full re-request is safe after a transient status (408/502/503/504) aborted a Range resume attempt.Used by the resume loop instead of
shouldResumeIncompleteBody(long, long, boolean, boolean, boolean)while the fallback is pending: the response stream's header-written state was reset when it swallowed the failed resume headers, so the Range-specificcanRangeResumegate can no longer pass even though headers and a partial body were already delivered to the browser. The remaining requirements are the same splice invariants — a definite Content-Length so the delivered prefix is known, and an incomplete body so there is still something to fetch. The caller (resume loop entry) has already required a reconnect callback and an idempotent GET/HEAD.- Parameters:
bodyReceived- entity-body bytes already delivered to the browserdataExpected- original response Content-Length, or -1 if unknown- Returns:
- true if a fresh full-entity request may be issued
withRangeHeader
static byte[] withRangeHeader(byte[] request, long start) Rewrite a buffered GET/HEAD request to resume atstartvia a Range header, replacing any Range the browser already sent.The header is inserted immediately before the terminating blank line of the header block.
start <= 0returns the request unchanged (full entity; no Range needed). A request with no header terminator is returned unchanged rather than corrupted.- Parameters:
request- the raw request bytes (request-line + headers), may be nullstart- first byte offset of the remaining entity (inclusive)- Returns:
- a new request array with the Range header, or the original when no rewrite applies; never null if
requestis non-null
withoutRangeHeader
static byte[] withoutRangeHeader(byte[] request) Returnrequestwith everyRange:header line removed — the non-Range fallback form re-requested after a transient status (408/502/503/504) aborts a Range resume. The response then starts at byte 0, and the resume stream drops the already-delivered prefix so the splice stays seamless.- Parameters:
request- the raw request bytes (request-line + headers), may be null- Returns:
- a copy without Range lines (original array when none present); null if
requestis null
indexOfHeaderEnd
private static int indexOfHeaderEnd(byte[] data) Index of the first\r\n\r\n(or\n\n) header terminator.- Parameters:
data- request or header bytes, may be null- Returns:
- index of the first byte of the terminator, or -1 if absent
stripRangeHeader
private static byte[] stripRangeHeader(byte[] request, int headerEnd) Return a copy ofrequestwith anyRange:header line removed. Only lines before the header terminator are considered.- Parameters:
request- raw request bytesheaderEnd- index fromindexOfHeaderEnd(byte[])- Returns:
- request without Range lines (new array if stripped, else original)
isRangeHeaderLine
private static boolean isRangeHeaderLine(byte[] data, int start, int end) Whether[start, end)is aRange:header line (not the request line).- Parameters:
data- the request bytes, including the request linestart- first byte of the lineend- one past the line (including CRLF)- Returns:
- true if the line starts with
Range:ignoring ASCII case
isRetryableRequest
static boolean isRetryableRequest(byte[] initialData) Whether the buffered initial request is safe to re-send on a fresh connection in the "empty response" retry.A request is retryable iff it is idempotent and carries no streamed body, so re-sending it cannot duplicate a submission or split a byte stream mid-body. Only GET and HEAD qualify: they have no request body by definition and repeating them is safe. The leading ASCII method token is compared case-insensitively because
initialI2PDataholds the raw request bytes as the browser sent them. This mirrors thePOST/PUTguard used when deciding whether to flush the initial packet before the body arrives, and is deliberately independent of the reconnect callback so a future caller cannot enable re-sends for a POST/PUT.- Parameters:
initialData- the buffered request (request-line + headers), may be null- Returns:
- true if the request starts with
GETorHEAD
getBodyProgress
Entity-body progress for a mid-body Range resume. Default: no HTTP response stream (server-side or non-HTTP runner) — resume never applies.- Returns:
- body bytes delivered, content length (-1 if unknown), and whether headers were written / Range can splice; defaults disable resume
prepareBodyResume
protected void prepareBodyResume()Prepare the HTTP response stream to swallow the next header block (mid-body Range resume). No-op when there is no response stream.redriveReceiveForwarder
Re-drive the I2P→browser forwarder inline after a reconnect, waiting up to the standard 180s for completion (same policy as the initial run).- Parameters:
out- browser-facing output stream (possibly HTTP-filtered)i2pin- input stream of the current I2P socket (updated field preferred)
pickRedriveStream
Choose the stream a re-driven receive forwarder reads from.The caller-provided stream always wins when present: the dual-race winner is delivered as a
PushbackInputStreamwhose buffer holds the first response byte the race already consumed, and reacquiringsock.getInputStream()would start past that byte, truncating the response header. A fresh socket stream is used only when the caller supplied none.- Parameters:
supplied- caller-provided stream (race winner's pushback wrapper or a fresh socket stream), may be nullsock- current I2P socket to reacquire from, may be null- Returns:
suppliedwhen non-null; otherwisesock's input stream; otherwise null- Throws:
IOException- ifsockcannot open its input stream
raceEmptyPair
Race two freshly connected sockets for an empty-response retry: write the buffered request to both, then the first socket to deliver a non-EOF byte wins. The loser is closed. A total failure (both empty, timed out, or errored) returns null so the outer cycle can burn an attempt. Pure I/O — no budget/permit decisions live here. All socket cleanup happens infinally: exactly the sockets not returned are closed on every exit path, including unexpected exceptions.- Parameters:
pair- two non-null sockets fromI2PTunnelRunner.ReconnectCallback.reconnectPair(Exception)request- buffered request bytes, may be null (nothing to re-send)- Returns:
- the winner with its pushback stream, or null if neither produced data
startRaceLeg
private void startRaceLeg(String label, I2PSocket sock, PushbackInputStream in, AtomicReference<I2PTunnelRunner.RaceWin> winner, AtomicInteger failures, CountDownLatch settled) One race leg: read a single first byte, unread it into a pushback stream, and publish the win. EOF / timeout / IOException counts as a leg failure; the second failure (or overall timeout) settles the race.- Parameters:
label- "A" or "B" for log contextsock- the socket this leg is reading from (closed by caller on loss)in- pushback stream wrapping that socket's inputwinner- shared winner slot (first CAS wins)failures- shared failure countersettled- counted down when the race has a winner or both legs failed
closeQuietly
Closes a socket, ignoring any error.- Parameters:
sock- the socket to close, may be null
sleepQuietly
private static boolean sleepQuietly(long delayMs) Sleeps, reporting whether interrupted.- Parameters:
delayMs- how long to sleep, in ms- Returns:
- true if the sleep was interrupted
resumeIncompleteBody
Resume an incomplete Content-Length body on a fresh I2P connection.Rotates to a new tunnel via
I2PTunnelRunner.ReconnectCallback, re-sends the buffered GET/HEAD withRange: bytes=N-for the undelivered remainder, and splices the second response body onto the browser stream without re-emitting headers. After a transient status (408/502/503/504) aborts a Range attempt the stream can no longer splice, so the next attempt re-requests the full entity with all Range headers stripped; the fresh 200's already-delivered prefix is then dropped so the splice stays seamless. The budget is progress-based: only consecutive stall cycles (no new body bytes since the previous attempt) count against the size-scaled cap fromstallCycleLimit(long)(baselineMAX_EMPTY_RECONNECT_CYCLES); any forward progress resets that counter so a slowly-advancing download is never abandoned for lack of progress.totalCycleLimit(long)is an absolute safety cap on total attempts regardless of progress.- Parameters:
out- browser-facing output stream
wasTransientResumeFailure
protected boolean wasTransientResumeFailure()Whether the last body-resume attempt failed with a transient status (408/502/503/504) rather than a real stall. Subclasses with an HTTP response stream override this; the base runner has no stream so it always returns false.- Returns:
- true if the last resume aborted with a transient status
hasTransientResumeFailure
protected boolean hasTransientResumeFailure()Whether a transient status (408/502/503/504) is currently latched on the response stream — observed without clearing, unlikewasTransientResumeFailure(). The resume loop clears the latch when it observes the failure. The forwarder's finally-block runs first and uses this peek to keep the browser stream open while the non-Range fallback is still pending.- Returns:
- true if a transient status aborted the last resume attempt and the loop has not yet observed it
isBodyResumePending
private boolean isBodyResumePending()Whether an incomplete body is still eligible for resume — used by the forwarder finally-block to keep the browser stream open. A latched transient status means the non-Range fallback is still pending. Closing the browser stream now would sever the splice target before the loop can re-request the full entity.- Returns:
- true if resume should run (or may still run) after this forwarder
startsWithIgnoreCase
private static boolean startsWithIgnoreCase(byte[] data, byte[] prefix) Case-insensitive ASCII prefix match ofprefixagainstdata.- Parameters:
data- the bytes to test, may be nullprefix- the byte sequence to match at the start ofdata- Returns:
- true if
datais non-null, at least as long asprefix, and equalsprefixignoring ASCII case
trackInitialResponseDeadline
protected boolean trackInitialResponseDeadline()Whether this runner type arms the first-response deadline watchdog. False in the base runner so idle long-lived sessions (HTTP CONNECT, SOCKS, IRC, server-side tunnels) are never torn down for silence; the HTTP request/response runner overrides it to true. For CONNECT the remote proxy's 200 response would retire the watchdog anyway, but the default keeps the deadline strictly scoped to request/response flows.- Returns:
- true to schedule the initial-response watchdog
noteRequestWritten
void noteRequestWritten()Record a request-side write upstream: start (or slide, or restart) the first-response deadline for the current attempt. Sliding on every toI2P write keeps a slow POST upload from tripping the deadline; a re-send after an empty response restarts it for the fresh attempt. Clears the fired latch so the new attempt may fire independently.noteRequestWritten
void noteRequestWritten(long nowMs) noteRequestWritten()with an explicit timestamp (test hook).- Parameters:
nowMs- epoch-ms to record as the request write anchor
noteFirstResponseByte
void noteFirstResponseByte()Record the first response byte upstream: permanently retires the initial-response watchdog from here on (BODY_STALL_READ_TIMEOUT_MScovers further stalling).checkInitialResponseDeadline
boolean checkInitialResponseDeadline(long nowMs) Enforce the first-response deadline: close the I2P socket if no response byte arrived withininitialResponseTimeoutMsof the last request-side write. Closing the socket is what unblocks the receive forwarder (a blocked read throws once the stream closes), and run()'s existing empty-transfer path then invokes the no-data failure callback — the single choke point — so the browser sees a proper 5xx instead of an indefinite hang. At most one close per request attempt; a subsequent re-send re-arms vianoteRequestWritten().- Parameters:
nowMs- current epoch-ms, fromSystem.currentTimeMillis()- Returns:
- true if the socket was closed by this call
scheduleInitialResponseWatchdog
private void scheduleInitialResponseWatchdog()Arm the first-response watchdog after the request has been written. Idempotent; a no-op whentrackInitialResponseDeadline()is false or the timer rejects scheduling (headless/broken context).cancelInitialResponseWatchdog
private void cancelInitialResponseWatchdog()Disarm the initial-response watchdog; called from run()'s finally so a completed runner never leaves a timer event behind. Safe to call when never scheduled.run
close
protected void close(OutputStream out, InputStream in, OutputStream i2pout, InputStream i2pin, Socket s, I2PSocket i2ps, Thread t1, Thread t2) throws InterruptedException Warning - overridden inI2PTunnelHTTPClientRunner. Here we ignore keepalive and always close both sides. The HTTP flavor handles keepalive.- Parameters:
out- may be nullin- may be nulli2pout- may be nulli2pin- may be nulls- the local socket to close, non-nulli2ps- the I2P socket to close, non-nullt1- may be nullt2- may be null, ignored, we only join t1- Throws:
InterruptedException- if the joining thread is interrupted while waiting on t1
removeRef
private void removeRef()Remove this runner's I2PSocket from the shared socket list.