Skip to content

Forward run stdin via blocking read instead of polling available()#2936

Open
jozanek wants to merge 4 commits into
scalacenter:mainfrom
jozanek:fix/882-stdin-blocking-read
Open

Forward run stdin via blocking read instead of polling available()#2936
jozanek wants to merge 4 commits into
scalacenter:mainfrom
jozanek:fix/882-stdin-blocking-read

Conversation

@jozanek
Copy link
Copy Markdown
Contributor

@jozanek jozanek commented Jun 2, 2026

Fixes #882.

Root cause

bloop run never delivered usable stdin to the forked application, and it broke on two layers:

  • Server sideForker forwarded stdin with a 50ms poll loop gated on opts.in.available() > 0. On the CLI path opts.in is nailgun's NGInputStream, whose available() returns 0 while data is in flight, so the loop never read — and never drove nailgun's demand protocol, so the server never even asked the client for input.
  • Client side — the nailgun client (the external, unmaintained scala-cli fork of snailgun) read stdin with readLine() and forwarded each line without its newline, so the forked app's readInt/readLine/Scanner never saw a delimiter and blocked forever. It also NPE'd on EOF (line.length() was checked before line == null).

Changes

  • Forker: replace the available() poll with a blocking read on a monix Task run on ExecutionContext.ioScheduler (the same blocking-I/O idiom used by BspServer/TestServer). This drives nailgun's demand protocol so the server actually requests and reads stdin.
  • Drop the external snailgun dependency (unmaintained, no release since 2023) and replace it with a minimal, bloop-native nailgun client under bloop.rifle.internal.nailgun, trimmed to what bloop uses (daemon-thread path only, BloopRifleLogger directly). Its stdin forwarder sends each line with its newline and handles EOF. Adapted from snailgun (Apache-2.0, attributed in-file).
  • Remove the now-unused BloopRifleLogger.nailgunLogger adapter.

Verification

  • RunSpec.canRunApplicationThatReadsStdinWithoutAvailableBytes: a regression test using an InputStream whose available() always returns 0 (simulating NGInputStream). Fails on the old poll loop (15s timeout), passes on the blocking read.
  • End-to-end: bloop run of an app doing println(StdIn.readInt() + StdIn.readInt()) now reads 1/2 and prints 3 (previously hung indefinitely).

@jozanek
Copy link
Copy Markdown
Contributor Author

jozanek commented Jun 3, 2026

@tgodzik feel free to rerun, should not be connected to my changes.

* `opts.in` is nailgun's NGInputStream, whose `available()` returns 0 even
* while data is in flight, which silently drops input. Mirrors the blocking
* `readOutput` threads used below for stdout/stderr. */
val thread = new Thread("bloop-forker-stdin") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether we could reuse any of the monix utilities instead of using Threads directly.

ExecutionContext.ioScheduler.scheduleWithFixedDelay(duration, duration) { wouldn't block as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

@tgodzik
Copy link
Copy Markdown
Contributor

tgodzik commented Jun 5, 2026

Were you able to confirm that it works when using bloop run ? It seems to not read anything at the moment with the example:

import scala.io.StdIn

@main def hello(): Unit =
  val x = StdIn.readInt()
  val y = StdIn.readInt()
  println(x + y)

@jozanek
Copy link
Copy Markdown
Contributor Author

jozanek commented Jun 5, 2026

Hey Tomek! You are right, the test wasn't sufficient. The issue is in snailgun, but since it is not maintained anymore, I am thinking about moving it completely under bloop (500 LOC) and then try to reduce it as much as possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reading input from application doesn't work

2 participants