blob: 94afe394ed165f7259842851b7a47a819c0a70d7 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001import 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):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050014 def __init__(self, terms):
15 self.terms = terms
Patrick Williamsc124f4f2015-09-15 14:41:29 -050016
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
Patrick Williamsc0f7c042017-02-23 20:41:17 -060029class Terminal(Popen, metaclass=Registry):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050030 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}
Patrick Williamsc0f7c042017-02-23 20:41:17 -060043 if isinstance(self.command, str):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050044 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):
Patrick Williamsc0f7c042017-02-23 20:41:17 -060055 command = 'gnome-terminal -t "{title}" -x {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050056 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
Brad Bishop6e60e8b2018-02-01 10:27:11 -050065 XTerminal.__init__(self, sh_cmd, title, env, d)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050066
67class Mate(XTerminal):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050068 command = 'mate-terminal --disable-factory -t "{title}" -x {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050069 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):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050080 command = 'konsole --separate --workdir . -p tabtitle="{title}" -e {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050081 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}'
Brad Bishop6e60e8b2018-02-01 10:27:11 -050089 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}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050092 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.warn(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 "{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'):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500131 raise UnsupportedTerminal('tmux pane too small or tmux < 1.9 version is being used')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500132
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 -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 -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 -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.warn(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):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500178 self.command = d and d.getVar('OE_TERMINAL_CUSTOMCMD')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500179 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.warn('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
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500192def 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500200def 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:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500209 raise NoSupportedTerminals(get_cmd_list())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500210
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
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500219 # 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500249
250def check_tmux_pane_size(tmux):
251 import subprocess as sub
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500252 # 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500258 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
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500269
270 return size/2 >= 19
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500271
272def check_terminal_version(terminalName):
273 import subprocess as sub
274 try:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500275 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)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500281 out, err = p.communicate()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600282 ver_info = out.decode().rstrip().split('\n')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500283 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]
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500295 if ver.startswith('MATE Terminal'):
296 vernum = ver.split(' ')[-1]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500297 if ver.startswith('tmux'):
298 vernum = ver.split()[-1]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500299 return vernum
300
301def distro_name():
302 try:
303 p = Popen(['lsb_release', '-i'])
304 out, err = p.communicate()
305 distro = out.split(':')[1].strip().lower()
306 except:
307 distro = "unknown"
308 return distro