diff --git a/src/builtin.cpp b/src/builtin.cpp index 9f7afd1f9..faf4af24d 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -500,7 +500,15 @@ int builtin_run(parser_t &parser, wchar_t **argv, io_streams_t &streams) { } if (const builtin_data_t *data = builtin_lookup(argv[0])) { - return data->func(parser, streams, argv); + // If we are interactive, save the foreground pgroup and restore it after in case the + // builtin needs to read from the terminal. See #4540. + bool grab_tty = is_interactive_session && isatty(streams.stdin_fd); + pid_t pgroup_to_restore = grab_tty ? terminal_acquire_before_builtin() : -1; + int ret = data->func(parser, streams, argv); + if (pgroup_to_restore >= 0) { + tcsetpgrp(STDIN_FILENO, pgroup_to_restore); + } + return ret; } debug(0, UNKNOWN_BUILTIN_ERR_MSG, argv[0]); diff --git a/src/proc.cpp b/src/proc.cpp index 4b40d21da..e23e389a0 100644 --- a/src/proc.cpp +++ b/src/proc.cpp @@ -870,6 +870,17 @@ bool terminal_give_to_job(const job_t *j, bool cont) { return true; } +pid_t terminal_acquire_before_builtin() { + pid_t selfpid = getpid(); + pid_t current_owner = tcgetpgrp(STDIN_FILENO); + if (current_owner >= 0 && current_owner != selfpid) { + if (tcsetpgrp(STDIN_FILENO, selfpid) == 0) { + return current_owner; + } + } + return -1; +} + /// Returns control of the terminal to the shell, and saves the terminal attribute state to the job, /// so that we can restore the terminal ownership to the job at a later time. static bool terminal_return_from_job(job_t *j) { diff --git a/src/proc.h b/src/proc.h index 66dc3addc..57b2e8526 100644 --- a/src/proc.h +++ b/src/proc.h @@ -373,3 +373,7 @@ pid_t proc_wait_any(); #endif bool terminal_give_to_job(const job_t *j, bool cont); + +/// Given that we are about to run a builtin, acquire the terminal if we do not own it. Returns the +/// pid to restore after running the builtin, or -1 if there is no pid to restore. +pid_t terminal_acquire_before_builtin(); diff --git a/tests/read.expect b/tests/read.expect index 9325c3215..6f9caac63 100644 --- a/tests/read.expect +++ b/tests/read.expect @@ -90,6 +90,14 @@ expect_prompt expect_marker 7 print_var_contents foo +# Verify we don't hang on `read | cat`. See #4540. +send_line "read | cat" +expect_read_prompt +send_line -h "bar\r_marker 4540" +expect_prompt +expect_marker 4540 + + # ========== # The fix for issue #2007 initially introduced a problem when using a function # to read from /dev/stdin when that is associated with the tty. These tests