fix(stability+terminal): stdout EPIPE → SIGABRT + zsh exec-flag-/ rerun (v0.7.42)
All checks were successful
boundary-lint / PR boundary-claim (Lint (push) Has been skipped
boundary-lint / duplication-deadline (Layer 1/2) (push) Successful in 18s
ci / test-health gate (push) Successful in 16s
ci / mutation test (broker) (push) Has been skipped
boundary-lint / ban-list lint (Lint (push) Successful in 19s
Release Publish (Gitea session_helper) / verify-release-tag (push) Successful in 17s
ci / rust debug (push) Successful in 2m17s
ci / rust release (push) Successful in 2m24s
ci / python (push) Successful in 1m37s
Release Publish (Gitea session_helper) / publish-linux-x86_64 (push) Successful in 3m42s

Two follow-ups to today's user-visible regressions.

1. local_bridge stdout EPIPE → SIGABRT
--------------------------------------

User crash report (local_bridge-2026-05-03-230610.ips) traced to
``main.rs:71`` → ``std::io::stdio::_print`` → ``panic_fmt`` →
``rust_panic`` → ``abort``. v0.7.39 (b44f708) hardened the three
``eprintln!`` sites against EPIPE-induced ``panic = "abort"`` aborts
but missed the matching ``println!`` sites at main.rs:52 (``--version``
banner) and main.rs:71 (one-shot JSON output). The latter is the path
exercised every time the Python ctypes parent dies first and the
bridge subprocess inherits a broken stdout pipe — reproducing the
phantom ``DiagnosticReport`` the v0.7.39 commit was supposed to
eliminate. Replaced both with ``let _ = writeln!(std::io::stdout(),
...)`` for parity with the stderr fix.

2. Open Remote Terminal: ``zsh:1: unknown exec flag -/`` again
--------------------------------------------------------------

v0.7.31 (b2f9334) dropped the ``</dev/tty`` redirect prefix that broke
``zsh: bad option: -/``. The remaining ``${SHELL:-/bin/sh}`` default
form re-tripped the same class of zsh setups in v0.7.31+ —
``${...:-/bin/sh}`` parameter expansion split such that the literal
``-/bin/sh`` reached ``exec`` as a flag. Quoting ``"\$SHELL"`` and
dropping the fallback is enough: sshd populates ``\$SHELL`` from the
user's passwd entry in every login session, so the ``:-`` default was
redundant.

Tests
-----

Python 1368 pass, Rust 486 pass. Did not add a stdout-EPIPE
regression test — same precedent as v0.7.39 (no stderr-EPIPE test
either): the timing-dependent reproduction is flaky enough to be
net-negative, and the pre-fix crash signature is a better future
regression detector than a brittle harness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 00:05:06 +09:00
parent 836d7e4a73
commit 5566e9ec16
7 changed files with 40 additions and 16 deletions

View File

@@ -1,6 +1,6 @@
[project]
name = "sessions-sublime"
version = "0.7.41"
version = "0.7.42"
description = "Sublime-facing Python code for Sessions."
requires-python = ">=3.8"
license = {text = "MIT"}

12
rust/Cargo.lock generated
View File

@@ -221,7 +221,7 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "local_bridge"
version = "0.7.41"
version = "0.7.42"
dependencies = [
"base64",
"glob",
@@ -432,7 +432,7 @@ dependencies = [
[[package]]
name = "session_helper"
version = "0.7.41"
version = "0.7.42"
dependencies = [
"base64",
"notify",
@@ -443,7 +443,7 @@ dependencies = [
[[package]]
name = "session_protocol"
version = "0.7.41"
version = "0.7.42"
dependencies = [
"base64",
"serde",
@@ -452,14 +452,14 @@ dependencies = [
[[package]]
name = "sessions_askpass"
version = "0.7.41"
version = "0.7.42"
dependencies = [
"tempfile",
]
[[package]]
name = "sessions_native"
version = "0.7.41"
version = "0.7.42"
dependencies = [
"base64",
"notify",
@@ -773,7 +773,7 @@ dependencies = [
[[package]]
name = "workspace_identity"
version = "0.7.41"
version = "0.7.42"
[[package]]
name = "zmij"

View File

@@ -12,7 +12,7 @@ resolver = "2"
[workspace.package]
edition = "2024"
license = "MIT"
version = "0.7.41"
version = "0.7.42"
authors = ["Myeongseon Choi <key262yek@gmail.com>"]
repository = "https://git.teahaven.kr/sublime-rs/sessions"
homepage = "https://git.teahaven.kr/sublime-rs/sessions"

View File

@@ -49,7 +49,12 @@ fn main() {
.map(String::as_str)
.is_some_and(|first| matches!(first, "--version" | "-V" | "version"))
{
println!("{LOCAL_BRIDGE_VERSION_BANNER}");
// Use ``writeln!`` + ``let _`` so EPIPE silently fails through to
// ``return`` instead of panicking → SIGABRT under
// ``panic = "abort"``. Same rationale as the eprintln sites
// hardened in v0.7.39 (b44f708): a parent that closes its end of
// our stdout before we write must not generate a phantom crash.
let _ = writeln!(std::io::stdout(), "{LOCAL_BRIDGE_VERSION_BANNER}");
return;
}
if args.first().map(String::as_str) == Some("lsp-stdio") {
@@ -68,7 +73,16 @@ fn main() {
match run(&args) {
Ok(output) => match serde_json::to_string(&output) {
Ok(encoded) => {
println!("{encoded}");
// ``writeln!`` + ``let _`` here for the same reason as the
// eprintln sites hardened in v0.7.39: when Sublime / the
// Python ctypes parent dies first the bridge inherits a
// broken stdout pipe, and a bare ``println!`` panics on
// EPIPE → SIGABRT under ``panic = "abort"``. The earlier
// pass only covered stderr; this is the missed stdout
// site that produced the
// ``local_bridge::main hf88e153b048e40f5 main.rs:71``
// abort signature in user crash reports.
let _ = writeln!(std::io::stdout(), "{encoded}");
}
Err(error) => {
let _ = writeln!(

View File

@@ -6952,9 +6952,16 @@ class SessionsOpenRemoteTerminalCommand(sublime_plugin.WindowCommand):
# out to break interactive zsh on some macOS → Linux setups
# ("zsh: bad option: -/") — dropped in v0.7.30.5. ``ssh -t``
# already allocates a pty so the redirections were redundant.
remote_invocation = "cd {}; exec ${{SHELL:-/bin/sh}} -il".format(
shlex.quote(remote_root)
)
#
# ``"$SHELL"`` not ``${SHELL:-/bin/sh}``: the ``:-`` fallback
# form re-tripped some zsh setups in v0.7.31+ with
# ``zsh:1: unknown exec flag -/`` — the parameter expansion
# split such that the literal ``-/bin/sh`` reached ``exec`` as
# a flag instead of expanding to ``/bin/sh``. sshd populates
# ``$SHELL`` from the user's passwd entry in every login
# session, so the fallback was redundant; quoting ``"$SHELL"``
# also handles the rare path-with-spaces case.
remote_invocation = 'cd {}; exec "$SHELL" -il'.format(shlex.quote(remote_root))
# ``panel_name`` makes Terminus open the shell as a panel
# docked at the bottom of the active window. Without it
# Terminus defaults to a new tab in the editor pane group,

View File

@@ -450,12 +450,15 @@ def test_open_remote_terminal_opens_transient_terminus_pane(
# handshake is racy. ``;`` not ``&&`` so a failed ``cd`` doesn't
# take the shell down with it. The earlier ``</dev/tty`` redirect
# prefix was dropped — it confused interactive zsh on some macOS →
# Linux setups (``zsh: bad option: -/``).
# Linux setups (``zsh: bad option: -/``). The ``${SHELL:-/bin/sh}``
# default form re-tripped the same class of zsh setups in v0.7.31+
# (``zsh:1: unknown exec flag -/``); ``"$SHELL"`` quoted is enough
# because sshd populates ``$SHELL`` from passwd in every login.
assert args["cmd"] == [
"ssh",
"-t",
"prod",
"cd /srv/app; exec ${SHELL:-/bin/sh} -il",
'cd /srv/app; exec "$SHELL" -il',
]
# ``auto_close=False`` so an unexpected shell exit (dotfile error,
# missing remote root, SSH drop) keeps the pane visible long enough

2
uv.lock generated
View File

@@ -854,7 +854,7 @@ wheels = [
[[package]]
name = "sessions-sublime"
version = "0.7.41"
version = "0.7.42"
source = { virtual = "." }
[package.dev-dependencies]