blob: 4412bc14c1d1ffc16e84f89313e9aed6694ab742 [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
Patrick Williamse760df82023-05-26 11:10:49 -0500107class URxvt(XTerminal):
108 command = 'urxvt -T "{title}" -e {command}'
109 priority = 1
110
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500111class Screen(Terminal):
112 command = 'screen -D -m -t "{title}" -S devshell {command}'
113
114 def __init__(self, sh_cmd, title=None, env=None, d=None):
115 s_id = "devshell_%i" % os.getpid()
116 self.command = "screen -D -m -t \"{title}\" -S %s {command}" % s_id
117 Terminal.__init__(self, sh_cmd, title, env, d)
118 msg = 'Screen started. Please connect in another terminal with ' \
119 '"screen -r %s"' % s_id
120 if (d):
121 bb.event.fire(bb.event.LogExecTTY(msg, "screen -r %s" % s_id,
122 0.5, 10), d)
123 else:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800124 logger.warning(msg)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500125
126class TmuxRunning(Terminal):
127 """Open a new pane in the current running tmux window"""
128 name = 'tmux-running'
Brad Bishop19323692019-04-05 15:28:33 -0400129 command = 'tmux split-window -c "{cwd}" "{command}"'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500130 priority = 2.75
131
132 def __init__(self, sh_cmd, title=None, env=None, d=None):
133 if not bb.utils.which(os.getenv('PATH'), 'tmux'):
134 raise UnsupportedTerminal('tmux is not installed')
135
136 if not os.getenv('TMUX'):
137 raise UnsupportedTerminal('tmux is not running')
138
139 if not check_tmux_pane_size('tmux'):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500140 raise UnsupportedTerminal('tmux pane too small or tmux < 1.9 version is being used')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500141
142 Terminal.__init__(self, sh_cmd, title, env, d)
143
144class TmuxNewWindow(Terminal):
145 """Open a new window in the current running tmux session"""
146 name = 'tmux-new-window'
Brad Bishop19323692019-04-05 15:28:33 -0400147 command = 'tmux new-window -c "{cwd}" -n "{title}" "{command}"'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500148 priority = 2.70
149
150 def __init__(self, sh_cmd, title=None, env=None, d=None):
151 if not bb.utils.which(os.getenv('PATH'), 'tmux'):
152 raise UnsupportedTerminal('tmux is not installed')
153
154 if not os.getenv('TMUX'):
155 raise UnsupportedTerminal('tmux is not running')
156
157 Terminal.__init__(self, sh_cmd, title, env, d)
158
159class Tmux(Terminal):
160 """Start a new tmux session and window"""
Brad Bishop19323692019-04-05 15:28:33 -0400161 command = 'tmux new -c "{cwd}" -d -s devshell -n devshell "{command}"'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500162 priority = 0.75
163
164 def __init__(self, sh_cmd, title=None, env=None, d=None):
165 if not bb.utils.which(os.getenv('PATH'), 'tmux'):
166 raise UnsupportedTerminal('tmux is not installed')
167
168 # TODO: consider using a 'devshell' session shared amongst all
169 # devshells, if it's already there, add a new window to it.
170 window_name = 'devshell-%i' % os.getpid()
171
Andrew Geisslerc926e172021-05-07 16:11:35 -0500172 self.command = 'tmux new -c "{{cwd}}" -d -s {0} -n {0} "{{command}}"'
173 if not check_tmux_version('1.9'):
174 # `tmux new-session -c` was added in 1.9;
175 # older versions fail with that flag
176 self.command = 'tmux new -d -s {0} -n {0} "{{command}}"'
177 self.command = self.command.format(window_name)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500178 Terminal.__init__(self, sh_cmd, title, env, d)
179
180 attach_cmd = 'tmux att -t {0}'.format(window_name)
181 msg = 'Tmux started. Please connect in another terminal with `tmux att -t {0}`'.format(window_name)
182 if d:
183 bb.event.fire(bb.event.LogExecTTY(msg, attach_cmd, 0.5, 10), d)
184 else:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800185 logger.warning(msg)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500186
187class Custom(Terminal):
188 command = 'false' # This is a placeholder
189 priority = 3
190
191 def __init__(self, sh_cmd, title=None, env=None, d=None):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500192 self.command = d and d.getVar('OE_TERMINAL_CUSTOMCMD')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500193 if self.command:
194 if not '{command}' in self.command:
195 self.command += ' {command}'
196 Terminal.__init__(self, sh_cmd, title, env, d)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800197 logger.warning('Custom terminal was started.')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500198 else:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600199 logger.debug('No custom terminal (OE_TERMINAL_CUSTOMCMD) set')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500200 raise UnsupportedTerminal('OE_TERMINAL_CUSTOMCMD not set')
201
202
203def prioritized():
204 return Registry.prioritized()
205
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500206def get_cmd_list():
207 terms = Registry.prioritized()
208 cmds = []
209 for term in terms:
210 if term.command:
211 cmds.append(term.command)
212 return cmds
213
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500214def spawn_preferred(sh_cmd, title=None, env=None, d=None):
215 """Spawn the first supported terminal, by priority"""
216 for terminal in prioritized():
217 try:
218 spawn(terminal.name, sh_cmd, title, env, d)
219 break
220 except UnsupportedTerminal:
Andrew Geissler1e34c2d2020-05-29 16:02:59 -0500221 pass
222 except:
223 bb.warn("Terminal %s is supported but did not start" % (terminal.name))
224 # when we've run out of options
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500225 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500226 raise NoSupportedTerminals(get_cmd_list())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500227
228def spawn(name, sh_cmd, title=None, env=None, d=None):
229 """Spawn the specified terminal, by name"""
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600230 logger.debug('Attempting to spawn terminal "%s"', name)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500231 try:
232 terminal = Registry.registry[name]
233 except KeyError:
234 raise UnsupportedTerminal(name)
235
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500236 # We need to know when the command completes but some terminals (at least
237 # gnome and tmux) gives us no way to do this. We therefore write the pid
238 # to a file using a "phonehome" wrapper script, then monitor the pid
239 # until it exits.
240 import tempfile
241 import time
242 pidfile = tempfile.NamedTemporaryFile(delete = False).name
243 try:
244 sh_cmd = bb.utils.which(os.getenv('PATH'), "oe-gnome-terminal-phonehome") + " " + pidfile + " " + sh_cmd
245 pipe = terminal(sh_cmd, title, env, d)
246 output = pipe.communicate()[0]
247 if output:
248 output = output.decode("utf-8")
249 if pipe.returncode != 0:
250 raise ExecutionError(sh_cmd, pipe.returncode, output)
251
252 while os.stat(pidfile).st_size <= 0:
253 time.sleep(0.01)
254 continue
255 with open(pidfile, "r") as f:
256 pid = int(f.readline())
257 finally:
258 os.unlink(pidfile)
259
260 while True:
261 try:
262 os.kill(pid, 0)
263 time.sleep(0.1)
264 except OSError:
265 return
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500266
Andrew Geisslerc926e172021-05-07 16:11:35 -0500267def check_tmux_version(desired):
268 vernum = check_terminal_version("tmux")
Andrew Geissler595f6302022-01-24 19:11:47 +0000269 if vernum and bb.utils.vercmp_string_op(vernum, desired, "<"):
Andrew Geisslerc926e172021-05-07 16:11:35 -0500270 return False
271 return vernum
272
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500273def check_tmux_pane_size(tmux):
274 import subprocess as sub
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500275 # On older tmux versions (<1.9), return false. The reason
276 # is that there is no easy way to get the height of the active panel
277 # on current window without nested formats (available from version 1.9)
Andrew Geisslerc926e172021-05-07 16:11:35 -0500278 if not check_tmux_version('1.9'):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500279 return False
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500280 try:
281 p = sub.Popen('%s list-panes -F "#{?pane_active,#{pane_height},}"' % tmux,
282 shell=True,stdout=sub.PIPE,stderr=sub.PIPE)
283 out, err = p.communicate()
284 size = int(out.strip())
285 except OSError as exc:
286 import errno
287 if exc.errno == errno.ENOENT:
288 return None
289 else:
290 raise
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500291
292 return size/2 >= 19
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500293
294def check_terminal_version(terminalName):
295 import subprocess as sub
296 try:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500297 cmdversion = '%s --version' % terminalName
298 if terminalName.startswith('tmux'):
299 cmdversion = '%s -V' % terminalName
300 newenv = os.environ.copy()
301 newenv["LANG"] = "C"
302 p = sub.Popen(['sh', '-c', cmdversion], stdout=sub.PIPE, stderr=sub.PIPE, env=newenv)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500303 out, err = p.communicate()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600304 ver_info = out.decode().rstrip().split('\n')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500305 except OSError as exc:
306 import errno
307 if exc.errno == errno.ENOENT:
308 return None
309 else:
310 raise
311 vernum = None
312 for ver in ver_info:
313 if ver.startswith('Konsole'):
314 vernum = ver.split(' ')[-1]
315 if ver.startswith('GNOME Terminal'):
316 vernum = ver.split(' ')[-1]
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500317 if ver.startswith('MATE Terminal'):
318 vernum = ver.split(' ')[-1]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500319 if ver.startswith('tmux'):
320 vernum = ver.split()[-1]
Brad Bishop19323692019-04-05 15:28:33 -0400321 if ver.startswith('tmux next-'):
322 vernum = ver.split()[-1][5:]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500323 return vernum
324
325def distro_name():
326 try:
327 p = Popen(['lsb_release', '-i'])
328 out, err = p.communicate()
329 distro = out.split(':')[1].strip().lower()
330 except:
331 distro = "unknown"
332 return distro