I am writing a Go wrapper that does various things and then executes an interactive bash shell. I would like to usurp FD2 of this child process with my own thing that is fed in from the Go wrapper. That part I can do fine. I can even execute bash fine. The problem is that readline refuses to use FD1 for its stream so it ends up such that my thing on FD2 kind of 'eats' the readline output. Meaning, nothing I type comes up on the screen (no it is not terminal echo mode if that's what you're thinking) and the last line of my prompt (I have a complex two line PS1 for this) gets eaten. Clearly, it is readline. However, as I understand it, bash+readline should only ever use FD2 for its stream if stdout is detected to not be a tty (if that is incorrect, please inform me).
I am going to paste some basic code from the Go wrapper that illustrates what I am doing and this is a working example as it does not actually do the FD2 thing. This just executes bash, works great.
I've tried several different iterations on this basic theme to get my idea to work (usurping FD2). This one below, it is obvious that the process' stdout is not a TTY, yes. But even when I pass in the 'tty' directly on both stdin and stdout, same thing happens. I've also used a PTY (creack/pty package) and the same thing happens. I know the PTY is the way to do this but I am trying to keep the surface area of external dependencies as narrow as possible.
func executeScript(scriptPath string, cfg *config.Config) error {
msgSock, err := service_util.OpenSocket("messages")
if err != nil {
return err
}
defer msgSock.Close()
cmd := exec.Command(
"/usr/bin/env",
"bash",
"-c",
buildScriptCommand(scriptPath, cfg),
)
cmd.ExtraFiles = append(make([]*os.File, 31334), msgSock)
if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
defer tty.Close()
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Ctty: int(tty.Fd()),
Foreground: true,
}
}
if len(cfg.Args) > 2 {
cmd.Args = append(cmd.Args, cfg.Args...)
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
Works great until I either try to set cmd.Stderr to be the msgSock, or if I try to even collapse fd1 and 2 in a bash init file while moving a different FD into place as FD2. It all has the same behavior, readline outputs to FD2 so my thing on FD2 eats it. I just want readline to output on FD1/STDOUT.
The version with the tty passed in just changed up the tty variable initialization properly in-scope and passed tty to cmd.Stdin and Stdout.
This post would be 10 pages long if I showed you everything I've tried, some of those include:
Executing bash differently
If I execute bash with --noediting to disable readline it proves that readline is the culprit here, as what I type gets echo'd back. But I want readline to work for this application. I also tried non-interactive which has a similar effect. I've also stripped down the command to just be bash without the extra script stuff I am passing in. Basically no matter how I slice this pie, I'm still eating sh**.
PTY
As I mentioned I have also used PTY and yes I know direct tty access is bad, please don't lecture me on this point because as far as I can tell in this instance, it's irrelevant as readline's behavior is the same. If however, you know of some magic incantation with the creack/pty library (the de facto standard afaict for go) then by all means, i am listening!
Fed FD2 in directly
As mentioned before I have tried passing msgSock in directly to FD2. This is another reason I know readline itself is trying to stream to FD2- in my go code I can see the last line of the prompt and what i type coming in, which I have began printing back just to prove that.
Put msgSock on high FD then set it up in bash with exec
I put msgSock on FD 31337 then did:
exec 2>&1 2<&
same difference.
Took a shot in the dark with .inputrc settings
I even threw some longshot settings in inputrc trying to see if I could alter this behavior. I got nowhere with that, just like I thought I would.
I am writing a Go wrapper that does various things and then executes an interactive bash shell. I would like to usurp FD2 of this child process with my own thing that is fed in from the Go wrapper. That part I can do fine. I can even execute bash fine. The problem is that readline refuses to use FD1 for its stream so it ends up such that my thing on FD2 kind of 'eats' the readline output. Meaning, nothing I type comes up on the screen (no it is not terminal echo mode if that's what you're thinking) and the last line of my prompt (I have a complex two line PS1 for this) gets eaten. Clearly, it is readline. However, as I understand it, bash+readline should only ever use FD2 for its stream if stdout is detected to not be a tty (if that is incorrect, please inform me).
I am going to paste some basic code from the Go wrapper that illustrates what I am doing and this is a working example as it does not actually do the FD2 thing. This just executes bash, works great.
I've tried several different iterations on this basic theme to get my idea to work (usurping FD2). This one below, it is obvious that the process' stdout is not a TTY, yes. But even when I pass in the 'tty' directly on both stdin and stdout, same thing happens. I've also used a PTY (creack/pty package) and the same thing happens. I know the PTY is the way to do this but I am trying to keep the surface area of external dependencies as narrow as possible.
func executeScript(scriptPath string, cfg *config.Config) error {
msgSock, err := service_util.OpenSocket("messages")
if err != nil {
return err
}
defer msgSock.Close()
cmd := exec.Command(
"/usr/bin/env",
"bash",
"-c",
buildScriptCommand(scriptPath, cfg),
)
cmd.ExtraFiles = append(make([]*os.File, 31334), msgSock)
if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
defer tty.Close()
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Ctty: int(tty.Fd()),
Foreground: true,
}
}
if len(cfg.Args) > 2 {
cmd.Args = append(cmd.Args, cfg.Args...)
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
Works great until I either try to set cmd.Stderr to be the msgSock, or if I try to even collapse fd1 and 2 in a bash init file while moving a different FD into place as FD2. It all has the same behavior, readline outputs to FD2 so my thing on FD2 eats it. I just want readline to output on FD1/STDOUT.
The version with the tty passed in just changed up the tty variable initialization properly in-scope and passed tty to cmd.Stdin and Stdout.
This post would be 10 pages long if I showed you everything I've tried, some of those include:
Executing bash differently
If I execute bash with --noediting to disable readline it proves that readline is the culprit here, as what I type gets echo'd back. But I want readline to work for this application. I also tried non-interactive which has a similar effect. I've also stripped down the command to just be bash without the extra script stuff I am passing in. Basically no matter how I slice this pie, I'm still eating sh**.
PTY
As I mentioned I have also used PTY and yes I know direct tty access is bad, please don't lecture me on this point because as far as I can tell in this instance, it's irrelevant as readline's behavior is the same. If however, you know of some magic incantation with the creack/pty library (the de facto standard afaict for go) then by all means, i am listening!
Fed FD2 in directly
As mentioned before I have tried passing msgSock in directly to FD2. This is another reason I know readline itself is trying to stream to FD2- in my go code I can see the last line of the prompt and what i type coming in, which I have began printing back just to prove that.
Put msgSock on high FD then set it up in bash with exec
I put msgSock on FD 31337 then did:
exec 2>&1 2<&
same difference.
Took a shot in the dark with .inputrc settings
I even threw some longshot settings in inputrc trying to see if I could alter this behavior. I got nowhere with that, just like I thought I would.
1 Answer
Reset to default 2This is achievable with a one-line patch to bashline.c in bash's source code:
diff --git a/bashline.c b/bashline.c
index c85b05b6..e2289e6c 100644
--- a/bashline.c
+++ b/bashline.c
@@ -456,7 +456,7 @@ initialize_readline ()
rl_terminal_name = get_string_value ("TERM");
rl_instream = stdin;
- rl_outstream = stderr;
+ rl_outstream = stdout;
/* Allow conditional parsing of the ~/.inputrc file. */
rl_readline_name = "Bash";
of course with a few more lines this could be controllable by an environment variable. For now I am going to vendor this in my project and just make it part of my build. When I have time I will try to make something a little more robust and reach out to the maintainers to see about getting a toggle included for this.
NOTE- I have NO EARTHLY CLUE what the larger implications of doing this are!! Please do not take this as advice. I am doing something wacky that most people would never want to do.
os/exec
isn't built to make that easy -- poor API design, that; if you compare to, say, the Python subprocess module, the latter lets one setclose_fds=False
to pass anything opened without theO_CLOEXEC
flag through to the child)). – Charles Duffy Commented Jan 29 at 19:45ExtraFiles
in cs.opensource.google/go/go/+/refs/tags/go1.23.5:src/os/exec/… to let you specify file handles to pass in additional/nonstandard descriptors. – Charles Duffy Commented Jan 29 at 19:49