| 13 | ) |
| 14 | |
| 15 | func startPty(cmdPty *Cmd, opt ...StartOption) (retPTY *otherPty, proc Process, err error) { |
| 16 | var opts startOptions |
| 17 | for _, o := range opt { |
| 18 | o(&opts) |
| 19 | } |
| 20 | |
| 21 | opty, err := newPty(opts.ptyOpts...) |
| 22 | if err != nil { |
| 23 | return nil, nil, xerrors.Errorf("newPty failed: %w", err) |
| 24 | } |
| 25 | |
| 26 | origEnv := cmdPty.Env |
| 27 | if opty.opts.sshReq != nil { |
| 28 | cmdPty.Env = append(cmdPty.Env, fmt.Sprintf("SSH_TTY=%s", opty.Name())) |
| 29 | } |
| 30 | if opty.opts.setGPGTTY { |
| 31 | cmdPty.Env = append(cmdPty.Env, fmt.Sprintf("GPG_TTY=%s", opty.Name())) |
| 32 | } |
| 33 | if cmdPty.Context == nil { |
| 34 | cmdPty.Context = context.Background() |
| 35 | } |
| 36 | cmdExec := cmdPty.AsExec() |
| 37 | |
| 38 | cmdExec.SysProcAttr = &syscall.SysProcAttr{ |
| 39 | Setsid: true, |
| 40 | Setctty: true, |
| 41 | } |
| 42 | cmdExec.Stdout = opty.tty |
| 43 | cmdExec.Stderr = opty.tty |
| 44 | cmdExec.Stdin = opty.tty |
| 45 | err = cmdExec.Start() |
| 46 | if err != nil { |
| 47 | _ = opty.Close() |
| 48 | if runtime.GOOS == "darwin" && strings.Contains(err.Error(), "bad file descriptor") { |
| 49 | // macOS has an obscure issue where the PTY occasionally closes |
| 50 | // before it's used. It's unknown why this is, but creating a new |
| 51 | // TTY resolves it. |
| 52 | cmdPty.Env = origEnv |
| 53 | return startPty(cmdPty, opt...) |
| 54 | } |
| 55 | return nil, nil, xerrors.Errorf("start: %w", err) |
| 56 | } |
| 57 | if runtime.GOOS == "linux" { |
| 58 | // Now that we've started the command, and passed the TTY to it, close |
| 59 | // our file so that the other process has the only open file to the TTY. |
| 60 | // Once the process closes the TTY (usually on exit), there will be no |
| 61 | // open references and the OS kernel returns an error when trying to |
| 62 | // read or write to our PTY end. Without this (on Linux), reading from |
| 63 | // the process output will block until we close our TTY. |
| 64 | // |
| 65 | // Note that on darwin, reads on the PTY don't block even if we keep the |
| 66 | // TTY file open, and keeping it open seems to prevent race conditions |
| 67 | // where we lose output. Couldn't find official documentation |
| 68 | // confirming this, but I did find a thread of someone else's |
| 69 | // observations: https://developer.apple.com/forums/thread/663632 |
| 70 | if err := opty.tty.Close(); err != nil { |
| 71 | _ = cmdExec.Process.Kill() |
| 72 | return nil, nil, xerrors.Errorf("close tty: %w", err) |