Skip to content

publish: wait for ENTER inside the web login's retry wait - #46884

Open
robobun wants to merge 2 commits into
mainfrom
robobun/00065911/publish-web-login-stdin
Open

robobun wants to merge 2 commits into
mainfrom
robobun/00065911/publish-web-login-stdin

Conversation

@robobun

@robobun robobun commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • After a web login, a thread of bun takes the first line typed at a postpublish or Enter OTP: question. From a pipe it takes 1024-byte blocks.
  • Cause: press_enter_to_open_in_browser (src/runtime/cli/publish_command.rs:883) loops on read(0) and nothing stops it.

Fix

  • The retry wait of the done url is the wait for ENTER (EnterOffer). No reader of stdin outlives the login. The thread and WEB_LOGIN (install: fix regressions and older bugs found after the package manager consolidation #45307) are deleted.
  • A terminal is read through a second, non-blocking open. Other stdin is read byte by byte, to the first line end.
  • Verified: test/cli/install/bun-publish.test.ts, 17 new cases, main fails 5. Self-reviewed: 10 concerns raised, 9 addressed (not: /dev/null for the opener's output).

Background

  • A web login prints the auth url and offers (press ENTER to open in browser). It asks the done url for the token. 202 means: ask again after retry-after.
  • Considered ENTER only on a terminal (npm). Public scripts run echo "" | bun publish, so a maintainer must decide. A pipe's first line is still ENTER.
  • Considered a stoppable thread: a thread and 3 fds more per login.

Downsides

  • ENTER during a done url request opens the page after the answer: 506 ms against 1 ms for 500 ms. A registry that holds the request never gets ENTER.
  • When another process takes a byte of a piped stdin first, the login waits in read for the next one. On main it went on.
  • Windows no longer prints the offer: ENTER never opened a page there (Bun tries to run non-existent executable start #26231).
Notes

How this was found. By reading the code during the review of #36369, which lists it under "Not changed here". #46192 names it too. No user reported lost input after a web login. This change lands before both.

What #45307 fixed is kept. Under -r, ENTER opens the page of the package that is being published. Each login now has its own offer, so there is no state between two logins. Both test rows of #45307 are unchanged and pass.

Rules of the offer

  • The offer is made for an http(s) auth url on every kind of stdin, as on main.
  • Windows makes no offer. start, its opener, is not a program that CreateProcessW can run, so ENTER never opened a page there.
  • ENTER is the first line end. A line that was typed before the login counts.
  • A line that is on stdin is taken whole in one wait. The retry deadline is only for input that is not there yet.
  • A wait that finds 1024 bytes without a line end ends the offer. A stdin such as /dev/zero cannot hold up the login.
  • One ENTER ends the offer. So does the end of input (Ctrl-D, the end of a pipe, /dev/null).
  • A line typed on a terminal during the request that returns the token answered the offer. bun takes it and opens nothing.
  • When bun cannot open the terminal a second time, it reads fd 0 one byte at a time.
  • When poll cannot watch the terminal (macOS, stdin is the /dev/tty alias), there is no offer. This is by source and did not run.
  • When the thread for the opener cannot start, bun prints ThreadSpawn: failed to spawn thread for opening auth url and the login goes on. main stopped the publish there.

Not changed here

  • The first line of a piped stdin is ENTER, as on main. Example: printf 'for the script\n' | bun publish, and the login has to wait. postpublish still reads 0 of 15 bytes, and the opener starts.
  • Five public release scripts in four repositories run OUTPUT=$(echo "" | bun publish ... 2>&1) to open the page. "No ENTER without a terminal" breaks them, so it is a decision for a maintainer. npm has that rule (lib/utils/open-url.js:62 of npm 11.16.0), and the first comment of Native bun login / bun logout #41289 asks for it. It is about 15 lines on top of this change.
  • Under -r, an earlier web login that has to wait takes a piped one-time password as ENTER. It is the same decision.
  • The Enter OTP: question takes up to 4096 bytes of a piped stdin (BufferedStdin). Delete BufferedStdin: prompt(), bun init and the publish OTP question take one line of stdin #36369 owns that.
  • The done url loop has no deadline.
  • The opener inherits stdin, stdout and stderr, as on main. A text-mode browser that xdg-open starts on a machine without a display takes the terminal.
  • AsyncHTTP::send_sync has no timed wait, so bun does not look for ENTER during a request. registry.npmjs.org answers in about 0.2 s with 202 and retry-after: 3.

The read of fd 0. A pipe has one open file description, which every process that inherited it shares. O_NONBLOCK on it would change the reads of those processes too, so bun reads it in blocking mode after poll says that a byte is there. The read waits only when another process takes that byte between the two calls. The same holds for a terminal that bun cannot open a second time.

Measurements (release builds, linux x64, merge base 179664e against this branch)

main branch
PTY, postpublish question after a login without ENTER, first and second typed gets second (20 of 20) gets first (20 of 20)
PTY, -r, Enter OTP: after a web login, two lines typed sends the second, exit 1 (18 of 20) sends the first, exit 0 (20 of 20)
pipe \nfor the script\n, login waits postpublish reads 0 of 16 bytes (20 of 20) reads the 15 after the line end (20 of 20)
pipe open the page\nfor the script\n, one wait of 0 ms reads nothing (20 of 20) reads for the script\n (20 of 20)
pipe for the script\n, done url answers at once reads 0 of 15 (20 of 20) reads 15 of 15 (20 of 20)
stdin is a FIFO in non-blocking mode, line end written later opener never starts (5 of 5) opener starts (5 of 5)
  • Syscalls of the ENTER wait, for a web login on a PTY without ENTER and 3 answers of 202 (ptrace log, 5 runs each): 16 to 12.
    • main: clone3, mmap, mprotect, 2 rt_sigprocmask, 3 clock_nanosleep, and 8 in the thread, which stays in read(0).
    • branch: 2 ioctl, fstat, readlink, newfstatat, openat, 5 poll, close. Threads started: 1 to 0.
  • The same with stdin ignored: 19 to 5 (3 clock_nanosleep, 1 poll, 1 read that returns 0).
  • A publish with no 401: the syscalls of the main thread are equal by type. read(0) 0 to 0, poll 0 to 0.
  • After the login (/proc/<pid>/task/*/syscall, 3 runs each): tasks in read(0) 1 to 0. Open fds: 9 to 10 during the login, 9 to 9 at the PUT with the OTP.
  • ENTER to opener, 20 interleaved runs. During a request held 0, 500, 2000 ms: 4, 506, 2005 ms (main: 1, 1, 1 ms). During the retry wait: 2 ms (main 1 ms). main against main: 1 ms and 1 ms.
  • Heap allocations of the main thread inside get_otp (gdb hit counts on the mi_* entry points, 3 runs each): 56 to 51.
  • size -A: .text 82,164,617 to 82,165,641 (+1,024), .rodata equal, .bss 1,806,328 to 1,806,264 (-64). Symbols of publish_command (nm -S): 41,838 to 42,344 bytes.
  • Not installed here: strace, perf, bloaty, valgrind.

Tests

New cases:

  • ENTER during the retry wait: a 202 with retry-after: 30, the page opens, exactly one done request.
  • ENTER during a request to the done url.
  • ENTER during the request that returns the token.
  • A postpublish question after the login.
  • A second reader of the terminal takes the line (5 runs).
  • The first line of a pipe, then more bytes, and one wait of 0 ms.
  • A pipe, and a login that does not have to wait.
  • A stdin that never has a line end (/dev/zero).
  • The length of the wait (2 rows).
  • stdout redirected.
  • The OTP question under -r, typed and piped.

A release build of main fails 5 of the 17 in every run. It fails a sixth (ENTER during the request that returns the token) in about half of the runs.

Each clause of the change has a case that fails without it. Checked with 10 modified builds:

  • poll with timeout 0
  • no sleep after the wait
  • retry-after ignored
  • the terminal read through fd 0
  • the terminal read through a blocking fd
  • a pipe read in blocks
  • the deadline stops the read of a line
  • no bound on the bytes without a line end
  • no read at the token
  • a read of a pipe at the token

The two cases for a blocking read of the terminal are a race. A single run fails in 12 of 20, so the case runs 5 times.

Unchanged and passing:

The 8 snapshot rows that print the offer check the heading of Windows and then compare with the snapshot of the other platforms.

Results:

  • bun-publish.test.ts: 296 pass on the debug build, and on the release build in 5 of 5 runs.
  • The web login subset (41 cases): 30 of 30 runs on the release build.
  • Also run: cargo check for x86_64-pc-windows-msvc and aarch64-apple-darwin, clippy for bun_runtime, the source lints that read this file.
  • macOS and Windows did not run here. On Windows the change is that nothing reads stdin during a web login. The terminal cases skip Windows, except the OTP question under -r, which runs through ConPTY.

Probe on the debug build. bun publish & in an interactive bash, keys typed at the prompt, then fg: completes, 6 of 6. SIGTTIN stops the job in 3 of 6, and it resumes.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/install/bun-publish.test.ts

A web login started a thread that read stdin until a line end, and
nothing stopped that thread when the login ended. On a terminal it took
the first line typed at a postpublish question, or at the OTP question
of a later package. From a piped stdin it took blocks of 1024 bytes.

The wait between two requests to the done url is now the wait for ENTER,
so no reader of stdin outlives the login. A terminal is read through a
second, non-blocking open of it. A stdin that is not a terminal is read
one byte at a time, up to the first line end. Windows makes no offer of
ENTER, because its opener cannot be spawned.
@robobun

robobun commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:38 AM PT - Oct 25th, 2026

❌ @robobun, your commit b46ac22 has 1 failures in Build #132618 (All Failures):

  • ❌ Baseline verification failed on Windows x64 — 1 check(s)
  • The baseline build crashed or failed tests under Nehalem (SSE4.2, no AVX/AVX2/AVX512) emulation (not an unsupported-instruction fault). See the step log for the crash output.

    • SIMD baseline tests

🧪   To try this PR locally:

bunx bun-pr 46884

That installs a local version of the PR into your bun-46884 executable, so you can run:

bun-46884 --bun

@robobun

robobun commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. The review findings are answered in their threads, and b46ac22 has the changes.

How it was reproduced

  • A mock registry (Bun.serve) answers the first PUT with 401, an authUrl and a doneUrl.
  • On a terminal (Bun.spawn({ terminal })), the done url returns the token at once and a postpublish script asks a question. first and second are typed. On main the script gets second.
  • With a piped stdin of open the page\nfor the script\n and a login that has to wait once, postpublish reads 0 bytes on main.
  • The cases are in test/cli/install/bun-publish.test.ts. A release build of main (179664e) fails 5 of the 17 new cases.

PR: #46884

@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between e20ebce and b46ac22.

Files selected for processing: 2
  • src/runtime/cli/publish_command.rs
  • test/cli/install/bun-publish.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

Walkthrough

The publish CLI now watches for ENTER during OTP web login and opens eligible auth URLs without a persistent stdin-reader thread. OTP polling uses retry deadlines. Tests cover terminal and piped input, login completion, retry timing, and subsequent prompts.

Changes

Publish OTP web-login input handling

Layer / File(s) Summary
Input offer and prompt handling
src/runtime/cli/publish_command.rs, test/cli/install/bun-publish.test.ts
Unix input polling offers ENTER-to-open behavior when the auth URL is safe to display; non-Unix platforms do not offer it. Tests cover input during and after login, later prompts, output redirection, and recursive publishing.
Retry timing and login completion
src/runtime/cli/publish_command.rs, test/cli/install/bun-publish.test.ts
HTTP 202 responses use a retry deadline and wait only for the remaining interval. Tests cover ENTER during a retry wait and verify supplied or default retry timing.

Suggested reviewers: jarred-sumner

Priority: Normal

Merge Risk: Low · up to b46ac

Publishing remains mergeable, but a fragmented piped answer can be truncated before it reaches a later prompt.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: waiting for ENTER during the web login retry interval.
Description check Passed The description clearly explains the problem, implementation, trade-offs, platform behavior, and verification results. It does not use the template headings exactly, but it provides the required infor…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description clearly explains the problem, implementation, trade-offs, platform behavior, and verification results. It does not use the template headings exactly, but it provides the required information in equivalent sections and is substantially complete.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR

@claude claude Bot left a comment

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.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread src/runtime/cli/publish_command.rs
Comment thread src/runtime/cli/publish_command.rs
Comment thread src/runtime/cli/publish_command.rs
Comment thread src/runtime/cli/publish_command.rs
Comment thread src/runtime/cli/publish_command.rs Outdated
Comment thread src/runtime/cli/publish_command.rs
Comment thread src/runtime/cli/publish_command.rs Outdated
Comment thread src/runtime/cli/publish_command.rs
Comment thread test/cli/install/bun-publish.test.ts Outdated
Comment thread test/cli/install/bun-publish.test.ts
The deadline of the retry wait stopped the read of a piped stdin after
one byte when the wait was zero, so a line was taken in parts and the
next reader got the rest of it. The deadline is now only for input that
is not there yet.

A wait ends the offer after 1024 bytes without a line end, so a stdin
such as /dev/zero cannot hold up the login.

The line that is typed during the last request is taken on every
terminal stdin, and not only on one that could be opened a second time.

When the thread for the opener cannot start, that is reported.

@claude claude Bot left a comment

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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Re-checked after b46ac22: the second commit does settle the earlier points on the one-byte read under retry-after: 0, the dropped thread-spawn error, the /dev/zero spin (now bounded at 1024 bytes), and the late ENTER on a terminal whose reopen failed. The gate at EnterOffer::new (:152) and the blocking read of fd 0 in the Stdin arm (:194) are unchanged from the first push, so those threads stay open as they are; beyond the two new inline nits I have nothing further to add, and a human look at the tty/poll handling across macOS is still worthwhile.

Extended reasoning...

The PR reworks the "press ENTER to open in browser" offer of bun publish web logins in src/runtime/cli/publish_command.rs, replacing a detached blocking stdin reader thread and a global with a poll-based wait on the main thread, and adds PTY-based tests in test/cli/install/bun-publish.test.ts. It touches auth-flow UX (web login token retrieval) but no credential handling itself. The second commit resolves four of the earlier inline threads as verified from the code, while the poll gate and the blocking fd 0 read are unchanged; the remaining complexity is platform-specific tty/poll behavior that did not run on macOS or Windows, so a human review is still warranted.

Comment on lines +187 to +204
while taken < line.len() {
let left = deadline.saturating_duration_since(Instant::now());
match self.readable_within(left) {
Some(true) => {}
Some(false) => return Waited::Nothing,
None => return Waited::EndOfInput,
}
match bun_sys::read(self.fd(), &mut line[..block]) {
Ok(0) => return Waited::EndOfInput,
Ok(len) if strings::contains_char(&line[..len], b'\n') => return Waited::Enter,
Ok(len) => taken += len,
// Another reader of the terminal took the line first.
Err(err) if err.is_retry() => return Waited::Nothing,
Err(_) => return Waited::EndOfInput,
}
}
// This many bytes without a line end are not ENTER, and /dev/zero never has one.
Waited::EndOfInput

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.

🟡 nit (optional): a user who pastes more than 1024 characters before pressing ENTER on a terminal gets no page, and the tail of that line reaches the next prompt. In wait the Terminal arm reads 1024-byte blocks and the loop at src/runtime/cli/publish_command.rs:187 stops once taken reaches 1024 and returns Waited::EndOfInput, leaving the rest of the line and its \n in the tty queue. Fix: for a terminal keep reading until the line end or EAGAIN (the line discipline already caps a line at 4095 bytes), or size the buffer to that cap, so the whole line is consumed before the offer ends; the 1024 cap can stay for the pipe arm.

Why this was flagged

On a Linux or macOS terminal in canonical mode, the user pastes a long string (over 1024 bytes, e.g. a copied URL with query text) and presses ENTER during a 202 retry wait, so get_otp calls EnterOffer::pressed_before at src/runtime/cli/publish_command.rs:1115. wait at :178 uses block = 1024 for Self::Terminal (:182); the first read at :194 returns 1024 bytes with no newline, taken becomes 1024 (:197), the while condition at :187 fails and :204 returns Waited::EndOfInput. pressed_before then sets the offer to None (:214) and returns false, so open_in_browser is never called and the remaining bytes plus the \n stay in the tty input queue. The next reader of fd 0 (a postpublish prompt, or the Enter OTP: prompt under --recursive at :1163) receives that tail as its answer. On the base branch press_enter_to_open_in_browser looped on read(0) with a 1024-byte buffer until a chunk contained \n, so the whole line was consumed and the page opened. The comment at :203 says this bound is for /dev/zero, which only applies to the Stdin arm.

Verification: nit — triggered when stdin is a canonical-mode terminal and the user pastes a line longer than 1024 bytes before pressing ENTER during a 202 retry wait. In src/runtime/cli/publish_command.rs the read at :194 returns 1024 bytes with no \n, the while taken < line.len() loop (:187) exits and :204 returns Waited::EndOfInput. On the base branch the thread kept reading until it saw \n, then opened the page.

Comment on lines +63 to +65
function untilWaitingToRetry(pid: number) {
const waits = process.arch === "arm64" ? /^(73|101|115) / : /^(7|35|230|271) /;
return until(() => waits.test(readFileSync(`/proc/${pid}/task/${pid}/syscall`, "utf8")));

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.

🟡 nit (optional): on a Linux runner where the kernel refuses ptrace-style access to a child (kernel.yama.ptrace_scope=2, a hardened seccomp or LSM profile), the six untilWaitingToRetry tests fail with EPERM instead of testing anything. readFileSync of /proc/<pid>/task/<pid>/syscall at test/cli/install/bun-publish.test.ts:65 needs PTRACE_MODE_ATTACH on bun, and the throw escapes until straight into the test. Fix: catch the read error and fall back to a signal that needs no ptrace access (for example poll the 202 request count on the mock, or skipIf the first read throws with a stated reason) so the login tests still assert the ENTER wait on such runners.

Why this was flagged

Trigger: the Linux CI or developer machine sets kernel.yama.ptrace_scope=2 or runs the suite under a container profile that denies PTRACE_MODE_ATTACH to child processes. untilWaitingToRetry at test/cli/install/bun-publish.test.ts:63-66 calls readFileSync(/proc/${pid}/task/${pid}/syscall) on every 1 ms tick; the kernel's lock_trace check returns EPERM, readFileSync throws inside the until callback at :54-56, and the promise rejects. The six tests that await it ("ENTER pressed while bun waits ..." at :676 and the five "another reader of the terminal" runs at :769) fail with an EPERM error rather than a statement about bun publish. On the base branch these tests do not exist, so nothing in the suite depends on /proc ptrace access; after the merge the Linux lane of bun-publish.test.ts is red on such a runner regardless of whether the ENTER wait works. No skipIf or try/catch guards the read.

Verification: nit. Trigger: a Linux host where the kernel denies PTRACE_MODE_ATTACH to the test process for its child. test/cli/install/bun-publish.test.ts:65 readFileSync of /proc/${pid}/task/${pid}/syscall throws EPERM. until at :55-57 has no try/catch, so the rejection propagates to the awaiting test. Callers :690 and :790 are gated only by skipIf(!isLinux). Base branch has no such dependency.

This branch has not been deployed

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant