blob: 634daa90330759aa02c97cb80917a088d0ceb123 [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):
14 pass
15
16
17class Registry(oe.classutils.ClassRegistry):
18 command = None
19
20 def __init__(cls, name, bases, attrs):
21 super(Registry, cls).__init__(name.lower(), bases, attrs)
22
23 @property
24 def implemented(cls):
25 return bool(cls.command)
26
27
28class Terminal(Popen):
29 __metaclass__ = Registry
30
31 def __init__(self, sh_cmd, title=None, env=None, d=None):
32 fmt_sh_cmd = self.format_command(sh_cmd, title)
33 try:
34 Popen.__init__(self, fmt_sh_cmd, env=env)
35 except OSError as exc:
36 import errno
37 if exc.errno == errno.ENOENT:
38 raise UnsupportedTerminal(self.name)
39 else:
40 raise
41
42 def format_command(self, sh_cmd, title):
43 fmt = {'title': title or 'Terminal', 'command': sh_cmd}
44 if isinstance(self.command, basestring):
45 return shlex.split(self.command.format(**fmt))
46 else:
47 return [element.format(**fmt) for element in self.command]
48
49class XTerminal(Terminal):
50 def __init__(self, sh_cmd, title=None, env=None, d=None):
51 Terminal.__init__(self, sh_cmd, title, env, d)
52 if not os.environ.get('DISPLAY'):
53 raise UnsupportedTerminal(self.name)
54
55class Gnome(XTerminal):
56 command = 'gnome-terminal -t "{title}" --disable-factory -x {command}'
57 priority = 2
58
59 def __init__(self, sh_cmd, title=None, env=None, d=None):
60 # Recent versions of gnome-terminal does not support non-UTF8 charset:
61 # https://bugzilla.gnome.org/show_bug.cgi?id=732127; as a workaround,
62 # clearing the LC_ALL environment variable so it uses the locale.
63 # Once fixed on the gnome-terminal project, this should be removed.
64 if os.getenv('LC_ALL'): os.putenv('LC_ALL','')
65
66 # Check version
67 vernum = check_terminal_version("gnome-terminal")
68 if vernum and LooseVersion(vernum) >= '3.10':
69 logger.debug(1, 'Gnome-Terminal 3.10 or later does not support --disable-factory')
70 self.command = 'gnome-terminal -t "{title}" -x {command}'
71 XTerminal.__init__(self, sh_cmd, title, env, d)
72
73class Mate(XTerminal):
74 command = 'mate-terminal -t "{title}" -x {command}'
75 priority = 2
76
77class Xfce(XTerminal):
78 command = 'xfce4-terminal -T "{title}" -e "{command}"'
79 priority = 2
80
81class Terminology(XTerminal):
82 command = 'terminology -T="{title}" -e {command}'
83 priority = 2
84
85class Konsole(XTerminal):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050086 command = 'konsole --nofork --workdir . -p tabtitle="{title}" -e {command}'
Patrick Williamsc124f4f2015-09-15 14:41:29 -050087 priority = 2
88
89 def __init__(self, sh_cmd, title=None, env=None, d=None):
90 # Check version
91 vernum = check_terminal_version("konsole")
92 if vernum and LooseVersion(vernum) < '2.0.0':
93 # Konsole from KDE 3.x
94 self.command = 'konsole -T "{title}" -e {command}'
95 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:
118 logger.warn(msg)
119
120class TmuxRunning(Terminal):
121 """Open a new pane in the current running tmux window"""
122 name = 'tmux-running'
123 command = 'tmux split-window "{command}"'
124 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'
141 command = 'tmux new-window -n "{title}" "{command}"'
142 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"""
155 command = 'tmux new -d -s devshell -n devshell "{command}"'
156 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
166 self.command = 'tmux new -d -s {0} -n {0} "{{command}}"'.format(window_name)
167 Terminal.__init__(self, sh_cmd, title, env, d)
168
169 attach_cmd = 'tmux att -t {0}'.format(window_name)
170 msg = 'Tmux started. Please connect in another terminal with `tmux att -t {0}`'.format(window_name)
171 if d:
172 bb.event.fire(bb.event.LogExecTTY(msg, attach_cmd, 0.5, 10), d)
173 else:
174 logger.warn(msg)
175
176class Custom(Terminal):
177 command = 'false' # This is a placeholder
178 priority = 3
179
180 def __init__(self, sh_cmd, title=None, env=None, d=None):
181 self.command = d and d.getVar('OE_TERMINAL_CUSTOMCMD', True)
182 if self.command:
183 if not '{command}' in self.command:
184 self.command += ' {command}'
185 Terminal.__init__(self, sh_cmd, title, env, d)
186 logger.warn('Custom terminal was started.')
187 else:
188 logger.debug(1, 'No custom terminal (OE_TERMINAL_CUSTOMCMD) set')
189 raise UnsupportedTerminal('OE_TERMINAL_CUSTOMCMD not set')
190
191
192def prioritized():
193 return Registry.prioritized()
194
195def spawn_preferred(sh_cmd, title=None, env=None, d=None):
196 """Spawn the first supported terminal, by priority"""
197 for terminal in prioritized():
198 try:
199 spawn(terminal.name, sh_cmd, title, env, d)
200 break
201 except UnsupportedTerminal:
202 continue
203 else:
204 raise NoSupportedTerminals()
205
206def spawn(name, sh_cmd, title=None, env=None, d=None):
207 """Spawn the specified terminal, by name"""
208 logger.debug(1, 'Attempting to spawn terminal "%s"', name)
209 try:
210 terminal = Registry.registry[name]
211 except KeyError:
212 raise UnsupportedTerminal(name)
213
214 pipe = terminal(sh_cmd, title, env, d)
215 output = pipe.communicate()[0]
216 if pipe.returncode != 0:
217 raise ExecutionError(sh_cmd, pipe.returncode, output)
218
219def check_tmux_pane_size(tmux):
220 import subprocess as sub
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500221 # On older tmux versions (<1.9), return false. The reason
222 # is that there is no easy way to get the height of the active panel
223 # on current window without nested formats (available from version 1.9)
224 vernum = check_terminal_version("tmux")
225 if vernum and LooseVersion(vernum) < '1.9':
226 return False
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500227 try:
228 p = sub.Popen('%s list-panes -F "#{?pane_active,#{pane_height},}"' % tmux,
229 shell=True,stdout=sub.PIPE,stderr=sub.PIPE)
230 out, err = p.communicate()
231 size = int(out.strip())
232 except OSError as exc:
233 import errno
234 if exc.errno == errno.ENOENT:
235 return None
236 else:
237 raise
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500238
239 return size/2 >= 19
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500240
241def check_terminal_version(terminalName):
242 import subprocess as sub
243 try:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500244 cmdversion = '%s --version' % terminalName
245 if terminalName.startswith('tmux'):
246 cmdversion = '%s -V' % terminalName
247 newenv = os.environ.copy()
248 newenv["LANG"] = "C"
249 p = sub.Popen(['sh', '-c', cmdversion], stdout=sub.PIPE, stderr=sub.PIPE, env=newenv)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500250 out, err = p.communicate()
251 ver_info = out.rstrip().split('\n')
252 except OSError as exc:
253 import errno
254 if exc.errno == errno.ENOENT:
255 return None
256 else:
257 raise
258 vernum = None
259 for ver in ver_info:
260 if ver.startswith('Konsole'):
261 vernum = ver.split(' ')[-1]
262 if ver.startswith('GNOME Terminal'):
263 vernum = ver.split(' ')[-1]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500264 if ver.startswith('tmux'):
265 vernum = ver.split()[-1]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500266 return vernum
267
268def distro_name():
269 try:
270 p = Popen(['lsb_release', '-i'])
271 out, err = p.communicate()
272 distro = out.split(':')[1].strip().lower()
273 except:
274 distro = "unknown"
275 return distro