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