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