1# 2# Copyright OpenEmbedded Contributors 3# 4# SPDX-License-Identifier: GPL-2.0-only 5# 6import logging 7import oe.classutils 8import shlex 9from bb.process import Popen, ExecutionError 10 11logger = logging.getLogger('BitBake.OE.Terminal') 12 13 14class UnsupportedTerminal(Exception): 15 pass 16 17class NoSupportedTerminals(Exception): 18 def __init__(self, terms): 19 self.terms = terms 20 21 22class Registry(oe.classutils.ClassRegistry): 23 command = None 24 25 def __init__(cls, name, bases, attrs): 26 super(Registry, cls).__init__(name.lower(), bases, attrs) 27 28 @property 29 def implemented(cls): 30 return bool(cls.command) 31 32 33class Terminal(Popen, metaclass=Registry): 34 def __init__(self, sh_cmd, title=None, env=None, d=None): 35 from subprocess import STDOUT 36 fmt_sh_cmd = self.format_command(sh_cmd, title) 37 try: 38 Popen.__init__(self, fmt_sh_cmd, env=env, stderr=STDOUT) 39 except OSError as exc: 40 import errno 41 if exc.errno == errno.ENOENT: 42 raise UnsupportedTerminal(self.name) 43 else: 44 raise 45 46 def format_command(self, sh_cmd, title): 47 fmt = {'title': title or 'Terminal', 'command': sh_cmd, 'cwd': os.getcwd() } 48 if isinstance(self.command, str): 49 return shlex.split(self.command.format(**fmt)) 50 else: 51 return [element.format(**fmt) for element in self.command] 52 53class XTerminal(Terminal): 54 def __init__(self, sh_cmd, title=None, env=None, d=None): 55 Terminal.__init__(self, sh_cmd, title, env, d) 56 if not os.environ.get('DISPLAY'): 57 raise UnsupportedTerminal(self.name) 58 59class Gnome(XTerminal): 60 command = 'gnome-terminal -t "{title}" -- {command}' 61 priority = 2 62 63 def __init__(self, sh_cmd, title=None, env=None, d=None): 64 # Recent versions of gnome-terminal does not support non-UTF8 charset: 65 # https://bugzilla.gnome.org/show_bug.cgi?id=732127; as a workaround, 66 # clearing the LC_ALL environment variable so it uses the locale. 67 # Once fixed on the gnome-terminal project, this should be removed. 68 if os.getenv('LC_ALL'): os.putenv('LC_ALL','') 69 70 XTerminal.__init__(self, sh_cmd, title, env, d) 71 72class Mate(XTerminal): 73 command = 'mate-terminal --disable-factory -t "{title}" -x {command}' 74 priority = 2 75 76class Xfce(XTerminal): 77 command = 'xfce4-terminal -T "{title}" -e "{command}"' 78 priority = 2 79 80class Terminology(XTerminal): 81 command = 'terminology -T="{title}" -e {command}' 82 priority = 2 83 84class Konsole(XTerminal): 85 command = 'konsole --separate --workdir . -p tabtitle="{title}" -e {command}' 86 priority = 2 87 88 def __init__(self, sh_cmd, title=None, env=None, d=None): 89 # Check version 90 vernum = check_terminal_version("konsole") 91 if vernum and bb.utils.vercmp_string_op(vernum, "2.0.0", "<"): 92 # Konsole from KDE 3.x 93 self.command = 'konsole -T "{title}" -e {command}' 94 elif vernum and bb.utils.vercmp_string_op(vernum, "16.08.1", "<"): 95 # Konsole pre 16.08.01 Has nofork 96 self.command = 'konsole --nofork --workdir . -p tabtitle="{title}" -e {command}' 97 XTerminal.__init__(self, sh_cmd, title, env, d) 98 99class XTerm(XTerminal): 100 command = 'xterm -T "{title}" -e {command}' 101 priority = 1 102 103class Rxvt(XTerminal): 104 command = 'rxvt -T "{title}" -e {command}' 105 priority = 1 106 107class URxvt(XTerminal): 108 command = 'urxvt -T "{title}" -e {command}' 109 priority = 1 110 111class Screen(Terminal): 112 command = 'screen -D -m -t "{title}" -S devshell {command}' 113 114 def __init__(self, sh_cmd, title=None, env=None, d=None): 115 s_id = "devshell_%i" % os.getpid() 116 self.command = "screen -D -m -t \"{title}\" -S %s {command}" % s_id 117 Terminal.__init__(self, sh_cmd, title, env, d) 118 msg = 'Screen started. Please connect in another terminal with ' \ 119 '"screen -r %s"' % s_id 120 if (d): 121 bb.event.fire(bb.event.LogExecTTY(msg, "screen -r %s" % s_id, 122 0.5, 10), d) 123 else: 124 logger.warning(msg) 125 126class TmuxRunning(Terminal): 127 """Open a new pane in the current running tmux window""" 128 name = 'tmux-running' 129 command = 'tmux split-window -c "{cwd}" "{command}"' 130 priority = 2.75 131 132 def __init__(self, sh_cmd, title=None, env=None, d=None): 133 if not bb.utils.which(os.getenv('PATH'), 'tmux'): 134 raise UnsupportedTerminal('tmux is not installed') 135 136 if not os.getenv('TMUX'): 137 raise UnsupportedTerminal('tmux is not running') 138 139 if not check_tmux_pane_size('tmux'): 140 raise UnsupportedTerminal('tmux pane too small or tmux < 1.9 version is being used') 141 142 Terminal.__init__(self, sh_cmd, title, env, d) 143 144class TmuxNewWindow(Terminal): 145 """Open a new window in the current running tmux session""" 146 name = 'tmux-new-window' 147 command = 'tmux new-window -c "{cwd}" -n "{title}" "{command}"' 148 priority = 2.70 149 150 def __init__(self, sh_cmd, title=None, env=None, d=None): 151 if not bb.utils.which(os.getenv('PATH'), 'tmux'): 152 raise UnsupportedTerminal('tmux is not installed') 153 154 if not os.getenv('TMUX'): 155 raise UnsupportedTerminal('tmux is not running') 156 157 Terminal.__init__(self, sh_cmd, title, env, d) 158 159class Tmux(Terminal): 160 """Start a new tmux session and window""" 161 command = 'tmux new -c "{cwd}" -d -s devshell -n devshell "{command}"' 162 priority = 0.75 163 164 def __init__(self, sh_cmd, title=None, env=None, d=None): 165 if not bb.utils.which(os.getenv('PATH'), 'tmux'): 166 raise UnsupportedTerminal('tmux is not installed') 167 168 # TODO: consider using a 'devshell' session shared amongst all 169 # devshells, if it's already there, add a new window to it. 170 window_name = 'devshell-%i' % os.getpid() 171 172 self.command = 'tmux new -c "{{cwd}}" -d -s {0} -n {0} "{{command}}"' 173 if not check_tmux_version('1.9'): 174 # `tmux new-session -c` was added in 1.9; 175 # older versions fail with that flag 176 self.command = 'tmux new -d -s {0} -n {0} "{{command}}"' 177 self.command = self.command.format(window_name) 178 Terminal.__init__(self, sh_cmd, title, env, d) 179 180 attach_cmd = 'tmux att -t {0}'.format(window_name) 181 msg = 'Tmux started. Please connect in another terminal with `tmux att -t {0}`'.format(window_name) 182 if d: 183 bb.event.fire(bb.event.LogExecTTY(msg, attach_cmd, 0.5, 10), d) 184 else: 185 logger.warning(msg) 186 187class Custom(Terminal): 188 command = 'false' # This is a placeholder 189 priority = 3 190 191 def __init__(self, sh_cmd, title=None, env=None, d=None): 192 self.command = d and d.getVar('OE_TERMINAL_CUSTOMCMD') 193 if self.command: 194 if not '{command}' in self.command: 195 self.command += ' {command}' 196 Terminal.__init__(self, sh_cmd, title, env, d) 197 logger.warning('Custom terminal was started.') 198 else: 199 logger.debug('No custom terminal (OE_TERMINAL_CUSTOMCMD) set') 200 raise UnsupportedTerminal('OE_TERMINAL_CUSTOMCMD not set') 201 202 203def prioritized(): 204 return Registry.prioritized() 205 206def get_cmd_list(): 207 terms = Registry.prioritized() 208 cmds = [] 209 for term in terms: 210 if term.command: 211 cmds.append(term.command) 212 return cmds 213 214def spawn_preferred(sh_cmd, title=None, env=None, d=None): 215 """Spawn the first supported terminal, by priority""" 216 for terminal in prioritized(): 217 try: 218 spawn(terminal.name, sh_cmd, title, env, d) 219 break 220 except UnsupportedTerminal: 221 pass 222 except: 223 bb.warn("Terminal %s is supported but did not start" % (terminal.name)) 224 # when we've run out of options 225 else: 226 raise NoSupportedTerminals(get_cmd_list()) 227 228def spawn(name, sh_cmd, title=None, env=None, d=None): 229 """Spawn the specified terminal, by name""" 230 logger.debug('Attempting to spawn terminal "%s"', name) 231 try: 232 terminal = Registry.registry[name] 233 except KeyError: 234 raise UnsupportedTerminal(name) 235 236 # We need to know when the command completes but some terminals (at least 237 # gnome and tmux) gives us no way to do this. We therefore write the pid 238 # to a file using a "phonehome" wrapper script, then monitor the pid 239 # until it exits. 240 import tempfile 241 import time 242 pidfile = tempfile.NamedTemporaryFile(delete = False).name 243 try: 244 sh_cmd = bb.utils.which(os.getenv('PATH'), "oe-gnome-terminal-phonehome") + " " + pidfile + " " + sh_cmd 245 pipe = terminal(sh_cmd, title, env, d) 246 output = pipe.communicate()[0] 247 if output: 248 output = output.decode("utf-8") 249 if pipe.returncode != 0: 250 raise ExecutionError(sh_cmd, pipe.returncode, output) 251 252 while os.stat(pidfile).st_size <= 0: 253 time.sleep(0.01) 254 continue 255 with open(pidfile, "r") as f: 256 pid = int(f.readline()) 257 finally: 258 os.unlink(pidfile) 259 260 while True: 261 try: 262 os.kill(pid, 0) 263 time.sleep(0.1) 264 except OSError: 265 return 266 267def check_tmux_version(desired): 268 vernum = check_terminal_version("tmux") 269 if vernum and bb.utils.vercmp_string_op(vernum, desired, "<"): 270 return False 271 return vernum 272 273def check_tmux_pane_size(tmux): 274 import subprocess as sub 275 # On older tmux versions (<1.9), return false. The reason 276 # is that there is no easy way to get the height of the active panel 277 # on current window without nested formats (available from version 1.9) 278 if not check_tmux_version('1.9'): 279 return False 280 try: 281 p = sub.Popen('%s list-panes -F "#{?pane_active,#{pane_height},}"' % tmux, 282 shell=True,stdout=sub.PIPE,stderr=sub.PIPE) 283 out, err = p.communicate() 284 size = int(out.strip()) 285 except OSError as exc: 286 import errno 287 if exc.errno == errno.ENOENT: 288 return None 289 else: 290 raise 291 292 return size/2 >= 19 293 294def check_terminal_version(terminalName): 295 import subprocess as sub 296 try: 297 cmdversion = '%s --version' % terminalName 298 if terminalName.startswith('tmux'): 299 cmdversion = '%s -V' % terminalName 300 newenv = os.environ.copy() 301 newenv["LANG"] = "C" 302 p = sub.Popen(['sh', '-c', cmdversion], stdout=sub.PIPE, stderr=sub.PIPE, env=newenv) 303 out, err = p.communicate() 304 ver_info = out.decode().rstrip().split('\n') 305 except OSError as exc: 306 import errno 307 if exc.errno == errno.ENOENT: 308 return None 309 else: 310 raise 311 vernum = None 312 for ver in ver_info: 313 if ver.startswith('Konsole'): 314 vernum = ver.split(' ')[-1] 315 if ver.startswith('GNOME Terminal'): 316 vernum = ver.split(' ')[-1] 317 if ver.startswith('MATE Terminal'): 318 vernum = ver.split(' ')[-1] 319 if ver.startswith('tmux'): 320 vernum = ver.split()[-1] 321 if ver.startswith('tmux next-'): 322 vernum = ver.split()[-1][5:] 323 return vernum 324 325def distro_name(): 326 try: 327 p = Popen(['lsb_release', '-i']) 328 out, err = p.communicate() 329 distro = out.split(':')[1].strip().lower() 330 except: 331 distro = "unknown" 332 return distro 333