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