| [3225] | 1 | #! /usr/bin/env python
|
|---|
| 2 | """Interfaces for launching and remotely controlling Web browsers."""
|
|---|
| 3 |
|
|---|
| 4 | import os
|
|---|
| 5 | import sys
|
|---|
| 6 | import stat
|
|---|
| 7 | import subprocess
|
|---|
| 8 | import time
|
|---|
| 9 |
|
|---|
| 10 | __all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
|
|---|
| 11 |
|
|---|
| 12 | class Error(Exception):
|
|---|
| 13 | pass
|
|---|
| 14 |
|
|---|
| 15 | _browsers = {} # Dictionary of available browser controllers
|
|---|
| 16 | _tryorder = [] # Preference order of available browsers
|
|---|
| 17 |
|
|---|
| 18 | def register(name, klass, instance=None, update_tryorder=1):
|
|---|
| 19 | """Register a browser connector and, optionally, connection."""
|
|---|
| 20 | _browsers[name.lower()] = [klass, instance]
|
|---|
| 21 | if update_tryorder > 0:
|
|---|
| 22 | _tryorder.append(name)
|
|---|
| 23 | elif update_tryorder < 0:
|
|---|
| 24 | _tryorder.insert(0, name)
|
|---|
| 25 |
|
|---|
| 26 | def get(using=None):
|
|---|
| 27 | """Return a browser launcher instance appropriate for the environment."""
|
|---|
| 28 | if using is not None:
|
|---|
| 29 | alternatives = [using]
|
|---|
| 30 | else:
|
|---|
| 31 | alternatives = _tryorder
|
|---|
| 32 | for browser in alternatives:
|
|---|
| 33 | if '%s' in browser:
|
|---|
| 34 | # User gave us a command line, split it into name and args
|
|---|
| 35 | return GenericBrowser(browser.split())
|
|---|
| 36 | else:
|
|---|
| 37 | # User gave us a browser name or path.
|
|---|
| 38 | try:
|
|---|
| 39 | command = _browsers[browser.lower()]
|
|---|
| 40 | except KeyError:
|
|---|
| 41 | command = _synthesize(browser)
|
|---|
| 42 | if command[1] is not None:
|
|---|
| 43 | return command[1]
|
|---|
| 44 | elif command[0] is not None:
|
|---|
| 45 | return command[0]()
|
|---|
| 46 | raise Error("could not locate runnable browser")
|
|---|
| 47 |
|
|---|
| 48 | # Please note: the following definition hides a builtin function.
|
|---|
| 49 | # It is recommended one does "import webbrowser" and uses webbrowser.open(url)
|
|---|
| 50 | # instead of "from webbrowser import *".
|
|---|
| 51 |
|
|---|
| 52 | def open(url, new=0, autoraise=1):
|
|---|
| 53 | for name in _tryorder:
|
|---|
| 54 | browser = get(name)
|
|---|
| 55 | if browser.open(url, new, autoraise):
|
|---|
| 56 | return True
|
|---|
| 57 | return False
|
|---|
| 58 |
|
|---|
| 59 | def open_new(url):
|
|---|
| 60 | return open(url, 1)
|
|---|
| 61 |
|
|---|
| 62 | def open_new_tab(url):
|
|---|
| 63 | return open(url, 2)
|
|---|
| 64 |
|
|---|
| 65 |
|
|---|
| 66 | def _synthesize(browser, update_tryorder=1):
|
|---|
| 67 | """Attempt to synthesize a controller base on existing controllers.
|
|---|
| 68 |
|
|---|
| 69 | This is useful to create a controller when a user specifies a path to
|
|---|
| 70 | an entry in the BROWSER environment variable -- we can copy a general
|
|---|
| 71 | controller to operate using a specific installation of the desired
|
|---|
| 72 | browser in this way.
|
|---|
| 73 |
|
|---|
| 74 | If we can't create a controller in this way, or if there is no
|
|---|
| 75 | executable for the requested browser, return [None, None].
|
|---|
| 76 |
|
|---|
| 77 | """
|
|---|
| 78 | cmd = browser.split()[0]
|
|---|
| 79 | if not _iscommand(cmd):
|
|---|
| 80 | return [None, None]
|
|---|
| 81 | name = os.path.basename(cmd)
|
|---|
| 82 | try:
|
|---|
| 83 | command = _browsers[name.lower()]
|
|---|
| 84 | except KeyError:
|
|---|
| 85 | return [None, None]
|
|---|
| 86 | # now attempt to clone to fit the new name:
|
|---|
| 87 | controller = command[1]
|
|---|
| 88 | if controller and name.lower() == controller.basename:
|
|---|
| 89 | import copy
|
|---|
| 90 | controller = copy.copy(controller)
|
|---|
| 91 | controller.name = browser
|
|---|
| 92 | controller.basename = os.path.basename(browser)
|
|---|
| 93 | register(browser, None, controller, update_tryorder)
|
|---|
| 94 | return [None, controller]
|
|---|
| 95 | return [None, None]
|
|---|
| 96 |
|
|---|
| 97 |
|
|---|
| 98 | if sys.platform[:3] == "win":
|
|---|
| 99 | def _isexecutable(cmd):
|
|---|
| 100 | cmd = cmd.lower()
|
|---|
| 101 | if os.path.isfile(cmd) and cmd.endswith((".exe", ".bat")):
|
|---|
| 102 | return True
|
|---|
| 103 | for ext in ".exe", ".bat":
|
|---|
| 104 | if os.path.isfile(cmd + ext):
|
|---|
| 105 | return True
|
|---|
| 106 | return False
|
|---|
| 107 | else:
|
|---|
| 108 | def _isexecutable(cmd):
|
|---|
| 109 | if os.path.isfile(cmd):
|
|---|
| 110 | mode = os.stat(cmd)[stat.ST_MODE]
|
|---|
| 111 | if mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH:
|
|---|
| 112 | return True
|
|---|
| 113 | return False
|
|---|
| 114 |
|
|---|
| 115 | def _iscommand(cmd):
|
|---|
| 116 | """Return True if cmd is executable or can be found on the executable
|
|---|
| 117 | search path."""
|
|---|
| 118 | if _isexecutable(cmd):
|
|---|
| 119 | return True
|
|---|
| 120 | path = os.environ.get("PATH")
|
|---|
| 121 | if not path:
|
|---|
| 122 | return False
|
|---|
| 123 | for d in path.split(os.pathsep):
|
|---|
| 124 | exe = os.path.join(d, cmd)
|
|---|
| 125 | if _isexecutable(exe):
|
|---|
| 126 | return True
|
|---|
| 127 | return False
|
|---|
| 128 |
|
|---|
| 129 |
|
|---|
| 130 | # General parent classes
|
|---|
| 131 |
|
|---|
| 132 | class BaseBrowser(object):
|
|---|
| 133 | """Parent class for all browsers. Do not use directly."""
|
|---|
| 134 |
|
|---|
| 135 | args = ['%s']
|
|---|
| 136 |
|
|---|
| 137 | def __init__(self, name=""):
|
|---|
| 138 | self.name = name
|
|---|
| 139 | self.basename = name
|
|---|
| 140 |
|
|---|
| 141 | def open(self, url, new=0, autoraise=1):
|
|---|
| 142 | raise NotImplementedError
|
|---|
| 143 |
|
|---|
| 144 | def open_new(self, url):
|
|---|
| 145 | return self.open(url, 1)
|
|---|
| 146 |
|
|---|
| 147 | def open_new_tab(self, url):
|
|---|
| 148 | return self.open(url, 2)
|
|---|
| 149 |
|
|---|
| 150 |
|
|---|
| 151 | class GenericBrowser(BaseBrowser):
|
|---|
| 152 | """Class for all browsers started with a command
|
|---|
| 153 | and without remote functionality."""
|
|---|
| 154 |
|
|---|
| 155 | def __init__(self, name):
|
|---|
| 156 | if isinstance(name, basestring):
|
|---|
| 157 | self.name = name
|
|---|
| 158 | else:
|
|---|
| 159 | # name should be a list with arguments
|
|---|
| 160 | self.name = name[0]
|
|---|
| 161 | self.args = name[1:]
|
|---|
| 162 | self.basename = os.path.basename(self.name)
|
|---|
| 163 |
|
|---|
| 164 | def open(self, url, new=0, autoraise=1):
|
|---|
| 165 | cmdline = [self.name] + [arg.replace("%s", url)
|
|---|
| 166 | for arg in self.args]
|
|---|
| 167 | try:
|
|---|
| 168 | p = subprocess.Popen(cmdline, close_fds=True)
|
|---|
| 169 | return not p.wait()
|
|---|
| 170 | except OSError:
|
|---|
| 171 | return False
|
|---|
| 172 |
|
|---|
| 173 |
|
|---|
| 174 | class BackgroundBrowser(GenericBrowser):
|
|---|
| 175 | """Class for all browsers which are to be started in the
|
|---|
| 176 | background."""
|
|---|
| 177 |
|
|---|
| 178 | def open(self, url, new=0, autoraise=1):
|
|---|
| 179 | cmdline = [self.name] + [arg.replace("%s", url)
|
|---|
| 180 | for arg in self.args]
|
|---|
| 181 | setsid = getattr(os, 'setsid', None)
|
|---|
| 182 | if not setsid:
|
|---|
| 183 | setsid = getattr(os, 'setpgrp', None)
|
|---|
| 184 | try:
|
|---|
| 185 | p = subprocess.Popen(cmdline, close_fds=True, preexec_fn=setsid)
|
|---|
| 186 | return (p.poll() is None)
|
|---|
| 187 | except OSError:
|
|---|
| 188 | return False
|
|---|
| 189 |
|
|---|
| 190 |
|
|---|
| 191 | class UnixBrowser(BaseBrowser):
|
|---|
| 192 | """Parent class for all Unix browsers with remote functionality."""
|
|---|
| 193 |
|
|---|
| 194 | raise_opts = None
|
|---|
| 195 | remote_args = ['%action', '%s']
|
|---|
| 196 | remote_action = None
|
|---|
| 197 | remote_action_newwin = None
|
|---|
| 198 | remote_action_newtab = None
|
|---|
| 199 | background = False
|
|---|
| 200 | redirect_stdout = True
|
|---|
| 201 |
|
|---|
| 202 | def _invoke(self, args, remote, autoraise):
|
|---|
| 203 | raise_opt = []
|
|---|
| 204 | if remote and self.raise_opts:
|
|---|
| 205 | # use autoraise argument only for remote invocation
|
|---|
| 206 | autoraise = int(bool(autoraise))
|
|---|
| 207 | opt = self.raise_opts[autoraise]
|
|---|
| 208 | if opt: raise_opt = [opt]
|
|---|
| 209 |
|
|---|
| 210 | cmdline = [self.name] + raise_opt + args
|
|---|
| 211 |
|
|---|
| 212 | if remote or self.background:
|
|---|
| 213 | inout = file(os.devnull, "r+")
|
|---|
| 214 | else:
|
|---|
| 215 | # for TTY browsers, we need stdin/out
|
|---|
| 216 | inout = None
|
|---|
| 217 | # if possible, put browser in separate process group, so
|
|---|
| 218 | # keyboard interrupts don't affect browser as well as Python
|
|---|
| 219 | setsid = getattr(os, 'setsid', None)
|
|---|
| 220 | if not setsid:
|
|---|
| 221 | setsid = getattr(os, 'setpgrp', None)
|
|---|
| 222 |
|
|---|
| 223 | p = subprocess.Popen(cmdline, close_fds=True, stdin=inout,
|
|---|
| 224 | stdout=(self.redirect_stdout and inout or None),
|
|---|
| 225 | stderr=inout, preexec_fn=setsid)
|
|---|
| 226 | if remote:
|
|---|
| 227 | # wait five secons. If the subprocess is not finished, the
|
|---|
| 228 | # remote invocation has (hopefully) started a new instance.
|
|---|
| 229 | time.sleep(1)
|
|---|
| 230 | rc = p.poll()
|
|---|
| 231 | if rc is None:
|
|---|
| 232 | time.sleep(4)
|
|---|
| 233 | rc = p.poll()
|
|---|
| 234 | if rc is None:
|
|---|
| 235 | return True
|
|---|
| 236 | # if remote call failed, open() will try direct invocation
|
|---|
| 237 | return not rc
|
|---|
| 238 | elif self.background:
|
|---|
| 239 | if p.poll() is None:
|
|---|
| 240 | return True
|
|---|
| 241 | else:
|
|---|
| 242 | return False
|
|---|
| 243 | else:
|
|---|
| 244 | return not p.wait()
|
|---|
| 245 |
|
|---|
| 246 | def open(self, url, new=0, autoraise=1):
|
|---|
| 247 | if new == 0:
|
|---|
| 248 | action = self.remote_action
|
|---|
| 249 | elif new == 1:
|
|---|
| 250 | action = self.remote_action_newwin
|
|---|
| 251 | elif new == 2:
|
|---|
| 252 | if self.remote_action_newtab is None:
|
|---|
| 253 | action = self.remote_action_newwin
|
|---|
| 254 | else:
|
|---|
| 255 | action = self.remote_action_newtab
|
|---|
| 256 | else:
|
|---|
| 257 | raise Error("Bad 'new' parameter to open(); " +
|
|---|
| 258 | "expected 0, 1, or 2, got %s" % new)
|
|---|
| 259 |
|
|---|
| 260 | args = [arg.replace("%s", url).replace("%action", action)
|
|---|
| 261 | for arg in self.remote_args]
|
|---|
| 262 | success = self._invoke(args, True, autoraise)
|
|---|
| 263 | if not success:
|
|---|
| 264 | # remote invocation failed, try straight way
|
|---|
| 265 | args = [arg.replace("%s", url) for arg in self.args]
|
|---|
| 266 | return self._invoke(args, False, False)
|
|---|
| 267 | else:
|
|---|
| 268 | return True
|
|---|
| 269 |
|
|---|
| 270 |
|
|---|
| 271 | class Mozilla(UnixBrowser):
|
|---|
| 272 | """Launcher class for Mozilla/Netscape browsers."""
|
|---|
| 273 |
|
|---|
| 274 | raise_opts = ["-noraise", "-raise"]
|
|---|
| 275 |
|
|---|
| 276 | remote_args = ['-remote', 'openURL(%s%action)']
|
|---|
| 277 | remote_action = ""
|
|---|
| 278 | remote_action_newwin = ",new-window"
|
|---|
| 279 | remote_action_newtab = ",new-tab"
|
|---|
| 280 |
|
|---|
| 281 | background = True
|
|---|
| 282 |
|
|---|
| 283 | Netscape = Mozilla
|
|---|
| 284 |
|
|---|
| 285 |
|
|---|
| 286 | class Galeon(UnixBrowser):
|
|---|
| 287 | """Launcher class for Galeon/Epiphany browsers."""
|
|---|
| 288 |
|
|---|
| 289 | raise_opts = ["-noraise", ""]
|
|---|
| 290 | remote_args = ['%action', '%s']
|
|---|
| 291 | remote_action = "-n"
|
|---|
| 292 | remote_action_newwin = "-w"
|
|---|
| 293 |
|
|---|
| 294 | background = True
|
|---|
| 295 |
|
|---|
| 296 |
|
|---|
| 297 | class Opera(UnixBrowser):
|
|---|
| 298 | "Launcher class for Opera browser."
|
|---|
| 299 |
|
|---|
| 300 | raise_opts = ["", "-raise"]
|
|---|
| 301 |
|
|---|
| 302 | remote_args = ['-remote', 'openURL(%s%action)']
|
|---|
| 303 | remote_action = ""
|
|---|
| 304 | remote_action_newwin = ",new-window"
|
|---|
| 305 | remote_action_newtab = ",new-page"
|
|---|
| 306 | background = True
|
|---|
| 307 |
|
|---|
| 308 |
|
|---|
| 309 | class Elinks(UnixBrowser):
|
|---|
| 310 | "Launcher class for Elinks browsers."
|
|---|
| 311 |
|
|---|
| 312 | remote_args = ['-remote', 'openURL(%s%action)']
|
|---|
| 313 | remote_action = ""
|
|---|
| 314 | remote_action_newwin = ",new-window"
|
|---|
| 315 | remote_action_newtab = ",new-tab"
|
|---|
| 316 | background = False
|
|---|
| 317 |
|
|---|
| 318 | # elinks doesn't like its stdout to be redirected -
|
|---|
| 319 | # it uses redirected stdout as a signal to do -dump
|
|---|
| 320 | redirect_stdout = False
|
|---|
| 321 |
|
|---|
| 322 |
|
|---|
| 323 | class Konqueror(BaseBrowser):
|
|---|
| 324 | """Controller for the KDE File Manager (kfm, or Konqueror).
|
|---|
| 325 |
|
|---|
| 326 | See the output of ``kfmclient --commands``
|
|---|
| 327 | for more information on the Konqueror remote-control interface.
|
|---|
| 328 | """
|
|---|
| 329 |
|
|---|
| 330 | def open(self, url, new=0, autoraise=1):
|
|---|
| 331 | # XXX Currently I know no way to prevent KFM from opening a new win.
|
|---|
| 332 | if new == 2:
|
|---|
| 333 | action = "newTab"
|
|---|
| 334 | else:
|
|---|
| 335 | action = "openURL"
|
|---|
| 336 |
|
|---|
| 337 | devnull = file(os.devnull, "r+")
|
|---|
| 338 | # if possible, put browser in separate process group, so
|
|---|
| 339 | # keyboard interrupts don't affect browser as well as Python
|
|---|
| 340 | setsid = getattr(os, 'setsid', None)
|
|---|
| 341 | if not setsid:
|
|---|
| 342 | setsid = getattr(os, 'setpgrp', None)
|
|---|
| 343 |
|
|---|
| 344 | try:
|
|---|
| 345 | p = subprocess.Popen(["kfmclient", action, url],
|
|---|
| |
|---|