blob: 53186c4a3ed7eb906d65d94ab4e3a36e46467046 [file] [log] [blame]
Brad Bishopc342db32019-05-15 21:57:59 -04001#
2# SPDX-License-Identifier: GPL-2.0-only
3#
Patrick Williamsc124f4f2015-09-15 14:41:29 -05004import logging
5import oe.classutils
6import shlex
7from bb.process import Popen, ExecutionError
Patrick Williamsc124f4f2015-09-15 14:41:29 -05008
9logger = logging.getLogger('BitBake.OE.Terminal')
10
11
12class UnsupportedTerminal(Exception):
13 pass
14
15class NoSupportedTerminals(Exception):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050016 def __init__(self, terms):
17 self.terms = terms
Patrick Williamsc124f4f2015-09-15 14:41:29 -050018
19
20class Registry(oe.classutils.ClassRegistry):
21 command = None
22
23 def __init__(cls, name, bases, attrs):
24 super(Registry, cls).__init__(name.lower(), bases, attrs)
25
26 @property
27 def implemented(cls):
28 return bool(cls.command)
29
30
Patrick Williamsc0f7c042017-02-23 20:41:17 -060031class Terminal(Popen, metaclass=Registry):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050032 def __init__(self, sh_cmd, title=None, env=None, d=None):
33 fmt_sh_cmd = self.format_command(sh_cmd, title)
34 try:
35 Popen.__init__(self, fmt_sh_cmd, env=env)
36 except OSError as exc:
37 import errno
38 if exc.errno == errno.ENOENT:
39 raise UnsupportedTerminal(self.name)
40 else:
41 raise
42
43 def format_command(self, sh_cmd, title):
Brad Bishop19323692019-04-05 15:28:33 -040044 fmt = {'title': title or 'Terminal', 'command': sh_cmd, 'cwd': os.getcwd() }
Patrick Williamsc0f7c042017-02-23 20:41:17 -060045 if isinstance(self.command, str):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050046 return shlex.split(self.command.format(**fmt))
47 else:
48 return [element.format(**fmt) for element in self.command]
49
50class XTerminal(Terminal):
51 def __init__(self, sh_cmd, title=None, env=None, d=None):
52 Terminal.__init__(self, sh_cmd, title, env, d)
53 if not os.environ.get('DISPLAY'):
54 raise UnsupportedTerminal(self.name)
55
56class Gnome(XTerminal):
Brad Bishopf3f93bb2019-10-16 14:33:32 -040057 command = 'gnome-terminal -t "{title}" -- {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050058 priority = 2
59
60 def __init__(self, sh_cmd, title=None, env=None, d=None):
61 # Recent versions of gnome-terminal does not support non-UTF8 charset:
62 # https://bugzilla.gnome.org/show_bug.cgi?id=732127; as a workaround,
63 # clearing the LC_ALL environment variable so it uses the locale.
64 # Once fixed on the gnome-terminal project, this should be removed.
65 if os.getenv('LC_ALL'): os.putenv('LC_ALL','')
66
Brad Bishop6e60e8b2018-02-01 10:27:11 -050067 XTerminal.__init__(self, sh_cmd, title, env, d)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050068
69class Mate(XTerminal):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050070 command = 'mate-terminal --disable-factory -t "{title}" -x {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050071 priority = 2
72
73class Xfce(XTerminal):
74 command = 'xfce4-terminal -T "{title}" -e "{command}"'
75 priority = 2
76
77class Terminology(XTerminal):
78 command = 'terminology -T="{title}" -e {command}'
79 priority = 2
80
81class Konsole(XTerminal):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050082 command = 'konsole --separate --workdir . -p tabtitle="{title}" -e {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050083 priority = 2
84
85 def __init__(self, sh_cmd, title=None, env=None, d=None):
86 # Check version
87 vernum = check_terminal_version("konsole")
Andrew Geissler595f6302022-01-24 19:11:47 +000088 if vernum and bb.utils.vercmp_string_op(vernum, "2.0.0", "<"):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050089 # Konsole from KDE 3.x
90 self.command = 'konsole -T "{title}" -e {command}'
Andrew Geissler595f6302022-01-24 19:11:47 +000091 elif vernum and bb.utils.vercmp_string_op(vernum, "16.08.1", "<"):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050092 # Konsole pre 16.08.01 Has nofork
93 self.command = 'konsole --nofork --workdir . -p tabtitle="{title}" -e {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050094 XTerminal.__init__(self, sh_cmd, title, env, d)
95
96class XTerm(XTerminal):
97 command = 'xterm -T "{title}" -e {command}'
98 priority = 1
99
100class Rxvt(XTerminal):
101 command = 'rxvt -T "{title}" -e {command}'
102 priority = 1
103
104class Screen(Terminal):
105 command = 'screen -D -m -t "{title}" -S devshell {command}'
106
107 def __init__(self, sh_cmd, title=None, env=None, d=None):
108 s_id = "devshell_%i" % os.getpid()
109 self.command = "screen -D -m -t \"{title}\" -S %s {command}" % s_id
110 Terminal.__init__(self, sh_cmd, title, env, d)
111 msg = 'Screen started. Please connect in another terminal with ' \
112 '"screen -r %s"' % s_id
113 if (d):
114 bb.event.fire(bb.event.LogExecTTY(msg, "screen -r %s" % s_id,
115 0.5, 10), d)
116 else:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800117 logger.warning(msg)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500118
119class TmuxRunning(Terminal):
120 """Open a new pane in the current running tmux window"""
121 name = 'tmux-running'
Brad Bishop19323692019-04-05 15:28:33 -0400122 command = 'tmux split-window -c "{cwd}" "{command}"'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500123 priority = 2.75
124
125 def __init__(self, sh_cmd, title=None, env=None, d=None):
126 if not bb.utils.which(os.getenv('PATH'), 'tmux'):
127 raise UnsupportedTerminal('tmux is not installed')
128
129 if not os.getenv('TMUX'):
130 raise UnsupportedTerminal('tmux is not running')
131
132 if not check_tmux_pane_size('tmux'):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500133 raise UnsupportedTerminal('tmux pane too small or tmux < 1.9 version is being used')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500134
135 Terminal.__init__(self, sh_cmd, title, env, d)
136
137class TmuxNewWindow(Terminal):
138 """Open a new window in the current running tmux session"""
139 name = 'tmux-new-window'
Brad Bishop19323692019-04-05 15:28:33 -0400140 command = 'tmux new-window -c "{cwd}" -n "{title}" "{command}"'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500141 priority = 2.70
142
143 def __init__(self, sh_cmd, title=None, env=None, d=None):
144 if not bb.utils.which(os.getenv('PATH'), 'tmux'):
145 raise UnsupportedTerminal('tmux is not installed')
146
147 if not os.getenv('TMUX'):
148 raise UnsupportedTerminal('tmux is not running')
149
150 Terminal.__init__(self, sh_cmd, title, env, d)
151
152class Tmux(Terminal):
153 """Start a new tmux session and window"""
Brad Bishop19323692019-04-05 15:28:33 -0400154 command = 'tmux new -c "{cwd}" -d -s devshell -n devshell "{command}"'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500155 priority = 0.75
156
157 def __init__(self, sh_cmd, title=None, env=None, d=None):
158 if not bb.utils.which(os.getenv('PATH'), 'tmux'):
159 raise UnsupportedTerminal('tmux is not installed')
160
161 # TODO: consider using a 'devshell' session shared amongst all
162 # devshells, if it's already there, add a new window to it.
163 window_name = 'devshell-%i' % os.getpid()
164
Andrew Geisslerc926e172021-05-07 16:11:35 -0500165 self.command = 'tmux new -c "{{cwd}}" -d -s {0} -n {0} "{{command}}"'
166 if not check_tmux_version('1.9'):
167 # `tmux new-session -c` was added in 1.9;
168 # older versions fail with that flag
169 self.command = 'tmux new -d -s {0} -n {0} "{{command}}"'
170 self.command = self.command.format(window_name)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500171 Terminal.__init__(self, sh_cmd, title, env, d)
172
173 attach_cmd = 'tmux att -t {0}'.format(window_name)
174 msg = 'Tmux started. Please connect in another terminal with `tmux att -t {0}`'.format(window_name)
175 if d:
176 bb.event.fire(bb.event.LogExecTTY(msg, attach_cmd, 0.5, 10), d)
177 else:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800178 logger.warning(msg)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500179
180class Custom(Terminal):
181 command = 'false' # This is a placeholder
182 priority = 3
183
184 def __init__(self, sh_cmd, title=None, env=None, d=None):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500185 self.command = d and d.getVar('OE_TERMINAL_CUSTOMCMD')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500186 if self.command:
187 if not '{command}' in self.command:
188 self.command += ' {command}'
189 Terminal.__init__(self, sh_cmd, title, env, d)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800190 logger.warning('Custom terminal was started.')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500191 else:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600192 logger.debug('No custom terminal (OE_TERMINAL_CUSTOMCMD) set')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500193 raise UnsupportedTerminal('OE_TERMINAL_CUSTOMCMD not set')
194
195
196def prioritized():
197 return Registry.prioritized()
198
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500199def get_cmd_list():
200 terms = Registry.prioritized()
201 cmds = []
202 for term in terms:
203 if term.command:
204 cmds.append(term.command)
205 return cmds
206
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500207def spawn_preferred(sh_cmd, title=None, env=None, d=None):
208 """Spawn the first supported terminal, by priority"""
209 for terminal in prioritized():
210 try:
211 spawn(terminal.name, sh_cmd, title, env, d)
212 break
213 except UnsupportedTerminal:
Andrew Geissler1e34c2d2020-05-29 16:02:59 -0500214 pass
215 except:
216 bb.warn("Terminal %s is supported but did not start" % (terminal.name))
217 # when we've run out of options
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500218 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500219 raise NoSupportedTerminals(get_cmd_list())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500220
221def spawn(name, sh_cmd, title=None, env=None, d=None):
222 """Spawn the specified terminal, by name"""
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600223 logger.debug('Attempting to spawn terminal "%s"', name)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500224 try:
225 terminal = Registry.registry[name]
226 except KeyError:
227 raise UnsupportedTerminal(name)
228
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500229 # We need to know when the command completes but some terminals (at least
230 # gnome and tmux) gives us no way to do this. We therefore write the pid
231 # to a file using a "phonehome" wrapper script, then monitor the pid
232 # until it exits.
233 import tempfile
234 import time
235 pidfile = tempfile.NamedTemporaryFile(delete = False).name
236 try:
237 sh_cmd = bb.utils.which(os.getenv('PATH'), "oe-gnome-terminal-phonehome") + " " + pidfile + " " + sh_cmd
238 pipe = terminal(sh_cmd, title, env, d)
239 output = pipe.communicate()[0]
240 if output:
241 output = output.decode("utf-8")
242 if pipe.returncode != 0:
243 raise ExecutionError(sh_cmd, pipe.returncode, output)
244
245 while os.stat(pidfile).st_size <= 0:
246 time.sleep(0.01)
247 continue
248 with open(pidfile, "r") as f:
249 pid = int(f.readline())
250 finally:
251 os.unlink(pidfile)
252
253 while True:
254 try:
255 os.kill(pid, 0)
256 time.sleep(0.1)
257 except OSError:
258 return
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500259
Andrew Geisslerc926e172021-05-07 16:11:35 -0500260def check_tmux_version(desired):
261 vernum = check_terminal_version("tmux")
Andrew Geissler595f6302022-01-24 19:11:47 +0000262 if vernum and bb.utils.vercmp_string_op(vernum, desired, "<"):
Andrew Geisslerc926e172021-05-07 16:11:35 -0500263 return False
264 return vernum
265
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500266def check_tmux_pane_size(tmux):
267 import subprocess as sub
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500268 # On older tmux versions (<1.9), return false. The reason
269 # is that there is no easy way to get the height of the active panel
270 # on current window without nested formats (available from version 1.9)
Andrew Geisslerc926e172021-05-07 16:11:35 -0500271 if not check_tmux_version('1.9'):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500272 return False
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500273 try:
274 p = sub.Popen('%s list-panes -F "#{?pane_active,#{pane_height},}"' % tmux,
275 shell=True,stdout=sub.PIPE,stderr=sub.PIPE)
276 out, err = p.communicate()
277 size = int(out.strip())
278 except OSError as exc:
279 import errno
280 if exc.errno == errno.ENOENT:
281 return None
282 else:
283 raise
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500284
285 return size/2 >= 19
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500286
287def check_terminal_version(terminalName):
288 import subprocess as sub
289 try:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500290 cmdversion = '%s --version' % terminalName
291 if terminalName.startswith('tmux'):
292 cmdversion = '%s -V' % terminalName
293 newenv = os.environ.copy()
294 newenv["LANG"] = "C"
295 p = sub.Popen(['sh', '-c', cmdversion], stdout=sub.PIPE, stderr=sub.PIPE, env=newenv)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500296 out, err = p.communicate()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600297 ver_info = out.decode().rstrip().split('\n')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500298 except OSError as exc:
299 import errno
300 if exc.errno == errno.ENOENT:
301 return None
302 else:
303 raise
304 vernum = None
305 for ver in ver_info:
306 if ver.startswith('Konsole'):
307 vernum = ver.split(' ')[-1]
308 if ver.startswith('GNOME Terminal'):
309 vernum = ver.split(' ')[-1]
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500310 if ver.startswith('MATE Terminal'):
311 vernum = ver.split(' ')[-1]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500312 if ver.startswith('tmux'):
313 vernum = ver.split()[-1]
Brad Bishop19323692019-04-05 15:28:33 -0400314 if ver.startswith('tmux next-'):
315 vernum = ver.split()[-1][5:]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500316 return vernum
317
318def distro_name():
319 try:
320 p = Popen(['lsb_release', '-i'])
321 out, err = p.communicate()
322 distro = out.split(':')[1].strip().lower()
323 except:
324 distro = "unknown"
325 return distro