blob: 59aa80de6696e8636e6ce0f0c14f7519e10e459a [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
8from distutils.version import LooseVersion
9
10logger = logging.getLogger('BitBake.OE.Terminal')
11
12
13class UnsupportedTerminal(Exception):
14 pass
15
16class NoSupportedTerminals(Exception):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050017 def __init__(self, terms):
18 self.terms = terms
Patrick Williamsc124f4f2015-09-15 14:41:29 -050019
20
21class Registry(oe.classutils.ClassRegistry):
22 command = None
23
24 def __init__(cls, name, bases, attrs):
25 super(Registry, cls).__init__(name.lower(), bases, attrs)
26
27 @property
28 def implemented(cls):
29 return bool(cls.command)
30
31
Patrick Williamsc0f7c042017-02-23 20:41:17 -060032class Terminal(Popen, metaclass=Registry):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050033 def __init__(self, sh_cmd, title=None, env=None, d=None):
34 fmt_sh_cmd = self.format_command(sh_cmd, title)
35 try:
36 Popen.__init__(self, fmt_sh_cmd, env=env)
37 except OSError as exc:
38 import errno
39 if exc.errno == errno.ENOENT:
40 raise UnsupportedTerminal(self.name)
41 else:
42 raise
43
44 def format_command(self, sh_cmd, title):
Brad Bishop19323692019-04-05 15:28:33 -040045 fmt = {'title': title or 'Terminal', 'command': sh_cmd, 'cwd': os.getcwd() }
Patrick Williamsc0f7c042017-02-23 20:41:17 -060046 if isinstance(self.command, str):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050047 return shlex.split(self.command.format(**fmt))
48 else:
49 return [element.format(**fmt) for element in self.command]
50
51class XTerminal(Terminal):
52 def __init__(self, sh_cmd, title=None, env=None, d=None):
53 Terminal.__init__(self, sh_cmd, title, env, d)
54 if not os.environ.get('DISPLAY'):
55 raise UnsupportedTerminal(self.name)
56
57class Gnome(XTerminal):
Brad Bishopf3f93bb2019-10-16 14:33:32 -040058 command = 'gnome-terminal -t "{title}" -- {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050059 priority = 2
60
61 def __init__(self, sh_cmd, title=None, env=None, d=None):
62 # Recent versions of gnome-terminal does not support non-UTF8 charset:
63 # https://bugzilla.gnome.org/show_bug.cgi?id=732127; as a workaround,
64 # clearing the LC_ALL environment variable so it uses the locale.
65 # Once fixed on the gnome-terminal project, this should be removed.
66 if os.getenv('LC_ALL'): os.putenv('LC_ALL','')
67
Brad Bishop6e60e8b2018-02-01 10:27:11 -050068 XTerminal.__init__(self, sh_cmd, title, env, d)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050069
70class Mate(XTerminal):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050071 command = 'mate-terminal --disable-factory -t "{title}" -x {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050072 priority = 2
73
74class Xfce(XTerminal):
75 command = 'xfce4-terminal -T "{title}" -e "{command}"'
76 priority = 2
77
78class Terminology(XTerminal):
79 command = 'terminology -T="{title}" -e {command}'
80 priority = 2
81
82class Konsole(XTerminal):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050083 command = 'konsole --separate --workdir . -p tabtitle="{title}" -e {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050084 priority = 2
85
86 def __init__(self, sh_cmd, title=None, env=None, d=None):
87 # Check version
88 vernum = check_terminal_version("konsole")
89 if vernum and LooseVersion(vernum) < '2.0.0':
90 # Konsole from KDE 3.x
91 self.command = 'konsole -T "{title}" -e {command}'
Brad Bishop6e60e8b2018-02-01 10:27:11 -050092 elif vernum and LooseVersion(vernum) < '16.08.1':
93 # Konsole pre 16.08.01 Has nofork
94 self.command = 'konsole --nofork --workdir . -p tabtitle="{title}" -e {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050095 XTerminal.__init__(self, sh_cmd, title, env, d)
96
97class XTerm(XTerminal):
98 command = 'xterm -T "{title}" -e {command}'
99 priority = 1
100
101class Rxvt(XTerminal):
102 command = 'rxvt -T "{title}" -e {command}'
103 priority = 1
104
105class Screen(Terminal):
106 command = 'screen -D -m -t "{title}" -S devshell {command}'
107
108 def __init__(self, sh_cmd, title=None, env=None, d=None):
109 s_id = "devshell_%i" % os.getpid()
110 self.command = "screen -D -m -t \"{title}\" -S %s {command}" % s_id
111 Terminal.__init__(self, sh_cmd, title, env, d)
112 msg = 'Screen started. Please connect in another terminal with ' \
113 '"screen -r %s"' % s_id
114 if (d):
115 bb.event.fire(bb.event.LogExecTTY(msg, "screen -r %s" % s_id,
116 0.5, 10), d)
117 else:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800118 logger.warning(msg)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500119
120class TmuxRunning(Terminal):
121 """Open a new pane in the current running tmux window"""
122 name = 'tmux-running'
Brad Bishop19323692019-04-05 15:28:33 -0400123 command = 'tmux split-window -c "{cwd}" "{command}"'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500124 priority = 2.75
125
126 def __init__(self, sh_cmd, title=None, env=None, d=None):
127 if not bb.utils.which(os.getenv('PATH'), 'tmux'):
128 raise UnsupportedTerminal('tmux is not installed')
129
130 if not os.getenv('TMUX'):
131 raise UnsupportedTerminal('tmux is not running')
132
133 if not check_tmux_pane_size('tmux'):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500134 raise UnsupportedTerminal('tmux pane too small or tmux < 1.9 version is being used')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500135
136 Terminal.__init__(self, sh_cmd, title, env, d)
137
138class TmuxNewWindow(Terminal):
139 """Open a new window in the current running tmux session"""
140 name = 'tmux-new-window'
Brad Bishop19323692019-04-05 15:28:33 -0400141 command = 'tmux new-window -c "{cwd}" -n "{title}" "{command}"'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500142 priority = 2.70
143
144 def __init__(self, sh_cmd, title=None, env=None, d=None):
145 if not bb.utils.which(os.getenv('PATH'), 'tmux'):
146 raise UnsupportedTerminal('tmux is not installed')
147
148 if not os.getenv('TMUX'):
149 raise UnsupportedTerminal('tmux is not running')
150
151 Terminal.__init__(self, sh_cmd, title, env, d)
152
153class Tmux(Terminal):
154 """Start a new tmux session and window"""
Brad Bishop19323692019-04-05 15:28:33 -0400155 command = 'tmux new -c "{cwd}" -d -s devshell -n devshell "{command}"'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500156 priority = 0.75
157
158 def __init__(self, sh_cmd, title=None, env=None, d=None):
159 if not bb.utils.which(os.getenv('PATH'), 'tmux'):
160 raise UnsupportedTerminal('tmux is not installed')
161
162 # TODO: consider using a 'devshell' session shared amongst all
163 # devshells, if it's already there, add a new window to it.
164 window_name = 'devshell-%i' % os.getpid()
165
Andrew Geisslerc926e172021-05-07 16:11:35 -0500166 self.command = 'tmux new -c "{{cwd}}" -d -s {0} -n {0} "{{command}}"'
167 if not check_tmux_version('1.9'):
168 # `tmux new-session -c` was added in 1.9;
169 # older versions fail with that flag
170 self.command = 'tmux new -d -s {0} -n {0} "{{command}}"'
171 self.command = self.command.format(window_name)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500172 Terminal.__init__(self, sh_cmd, title, env, d)
173
174 attach_cmd = 'tmux att -t {0}'.format(window_name)
175 msg = 'Tmux started. Please connect in another terminal with `tmux att -t {0}`'.format(window_name)
176 if d:
177 bb.event.fire(bb.event.LogExecTTY(msg, attach_cmd, 0.5, 10), d)
178 else:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800179 logger.warning(msg)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500180
181class Custom(Terminal):
182 command = 'false' # This is a placeholder
183 priority = 3
184
185 def __init__(self, sh_cmd, title=None, env=None, d=None):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500186 self.command = d and d.getVar('OE_TERMINAL_CUSTOMCMD')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500187 if self.command:
188 if not '{command}' in self.command:
189 self.command += ' {command}'
190 Terminal.__init__(self, sh_cmd, title, env, d)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800191 logger.warning('Custom terminal was started.')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500192 else:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600193 logger.debug('No custom terminal (OE_TERMINAL_CUSTOMCMD) set')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500194 raise UnsupportedTerminal('OE_TERMINAL_CUSTOMCMD not set')
195
196
197def prioritized():
198 return Registry.prioritized()
199
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500200def get_cmd_list():
201 terms = Registry.prioritized()
202 cmds = []
203 for term in terms:
204 if term.command:
205 cmds.append(term.command)
206 return cmds
207
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500208def spawn_preferred(sh_cmd, title=None, env=None, d=None):
209 """Spawn the first supported terminal, by priority"""
210 for terminal in prioritized():
211 try:
212 spawn(terminal.name, sh_cmd, title, env, d)
213 break
214 except UnsupportedTerminal:
Andrew Geissler1e34c2d2020-05-29 16:02:59 -0500215 pass
216 except:
217 bb.warn("Terminal %s is supported but did not start" % (terminal.name))
218 # when we've run out of options
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500219 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500220 raise NoSupportedTerminals(get_cmd_list())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500221
222def spawn(name, sh_cmd, title=None, env=None, d=None):
223 """Spawn the specified terminal, by name"""
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600224 logger.debug('Attempting to spawn terminal "%s"', name)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500225 try:
226 terminal = Registry.registry[name]
227 except KeyError:
228 raise UnsupportedTerminal(name)
229
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500230 # We need to know when the command completes but some terminals (at least
231 # gnome and tmux) gives us no way to do this. We therefore write the pid
232 # to a file using a "phonehome" wrapper script, then monitor the pid
233 # until it exits.
234 import tempfile
235 import time
236 pidfile = tempfile.NamedTemporaryFile(delete = False).name
237 try:
238 sh_cmd = bb.utils.which(os.getenv('PATH'), "oe-gnome-terminal-phonehome") + " " + pidfile + " " + sh_cmd
239 pipe = terminal(sh_cmd, title, env, d)
240 output = pipe.communicate()[0]
241 if output:
242 output = output.decode("utf-8")
243 if pipe.returncode != 0:
244 raise ExecutionError(sh_cmd, pipe.returncode, output)
245
246 while os.stat(pidfile).st_size <= 0:
247 time.sleep(0.01)
248 continue
249 with open(pidfile, "r") as f:
250 pid = int(f.readline())
251 finally:
252 os.unlink(pidfile)
253
254 while True:
255 try:
256 os.kill(pid, 0)
257 time.sleep(0.1)
258 except OSError:
259 return
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500260
Andrew Geisslerc926e172021-05-07 16:11:35 -0500261def check_tmux_version(desired):
262 vernum = check_terminal_version("tmux")
263 if vernum and LooseVersion(vernum) < desired:
264 return False
265 return vernum
266
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500267def check_tmux_pane_size(tmux):
268 import subprocess as sub
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500269 # On older tmux versions (<1.9), return false. The reason
270 # is that there is no easy way to get the height of the active panel
271 # on current window without nested formats (available from version 1.9)
Andrew Geisslerc926e172021-05-07 16:11:35 -0500272 if not check_tmux_version('1.9'):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500273 return False
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500274 try:
275 p = sub.Popen('%s list-panes -F "#{?pane_active,#{pane_height},}"' % tmux,
276 shell=True,stdout=sub.PIPE,stderr=sub.PIPE)
277 out, err = p.communicate()
278 size = int(out.strip())
279 except OSError as exc:
280 import errno
281 if exc.errno == errno.ENOENT:
282 return None
283 else:
284 raise
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500285
286 return size/2 >= 19
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500287
288def check_terminal_version(terminalName):
289 import subprocess as sub
290 try:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500291 cmdversion = '%s --version' % terminalName
292 if terminalName.startswith('tmux'):
293 cmdversion = '%s -V' % terminalName
294 newenv = os.environ.copy()
295 newenv["LANG"] = "C"
296 p = sub.Popen(['sh', '-c', cmdversion], stdout=sub.PIPE, stderr=sub.PIPE, env=newenv)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500297 out, err = p.communicate()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600298 ver_info = out.decode().rstrip().split('\n')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500299 except OSError as exc:
300 import errno
301 if exc.errno == errno.ENOENT:
302 return None
303 else:
304 raise
305 vernum = None
306 for ver in ver_info:
307 if ver.startswith('Konsole'):
308 vernum = ver.split(' ')[-1]
309 if ver.startswith('GNOME Terminal'):
310 vernum = ver.split(' ')[-1]
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500311 if ver.startswith('MATE Terminal'):
312 vernum = ver.split(' ')[-1]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500313 if ver.startswith('tmux'):
314 vernum = ver.split()[-1]
Brad Bishop19323692019-04-05 15:28:33 -0400315 if ver.startswith('tmux next-'):
316 vernum = ver.split()[-1][5:]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500317 return vernum
318
319def distro_name():
320 try:
321 p = Popen(['lsb_release', '-i'])
322 out, err = p.communicate()
323 distro = out.split(':')[1].strip().lower()
324 except:
325 distro = "unknown"
326 return distro