1import 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 def __init__(self, terms): 15 self.terms = terms 16 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 29class Terminal(Popen, metaclass=Registry): 30 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, 'cwd': os.getcwd() } 43 if isinstance(self.command, str): 44 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): 55 command = 'gnome-terminal -t "{title}" -x {command}' 56 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 65 XTerminal.__init__(self, sh_cmd, title, env, d) 66 67class Mate(XTerminal): 68 command = 'mate-terminal --disable-factory -t "{title}" -x {command}' 69 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): 80 command = 'konsole --separate --workdir . -p tabtitle="{title}" -e {command}' 81 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}' 89 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}' 92 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.warning(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 -c "{cwd}" "{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'): 131 raise UnsupportedTerminal('tmux pane too small or tmux < 1.9 version is being used') 132 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 -c "{cwd}" -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 -c "{cwd}" -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 -c "{{cwd}}" -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.warning(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): 178 self.command = d and d.getVar('OE_TERMINAL_CUSTOMCMD') 179 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.warning('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 192def 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 200def 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: 209 raise NoSupportedTerminals(get_cmd_list()) 210 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 219 # 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 249 250def check_tmux_pane_size(tmux): 251 import subprocess as sub 252 # 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 258 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 269 270 return size/2 >= 19 271 272def check_terminal_version(terminalName): 273 import subprocess as sub 274 try: 275 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) 281 out, err = p.communicate() 282 ver_info = out.decode().rstrip().split('\n') 283 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] 295 if ver.startswith('MATE Terminal'): 296 vernum = ver.split(' ')[-1] 297 if ver.startswith('tmux'): 298 vernum = ver.split()[-1] 299 if ver.startswith('tmux next-'): 300 vernum = ver.split()[-1][5:] 301 return vernum 302 303def distro_name(): 304 try: 305 p = Popen(['lsb_release', '-i']) 306 out, err = p.communicate() 307 distro = out.split(':')[1].strip().lower() 308 except: 309 distro = "unknown" 310 return distro 311