tty_unix.go (4724B)
1 // Copyright 2021 The TCell Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use file except in compliance with the License. 5 // You may obtain a copy of the license at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos 16 // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos 17 18 package tcell 19 20 import ( 21 "errors" 22 "fmt" 23 "os" 24 "os/signal" 25 "strconv" 26 "sync" 27 "syscall" 28 "time" 29 30 "golang.org/x/sys/unix" 31 "golang.org/x/term" 32 ) 33 34 // devTty is an implementation of the Tty API based upon /dev/tty. 35 type devTty struct { 36 fd int 37 f *os.File 38 of *os.File // the first open of /dev/tty 39 saved *term.State 40 sig chan os.Signal 41 cb func() 42 stopQ chan struct{} 43 dev string 44 wg sync.WaitGroup 45 l sync.Mutex 46 } 47 48 func (tty *devTty) Read(b []byte) (int, error) { 49 return tty.f.Read(b) 50 } 51 52 func (tty *devTty) Write(b []byte) (int, error) { 53 return tty.f.Write(b) 54 } 55 56 func (tty *devTty) Close() error { 57 return tty.f.Close() 58 } 59 60 func (tty *devTty) Start() error { 61 tty.l.Lock() 62 defer tty.l.Unlock() 63 64 // We open another copy of /dev/tty. This is a workaround for unusual behavior 65 // observed in macOS, apparently caused when a subshell (for example) closes our 66 // own tty device (when it exits for example). Getting a fresh new one seems to 67 // resolve the problem. (We believe this is a bug in the macOS tty driver that 68 // fails to account for dup() references to the same file before applying close() 69 // related behaviors to the tty.) We're also holding the original copy we opened 70 // since closing that might have deleterious effects as well. The upshot is that 71 // we will have up to two separate file handles open on /dev/tty. (Note that when 72 // using stdin/stdout instead of /dev/tty this problem is not observed.) 73 var err error 74 if tty.f, err = os.OpenFile(tty.dev, os.O_RDWR, 0); err != nil { 75 return err 76 } 77 78 if !term.IsTerminal(tty.fd) { 79 return errors.New("device is not a terminal") 80 } 81 82 _ = tty.f.SetReadDeadline(time.Time{}) 83 saved, err := term.MakeRaw(tty.fd) // also sets vMin and vTime 84 if err != nil { 85 return err 86 } 87 tty.saved = saved 88 89 tty.stopQ = make(chan struct{}) 90 tty.wg.Add(1) 91 go func(stopQ chan struct{}) { 92 defer tty.wg.Done() 93 for { 94 select { 95 case <-tty.sig: 96 tty.l.Lock() 97 cb := tty.cb 98 tty.l.Unlock() 99 if cb != nil { 100 cb() 101 } 102 case <-stopQ: 103 return 104 } 105 } 106 }(tty.stopQ) 107 108 signal.Notify(tty.sig, syscall.SIGWINCH) 109 return nil 110 } 111 112 func (tty *devTty) Drain() error { 113 _ = tty.f.SetReadDeadline(time.Now()) 114 if err := tcSetBufParams(tty.fd, 0, 0); err != nil { 115 return err 116 } 117 return nil 118 } 119 120 func (tty *devTty) Stop() error { 121 tty.l.Lock() 122 if err := term.Restore(tty.fd, tty.saved); err != nil { 123 tty.l.Unlock() 124 return err 125 } 126 _ = tty.f.SetReadDeadline(time.Now()) 127 128 signal.Stop(tty.sig) 129 close(tty.stopQ) 130 tty.l.Unlock() 131 132 tty.wg.Wait() 133 134 // close our tty device -- we'll get another one if we Start again later. 135 _ = tty.f.Close() 136 137 return nil 138 } 139 140 func (tty *devTty) WindowSize() (WindowSize, error) { 141 size := WindowSize{} 142 ws, err := unix.IoctlGetWinsize(tty.fd, unix.TIOCGWINSZ) 143 if err != nil { 144 return size, err 145 } 146 w := int(ws.Col) 147 h := int(ws.Row) 148 if w == 0 { 149 w, _ = strconv.Atoi(os.Getenv("COLUMNS")) 150 } 151 if w == 0 { 152 w = 80 // default 153 } 154 if h == 0 { 155 h, _ = strconv.Atoi(os.Getenv("LINES")) 156 } 157 if h == 0 { 158 h = 25 // default 159 } 160 size.Width = w 161 size.Height = h 162 size.PixelWidth = int(ws.Xpixel) 163 size.PixelHeight = int(ws.Ypixel) 164 return size, nil 165 } 166 167 func (tty *devTty) NotifyResize(cb func()) { 168 tty.l.Lock() 169 tty.cb = cb 170 tty.l.Unlock() 171 } 172 173 // NewDevTty opens a /dev/tty based Tty. 174 func NewDevTty() (Tty, error) { 175 return NewDevTtyFromDev("/dev/tty") 176 } 177 178 // NewDevTtyFromDev opens a tty device given a path. This can be useful to bind to other nodes. 179 func NewDevTtyFromDev(dev string) (Tty, error) { 180 tty := &devTty{ 181 dev: dev, 182 sig: make(chan os.Signal), 183 } 184 var err error 185 if tty.of, err = os.OpenFile(dev, os.O_RDWR, 0); err != nil { 186 return nil, err 187 } 188 tty.fd = int(tty.of.Fd()) 189 if !term.IsTerminal(tty.fd) { 190 _ = tty.f.Close() 191 return nil, errors.New("not a terminal") 192 } 193 if tty.saved, err = term.GetState(tty.fd); err != nil { 194 _ = tty.f.Close() 195 return nil, fmt.Errorf("failed to get state: %w", err) 196 } 197 return tty, nil 198 }