xref: /OK3568_Linux_fs/yocto/poky/meta/lib/oe/terminal.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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