xref: /openbmc/openbmc/poky/meta/lib/oe/terminal.py (revision e760df85)
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