github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/tty.go (about) 1 package main 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "os" 8 "os/signal" 9 "sync" 10 11 "github.com/containerd/console" 12 "github.com/opencontainers/runc/libcontainer" 13 "github.com/opencontainers/runc/libcontainer/utils" 14 ) 15 16 type tty struct { 17 epoller *console.Epoller 18 console *console.EpollConsole 19 hostConsole console.Console 20 closers []io.Closer 21 postStart []io.Closer 22 wg sync.WaitGroup 23 consoleC chan error 24 } 25 26 func (t *tty) copyIO(w io.Writer, r io.ReadCloser) { 27 defer t.wg.Done() 28 _, _ = io.Copy(w, r) 29 _ = r.Close() 30 } 31 32 // setup pipes for the process so that advanced features like c/r are able to easily checkpoint 33 // and restore the process's IO without depending on a host specific path or device 34 func setupProcessPipes(p *libcontainer.Process, rootuid, rootgid int) (*tty, error) { 35 i, err := p.InitializeIO(rootuid, rootgid) 36 if err != nil { 37 return nil, err 38 } 39 t := &tty{ 40 closers: []io.Closer{ 41 i.Stdin, 42 i.Stdout, 43 i.Stderr, 44 }, 45 } 46 // add the process's io to the post start closers if they support close 47 for _, cc := range []interface{}{ 48 p.Stdin, 49 p.Stdout, 50 p.Stderr, 51 } { 52 if c, ok := cc.(io.Closer); ok { 53 t.postStart = append(t.postStart, c) 54 } 55 } 56 go func() { 57 _, _ = io.Copy(i.Stdin, os.Stdin) 58 _ = i.Stdin.Close() 59 }() 60 t.wg.Add(2) 61 go t.copyIO(os.Stdout, i.Stdout) 62 go t.copyIO(os.Stderr, i.Stderr) 63 return t, nil 64 } 65 66 func inheritStdio(process *libcontainer.Process) { 67 process.Stdin = os.Stdin 68 process.Stdout = os.Stdout 69 process.Stderr = os.Stderr 70 } 71 72 func (t *tty) initHostConsole() error { 73 // Usually all three (stdin, stdout, and stderr) streams are open to 74 // the terminal, but they might be redirected, so try them all. 75 for _, s := range []*os.File{os.Stderr, os.Stdout, os.Stdin} { 76 c, err := console.ConsoleFromFile(s) 77 if err == nil { 78 t.hostConsole = c 79 return nil 80 } 81 if errors.Is(err, console.ErrNotAConsole) { 82 continue 83 } 84 // should not happen 85 return fmt.Errorf("unable to get console: %w", err) 86 } 87 // If all streams are redirected, but we still have a controlling 88 // terminal, it can be obtained by opening /dev/tty. 89 tty, err := os.Open("/dev/tty") 90 if err != nil { 91 return err 92 } 93 c, err := console.ConsoleFromFile(tty) 94 if err != nil { 95 return fmt.Errorf("unable to get console: %w", err) 96 } 97 98 t.hostConsole = c 99 return nil 100 } 101 102 func (t *tty) recvtty(socket *os.File) (Err error) { 103 f, err := utils.RecvFile(socket) 104 if err != nil { 105 return err 106 } 107 cons, err := console.ConsoleFromFile(f) 108 if err != nil { 109 return err 110 } 111 err = console.ClearONLCR(cons.Fd()) 112 if err != nil { 113 return err 114 } 115 epoller, err := console.NewEpoller() 116 if err != nil { 117 return err 118 } 119 epollConsole, err := epoller.Add(cons) 120 if err != nil { 121 return err 122 } 123 defer func() { 124 if Err != nil { 125 _ = epollConsole.Close() 126 } 127 }() 128 go func() { _ = epoller.Wait() }() 129 go func() { _, _ = io.Copy(epollConsole, os.Stdin) }() 130 t.wg.Add(1) 131 go t.copyIO(os.Stdout, epollConsole) 132 133 // Set raw mode for the controlling terminal. 134 if err := t.hostConsole.SetRaw(); err != nil { 135 return fmt.Errorf("failed to set the terminal from the stdin: %w", err) 136 } 137 go handleInterrupt(t.hostConsole) 138 139 t.epoller = epoller 140 t.console = epollConsole 141 t.closers = []io.Closer{epollConsole} 142 return nil 143 } 144 145 func handleInterrupt(c console.Console) { 146 sigchan := make(chan os.Signal, 1) 147 signal.Notify(sigchan, os.Interrupt) 148 <-sigchan 149 _ = c.Reset() 150 os.Exit(0) 151 } 152 153 func (t *tty) waitConsole() error { 154 if t.consoleC != nil { 155 return <-t.consoleC 156 } 157 return nil 158 } 159 160 // ClosePostStart closes any fds that are provided to the container and dup2'd 161 // so that we no longer have copy in our process. 162 func (t *tty) ClosePostStart() { 163 for _, c := range t.postStart { 164 _ = c.Close() 165 } 166 } 167 168 // Close closes all open fds for the tty and/or restores the original 169 // stdin state to what it was prior to the container execution 170 func (t *tty) Close() { 171 // ensure that our side of the fds are always closed 172 for _, c := range t.postStart { 173 _ = c.Close() 174 } 175 // the process is gone at this point, shutting down the console if we have 176 // one and wait for all IO to be finished 177 if t.console != nil && t.epoller != nil { 178 _ = t.console.Shutdown(t.epoller.CloseConsole) 179 } 180 t.wg.Wait() 181 for _, c := range t.closers { 182 _ = c.Close() 183 } 184 if t.hostConsole != nil { 185 _ = t.hostConsole.Reset() 186 } 187 } 188 189 func (t *tty) resize() error { 190 if t.console == nil || t.hostConsole == nil { 191 return nil 192 } 193 return t.console.ResizeFrom(t.hostConsole) 194 }