Skip to content

terminal: Close the terminal if EOF is detected#39082

Closed
lemorage wants to merge 14 commits intozed-industries:mainfrom
lemorage:fixes
Closed

terminal: Close the terminal if EOF is detected#39082
lemorage wants to merge 14 commits intozed-industries:mainfrom
lemorage:fixes

Conversation

@lemorage
Copy link
Copy Markdown
Contributor

@lemorage lemorage commented Sep 29, 2025

Closes #38901

Release Notes:

Implementation Details:

  • Interactive shells now only close when user has sent keyboard input
  • Spawn failures (which never receive keyboard input) keep the terminal open to show error messages
  • User-initiated exits via Ctrl+D or typing "exit" close the terminal as expected
  • Tasks/commands (Shell::WithArguments) continue to close based on exit code (0 = success)

@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Sep 29, 2025
@SomeoneToIgnore
Copy link
Copy Markdown
Contributor

Passing by and noting that this change is a bit odd and things were working in Linux for me before.
Could be that recent Alacritty bump could have caused this, and we need to understand what had changed and was this a regression or a new configuration is needed?

@lemorage
Copy link
Copy Markdown
Contributor Author

Umm, possible. I never tested the before version in linux. I can grab some time see if any Alacritty related issue resulting in this.

@Veykril
Copy link
Copy Markdown
Member

Veykril commented Sep 29, 2025

I'd like for us to see if we can make a simple test for this behavior as it feels like its easy for us to break things here (as is evident by this PR being raised now). Would you mind having a brief look at whether this is easily testable? If not I'll try to commit a bit of time into looking into that myself this week.

@lemorage
Copy link
Copy Markdown
Contributor Author

Sure. Np!

@SomeoneToIgnore
Copy link
Copy Markdown
Contributor

#39232 adds a test that fails on Linux, to help with fixing the issue.

@lemorage
Copy link
Copy Markdown
Contributor Author

lemorage commented Oct 1, 2025

Thanks! I'll see!

/* Close only if success or no exit code */
self.child_exited.is_none_or(|e| e.code() == Some(0))
} else {
/* Always close the interactive shell */
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this will cause us to close the shell again if we fail to spawn it for whatever reason. Now, that shouldn't really be happening but an example would be a $SHELL being misconfigured and us using Shell::System, which we then again don't surface to the user because of the immediate closing of the shell

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.

Valid concern. But tbh, a fundamental problem is that we cannot distinguish between:

  • PTY Exit from spawn failure
  • PTY Exit from user pressing Ctrl+D
    So I find it hard to have the best of both worlds. Do u have any insights on this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hmm, that is problematic. Is there really no way for us to differentiate spawn failure from a EOF signal here? ...

Comment on lines +2113 to +2114
let is_interactive_shell =
!matches!(self.template.shell, Shell::WithArguments { .. });
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doesn't work. Either of the three variants could be an interactive shell. A better approach i think would be to check if self.task.is_none(), then we are definitely a shell session and not a task

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.

Good idea! I'll see.

Comment on lines +2508 to +2509
// Give shell time to process SIGINT
smol::Timer::after(Duration::from_millis(50)).await;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems a bit odd, we have the event receiving logic from before expecting a wakeup after all. We should keep that instead, if the events differ on platforms we might want to make this a bit more flexible, but waiting for a given duration makes the test flaky

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.

I remembered this test had some issues on Linux. I'll check this later on my Linux machine.

Some(task) => task,
None => {
if self.child_exited.is_none_or(|e| e.code() == Some(0)) {
// Always record the latest exit status.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we add a comment to this entire block describing the intent of this PR? Otherwise this logic looks arbitrary to readers and we might regress unintentionally

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.

Sure thing!

@lemorage
Copy link
Copy Markdown
Contributor Author

After Ctrl + C and Ctrl + D, the terminal exits smoothly on Linux. This is verified. But in the test_terminal_eof, whatever approaches I try, it always fails. It works only after a short interval here, say smol::Timer::after(Duration::from_millis(50)).await;. So I still keep it there in the test. Feel free to tell if you have any insights.

Copy link
Copy Markdown
Member

@Veykril Veykril left a comment

Choose a reason for hiding this comment

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

Sorry for the delay, turns out I never submitted this review

let wakeup = event_rx.recv().await.expect("No wakeup event received");
assert_eq!(wakeup, first_event, "Expected wakeup, got {wakeup:?}");
#[cfg(target_os = "linux")]
let first_event = Event::BreadcrumbsChanged;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Given this is weirdly the first event on linux, could you check if the timing issue disappears if we make it poll events until the first wakeup event comes in? I unfortunately do not have a linux machine set up at the moment so I can't test this myself.

@lemorage
Copy link
Copy Markdown
Contributor Author

I did some time of testing. So if we poll events until the first wakeup event comes in, the test still fails on Linux unless we adds the following:

while let Ok(Ok(_first_event)) = smol_timeout(Duration::from_millis(100), event_rx.recv()).await {}

But this is only a remedy on Linux end, which will in turn cause tests failing on MacOS. So whatever a special case #[cfg(target_os = "linux")] is unavoidable, per my cents. Or I would like to hear if there is anything I am missing.

@lemorage lemorage force-pushed the fixes branch 2 times, most recently from cebcf05 to a2e3281 Compare December 7, 2025 15:05
@Veykril
Copy link
Copy Markdown
Member

Veykril commented Jan 23, 2026

Sorry for the silence on my part, @smitbarmase will take this over and have a look as they have a linux machine to test this on

@Veykril Veykril removed their assignment Jan 23, 2026
@lemorage
Copy link
Copy Markdown
Contributor Author

@Veykril Thanks! No worries!

smitbarmase

This comment was marked as outdated.

@smitbarmase
Copy link
Copy Markdown
Member

Hey @lemorage, any findings after rebase?

@lemorage
Copy link
Copy Markdown
Contributor Author

@smitbarmase Could u make the CI tests running? The tests work properly on my Mac, but I am not sure if they are all passed across platforms.

Copy link
Copy Markdown
Member

@smitbarmase smitbarmase left a comment

Choose a reason for hiding this comment

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

Hey, thanks for this. Looking at the code, it seems like a bunch of cases don’t really make sense. Also, from testing, it doesn’t do what it claims to do, since the keyboard_input_sent heuristic is unreachable.

I’m sure this may have drifted during a rebase or changed further on top of the original patch. I’m going to close this for now, but feel free to take another shot at it. I think we should have tests covering every branch this might introduce.

After checking test_terminal_eof more, it is indeed flaky on Linux because we call Ctrl-C and Ctrl-D right after each other. Sometimes Ctrl-C causes consuming/flushing behavior, which makes the test behave inconsistently. This needs special attention to fix, and I think it’s a separate issue from this patch.

A better way to test the behavior we want would be to run false, and then exit. That also blocks the terminal from closing, and it reproduces on both macOS and Linux with bash.

None => {
if self.child_exited.is_none_or(|e| e.code() == Some(0)) {
// Always record the latest exit status.
if let Some(e) = exit_status {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We already do this on line 2241 right? Why do we need this again right after it?

//
// For tasks/commands, we close on successful exit (exit code 0).
let should_close = if raw_status.is_none() {
if self.task.is_none() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We already know we are in task is None case, so this is always true?

// receive keyboard input, while user exits always require it.
//
// For tasks/commands, we close on successful exit (exit code 0).
let should_close = if raw_status.is_none() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also, raw_status is never None (if you check the call sites) so everything inside it is just a dead code.

@github-project-automation github-project-automation bot moved this from Community PRs to Done in Quality Week – December 2025 Mar 26, 2026
Veykril pushed a commit that referenced this pull request Mar 27, 2026
Closes #38901

Supersedes #39082

This PR fixes an issue where the terminal tab would stay open after the
user exits a shell that has a non-zero exit code (e.g. running `false`
then `exit`, or pressing Ctrl-C followed by Ctrl-D). We now track
whether any keyboard input was sent to distinguish user-initiated exits
from shell spawn failures.

Release Notes:

- Fixed terminal tab not closing when the shell exits with a non-zero
code.

Co-authored-by: Glenn Miao <one.lemorage@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement

Projects

Development

Successfully merging this pull request may close these issues.

Terminal freezes in Linux session when Ctrl+C is pressed before exit

5 participants