blob: 71ffc87019f4e3805ba69012dc2c33e8e001dad0 [file] [log] [blame]
Brad Bishopc342db32019-05-15 21:57:59 -04001#
Patrick Williams92b42cb2022-09-03 06:53:57 -05002# Copyright OpenEmbedded Contributors
3#
Brad Bishopc342db32019-05-15 21:57:59 -04004# SPDX-License-Identifier: GPL-2.0-only
5#
Patrick Williamsc124f4f2015-09-15 14:41:29 -05006import logging
7import oe.classutils
8import shlex
9from bb.process import Popen, ExecutionError
Patrick Williamsc124f4f2015-09-15 14:41:29 -050010
11logger = logging.getLogger('BitBake.OE.Terminal')
12
13
14class UnsupportedTerminal(Exception):
15 pass
16
17class NoSupportedTerminals(Exception):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050018 def __init__(self, terms):
19 self.terms = terms
Patrick Williamsc124f4f2015-09-15 14:41:29 -050020
21
22class Registry(oe.classutils.ClassRegistry):
23 command = None
24
25 def __init__(cls, name, bases, attrs):
26 super(Registry, cls).__init__(name.lower(), bases, attrs)
27
28 @property
29 def implemented(cls):
30 return bool(cls.command)
31
32
Patrick Williamsc0f7c042017-02-23 20:41:17 -060033class Terminal(Popen, metaclass=Registry):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050034 def __init__(self, sh_cmd, title=None, env=None, d=None):
Patrick Williams03907ee2022-05-01 06:28:52 -050035 from subprocess import STDOUT
Patrick Williamsc124f4f2015-09-15 14:41:29 -050036 fmt_sh_cmd = self.format_command(sh_cmd, title)
37 try:
Patrick Williams03907ee2022-05-01 06:28:52 -050038 Popen.__init__(self, fmt_sh_cmd, env=env, stderr=STDOUT)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050039 except OSError as exc:
40 import errno
41 if exc.errno == errno.ENOENT:
42 raise UnsupportedTerminal(self.name)
43 else:
44 raise
45
46 def format_command(self, sh_cmd, title):
Brad Bishop19323692019-04-05 15:28:33 -040047 fmt = {'title': title or 'Terminal', 'command': sh_cmd, 'cwd': os.getcwd() }
Patrick Williamsc0f7c042017-02-23 20:41:17 -060048 if isinstance(self.command, str):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050049 return shlex.split(self.command.format(**fmt))
50 else:
51 return [element.format(**fmt) for element in self.command]
52
53class XTerminal(Terminal):
54 def __init__(self, sh_cmd, title=None, env=None, d=None):
55 Terminal.__init__(self, sh_cmd, title, env, d)
56 if not os.environ.get('DISPLAY'):
57 raise UnsupportedTerminal(self.name)
58
59class Gnome(XTerminal):
Brad Bishopf3f93bb2019-10-16 14:33:32 -040060 command = 'gnome-terminal -t "{title}" -- {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050061 priority = 2
62
63 def __init__(self, sh_cmd, title=None, env=None, d=None):
64 # Recent versions of gnome-terminal does not support non-UTF8 charset:
65 # https://bugzilla.gnome.org/show_bug.cgi?id=732127; as a workaround,
66 # clearing the LC_ALL environment variable so it uses the locale.
67 # Once fixed on the gnome-terminal project, this should be removed.
68 if os.getenv('LC_ALL'): os.putenv('LC_ALL','')
69
Brad Bishop6e60e8b2018-02-01 10:27:11 -050070 XTerminal.__init__(self, sh_cmd, title, env, d)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050071
72class Mate(XTerminal):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050073 command = 'mate-terminal --disable-factory -t "{title}" -x {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050074 priority = 2
75
76class Xfce(XTerminal):
77 command = 'xfce4-terminal -T "{title}" -e "{command}"'
78 priority = 2
79
80class Terminology(XTerminal):
81 command = 'terminology -T="{title}" -e {command}'
82 priority = 2
83
84class Konsole(XTerminal):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050085 command = 'konsole --separate --workdir . -p tabtitle="{title}" -e {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050086 priority = 2
87
88 def __init__(self, sh_cmd, title=None, env=None, d=None):
89 # Check version
90 vernum = check_terminal_version("konsole")
Andrew Geissler595f6302022-01-24 19:11:47 +000091 if vernum and bb.utils.vercmp_string_op(vernum, "2.0.0", "<"):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050092 # Konsole from KDE 3.x
93 self.command = 'konsole -T "{title}" -e {command}'
Andrew Geissler595f6302022-01-24 19:11:47 +000094 elif vernum and bb.utils.vercmp_string_op(vernum, "16.08.1", "<"):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050095 # Konsole pre 16.08.01 Has nofork
96 self.command = 'konsole --nofork --workdir . -p tabtitle="{title}" -e {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050097 XTerminal.__init__(self, sh_cmd, title, env, d)
98
99class XTerm(XTerminal):
100 command = 'xterm -T "{title}" -e {command}'
101 priority = 1
102
103class Rxvt(XTerminal):
104 command = 'rxvt -T "{title}" -e {command}'
105 priority = 1
106
107class Screen(Terminal):
108 command = 'screen -D -m -t "{title}" -S devshell {command}'
109
110 def __init__(self, sh_cmd, title=None, env=None, d=None):
111 s_id = "devshell_%i" % os.getpid()
112 self.command = "screen -D -m -t \"{title}\" -S %s {command}" % s_id
113 Terminal.__init__(self, sh_cmd, title, env, d)
114 msg = 'Screen started. Please connect in another terminal with ' \
115 '"screen -r %s"' % s_id
116 if (d):
117 bb.event.fire(bb.event.LogExecTTY(msg, "screen -r %s" % s_id,
118 0.5, 10), d)
119 else:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800120 logger.warning(msg)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500121
122class TmuxRunning(Terminal):
123 """Open a new pane in the current running tmux window"""
124 name = 'tmux-running'
Brad Bishop19323692019-04-05 15:28:33 -0400125 command = 'tmux split-window -c "{cwd}" "{command}"'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500126 priority = 2.75
127
128 def __init__(self, sh_cmd, title=None, env=None, d=None):
129 if not bb.utils.which(os.getenv('PATH'), 'tmux'):
130 raise UnsupportedTerminal('tmux is not installed')
131
132 if not os.getenv('TMUX'):
133 raise UnsupportedTerminal('tmux is not running')
134
135 if not check_tmux_pane_size('tmux'):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500136 raise UnsupportedTerminal('tmux pane too small or tmux < 1.9 version is being used')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500137
138 Terminal.__init__(self, sh_cmd, title, env, d)
139
140class TmuxNewWindow(Terminal):
141 """Open a new window in the current running tmux session"""
142 name = 'tmux-new-window'
Brad Bishop19323692019-04-05 15:28:33 -0400143 command = 'tmux new-window -c "{cwd}" -n "{title}" "{command}"'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500144 priority = 2.70
145
146 def __init__(self, sh_cmd, title=None, env=None, d=None):
147 if not bb.utils.which(os.getenv('PATH'), 'tmux'):
148 raise UnsupportedTerminal('tmux is not installed')
149
150 if not os.getenv('TMUX'):
151 raise UnsupportedTerminal('tmux is not running')
152
153 Terminal.__init__(self, sh_cmd, title, env, d)
154
155class Tmux(Terminal):
156 """Start a new tmux session and window"""
Brad Bishop19323692019-04-05 15:28:33 -0400157 command = 'tmux new -c "{cwd}" -d -s devshell -n devshell "{command}"'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500158 priority = 0.75
159
160 def __init__(self, sh_cmd, title=None, env=None, d=None):
161 if not bb.utils.which(os.getenv('PATH'), 'tmux'):
162 raise UnsupportedTerminal('tmux is not installed')
163
164 # TODO: consider using a 'devshell' session shared amongst all
165 # devshells, if it's already there, add a new window to it.
166 window_name = 'devshell-%i' % os.getpid()
167
Andrew Geisslerc926e172021-05-07 16:11:35 -0500168 self.command = 'tmux new -c "{{cwd}}" -d -s {0} -n {0} "{{command}}"'
169 if not check_tmux_version('1.9'):
170 # `tmux new-session -c` was added in 1.9;
171 # older versions fail with that flag
172 self.command = 'tmux new -d -s {0} -n {0} "{{command}}"'
173 self.command = self.command.format(window_name)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500174 Terminal.__init__(self, sh_cmd, title, env, d)
175
176 attach_cmd = 'tmux att -t {0}'.format(window_name)
177 msg = 'Tmux started. Please connect in another terminal with `tmux att -t {0}`'.format(window_name)
178 if d:
179 bb.event.fire(bb.event.LogExecTTY(msg, attach_cmd, 0.5, 10), d)
180 else:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800181 logger.warning(msg)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500182
183class Custom(Terminal):
184 command = 'false' # This is a placeholder
185 priority = 3
186
187 def __init__(self, sh_cmd, title=None, env=None, d=None):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500188 self.command = d and d.getVar('OE_TERMINAL_CUSTOMCMD')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500189 if self.command:
190 if not '{command}' in self.command:
191 self.command += ' {command}'
192 Terminal.__init__(self, sh_cmd, title, env, d)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800193 logger.warning('Custom terminal was started.')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500194 else:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600195 logger.debug('No custom terminal (OE_TERMINAL_CUSTOMCMD) set')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500196 raise UnsupportedTerminal('OE_TERMINAL_CUSTOMCMD not set')
197
198
199def prioritized():
200 return Registry.prioritized()
201
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500202def get_cmd_list():
203 terms = Registry.prioritized()
204 cmds = []
205 for term in terms:
206 if term.command:
207 cmds.append(term.command)
208 return cmds
209
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500210def spawn_preferred(sh_cmd, title=None, env=None, d=None):
211 """Spawn the first supported terminal, by priority"""
212 for terminal in prioritized():
213 try:
214 spawn(terminal.name, sh_cmd, title, env, d)
215 break
216 except UnsupportedTerminal:
Andrew Geissler1e34c2d2020-05-29 16:02:59 -0500217 pass
218 except:
219 bb.warn("Terminal %s is supported but did not start" % (terminal.name))
220 # when we've run out of options
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500221 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500222 raise NoSupportedTerminals(get_cmd_list())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500223
224def spawn(name, sh_cmd, title=None, env=None, d=None):
225 """Spawn the specified terminal, by name"""
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600226 logger.debug('Attempting to spawn terminal "%s"', name)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500227 try:
228 terminal = Registry.registry[name]
229 except KeyError:
230 raise UnsupportedTerminal(name)
231
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500232 # We need to know when the command completes but some terminals (at least
233 # gnome and tmux) gives us no way to do this. We therefore write the pid
234 # to a file using a "phonehome" wrapper script, then monitor the pid
235 # until it exits.
236 import tempfile
237 import time
238 pidfile = tempfile.NamedTemporaryFile(delete = False).name
239 try:
240 sh_cmd = bb.utils.which(os.getenv('PATH'), "oe-gnome-terminal-phonehome") + " " + pidfile + " " + sh_cmd
241 pipe = terminal(sh_cmd, title, env, d)
242 output = pipe.communicate()[0]
243 if output:
244 output = output.decode("utf-8")
245 if pipe.returncode != 0:
246 raise ExecutionError(sh_cmd, pipe.returncode, output)
247
248 while os.stat(pidfile).st_size <= 0:
249 time.sleep(0.01)
250 continue
251 with open(pidfile, "r") as f:
252 pid = int(f.readline())
253 finally:
254 os.unlink(pidfile)
255
256 while True:
257 try:
258 os.kill(pid, 0)
259 time.sleep(0.1)
260 except OSError:
261 return
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500262
Andrew Geisslerc926e172021-05-07 16:11:35 -0500263def check_tmux_version(desired):
264 vernum = check_terminal_version("tmux")
Andrew Geissler595f6302022-01-24 19:11:47 +0000265 if vernum and bb.utils.vercmp_string_op(vernum, desired, "<"):
Andrew Geisslerc926e172021-05-07 16:11:35 -0500266 return False
267 return vernum
268
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500269def check_tmux_pane_size(tmux):
270 import subprocess as sub
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500271 # On older tmux versions (<1.9), return false. The reason
272 # is that there is no easy way to get the height of the active panel
273 # on current window without nested formats (available from version 1.9)
Andrew Geisslerc926e172021-05-07 16:11:35 -0500274 if not check_tmux_version('1.9'):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500275 return False
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500276 try:
277 p = sub.Popen('%s list-panes -F "#{?pane_active,#{pane_height},}"' % tmux,
278 shell=True,stdout=sub.PIPE,stderr=sub.PIPE)
279 out, err = p.communicate()
280 size = int(out.strip())
281 except OSError as exc:
282 import errno
283 if exc.errno == errno.ENOENT:
284 return None
285 else:
286 raise
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500287
288 return size/2 >= 19
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500289
290def check_terminal_version(terminalName):
291 import subprocess as sub
292 try:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500293 cmdversion = '%s --version' % terminalName
294 if terminalName.startswith('tmux'):
295 cmdversion = '%s -V' % terminalName
296 newenv = os.environ.copy()
297 newenv["LANG"] = "C"
298 p = sub.Popen(['sh', '-c', cmdversion], stdout=sub.PIPE, stderr=sub.PIPE, env=newenv)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500299 out, err = p.communicate()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600300 ver_info = out.decode().rstrip().split('\n')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500301 except OSError as exc:
302 import errno
303 if exc.errno == errno.ENOENT:
304 return None
305 else:
306 raise
307 vernum = None
308 for ver in ver_info:
309 if ver.startswith('Konsole'):
310 vernum = ver.split(' ')[-1]
311 if ver.startswith('GNOME Terminal'):
312 vernum = ver.split(' ')[-1]
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500313 if ver.startswith('MATE Terminal'):
314 vernum = ver.split(' ')[-1]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500315 if ver.startswith('tmux'):
316 vernum = ver.split()[-1]
Brad Bishop19323692019-04-05 15:28:33 -0400317 if ver.startswith('tmux next-'):
318 vernum = ver.split()[-1][5:]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500319 return vernum
320
321def distro_name():
322 try:
323 p = Popen(['lsb_release', '-i'])
324 out, err = p.communicate()
325 distro = out.split(':')[1].strip().lower()
326 except:
327 distro = "unknown"
328 return distro