1#! /usr/bin/env python 2"""Interfaces for launching and remotely controlling Web browsers.""" 3# Maintained by Georg Brandl. 4 5import os 6import shlex 7import sys 8import stat 9import subprocess 10import time 11 12__all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"] 13 14class Error(Exception): 15 pass 16 17_browsers = {} # Dictionary of available browser controllers 18_tryorder = [] # Preference order of available browsers 19 20def register(name, klass, instance=None, update_tryorder=1): 21 """Register a browser connector and, optionally, connection.""" 22 _browsers[name.lower()] = [klass, instance] 23 if update_tryorder > 0: 24 _tryorder.append(name) 25 elif update_tryorder < 0: 26 _tryorder.insert(0, name) 27 28def get(using=None): 29 """Return a browser launcher instance appropriate for the environment.""" 30 if using is not None: 31 alternatives = [using] 32 else: 33 alternatives = _tryorder 34 for browser in alternatives: 35 if '%s' in browser: 36 # User gave us a command line, split it into name and args 37 browser = shlex.split(browser) 38 if browser[-1] == '&': 39 return BackgroundBrowser(browser[:-1]) 40 else: 41 return GenericBrowser(browser) 42 else: 43 # User gave us a browser name or path. 44 try: 45 command = _browsers[browser.lower()] 46 except KeyError: 47 command = _synthesize(browser) 48 if command[1] is not None: 49 return command[1] 50 elif command[0] is not None: 51 return command[0]() 52 raise Error("could not locate runnable browser") 53 54# Please note: the following definition hides a builtin function. 55# It is recommended one does "import webbrowser" and uses webbrowser.open(url) 56# instead of "from webbrowser import *". 57 58def open(url, new=0, autoraise=True): 59 for name in _tryorder: 60 browser = get(name) 61 if browser.open(url, new, autoraise): 62 return True 63 return False 64 65def open_new(url): 66 return open(url, 1) 67 68def open_new_tab(url): 69 return open(url, 2) 70 71 72def _synthesize(browser, update_tryorder=1): 73 """Attempt to synthesize a controller base on existing controllers. 74 75 This is useful to create a controller when a user specifies a path to 76 an entry in the BROWSER environment variable -- we can copy a general 77 controller to operate using a specific installation of the desired 78 browser in this way. 79 80 If we can't create a controller in this way, or if there is no 81 executable for the requested browser, return [None, None]. 82 83 """ 84 cmd = browser.split()[0] 85 if not _iscommand(cmd): 86 return [None, None] 87 name = os.path.basename(cmd) 88 try: 89 command = _browsers[name.lower()] 90 except KeyError: 91 return [None, None] 92 # now attempt to clone to fit the new name: 93 controller = command[1] 94 if controller and name.lower() == controller.basename: 95 import copy 96 controller = copy.copy(controller) 97 controller.name = browser 98 controller.basename = os.path.basename(browser) 99 register(browser, None, controller, update_tryorder) 100 return [None, controller] 101 return [None, None] 102 103 104if sys.platform[:3] == "win": 105 def _isexecutable(cmd): 106 cmd = cmd.lower() 107 if os.path.isfile(cmd) and cmd.endswith((".exe", ".bat")): 108 return True 109 for ext in ".exe", ".bat": 110 if os.path.isfile(cmd + ext): 111 return True 112 return False 113else: 114 def _isexecutable(cmd): 115 if os.path.isfile(cmd): 116 mode = os.stat(cmd)[stat.ST_MODE] 117 if mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH: 118 return True 119 return False 120 121def _iscommand(cmd): 122 """Return True if cmd is executable or can be found on the executable 123 search path.""" 124 if _isexecutable(cmd): 125 return True 126 path = os.environ.get("PATH") 127 if not path: 128 return False 129 for d in path.split(os.pathsep): 130 exe = os.path.join(d, cmd) 131 if _isexecutable(exe): 132 return True 133 return False 134 135 136# General parent classes 137 138class BaseBrowser(object): 139 """Parent class for all browsers. Do not use directly.""" 140 141 args = ['%s'] 142 143 def __init__(self, name=""): 144 self.name = name 145 self.basename = name 146 147 def open(self, url, new=0, autoraise=True): 148 raise NotImplementedError 149 150 def open_new(self, url): 151 return self.open(url, 1) 152 153 def open_new_tab(self, url): 154 return self.open(url, 2) 155 156 157class GenericBrowser(BaseBrowser): 158 """Class for all browsers started with a command 159 and without remote functionality.""" 160 161 def __init__(self, name): 162 if isinstance(name, basestring): 163 self.name = name 164 self.args = ["%s"] 165 else: 166 # name should be a list with arguments 167 self.name = name[0] 168 self.args = name[1:] 169 self.basename = os.path.basename(self.name) 170 171 def open(self, url, new=0, autoraise=True): 172 cmdline = [self.name] + [arg.replace("%s", url) 173 for arg in self.args] 174 try: 175 if sys.platform[:3] == 'win': 176 p = subprocess.Popen(cmdline) 177 else: 178 p = subprocess.Popen(cmdline, close_fds=True) 179 return not p.wait() 180 except OSError: 181 return False 182 183 184class BackgroundBrowser(GenericBrowser): 185 """Class for all browsers which are to be started in the 186 background.""" 187 188 def open(self, url, new=0, autoraise=True): 189 cmdline = [self.name] + [arg.replace("%s", url) 190 for arg in self.args] 191 try: 192 if sys.platform[:3] == 'win': 193 p = subprocess.Popen(cmdline) 194 else: 195 setsid = getattr(os, 'setsid', None) 196 if not setsid: 197 setsid = getattr(os, 'setpgrp', None) 198 p = subprocess.Popen(cmdline, close_fds=True, preexec_fn=setsid) 199 return (p.poll() is None) 200 except OSError: 201 return False 202 203 204class UnixBrowser(BaseBrowser): 205 """Parent class for all Unix browsers with remote functionality.""" 206 207 raise_opts = None 208 remote_args = ['%action', '%s'] 209 remote_action = None 210 remote_action_newwin = None 211 remote_action_newtab = None 212 background = False 213 redirect_stdout = True 214 215 def _invoke(self, args, remote, autoraise): 216 raise_opt = [] 217 if remote and self.raise_opts: 218 # use autoraise argument only for remote invocation 219 autoraise = int(autoraise) 220 opt = self.raise_opts[autoraise] 221 if opt: raise_opt = [opt] 222 223 cmdline = [self.name] + raise_opt + args 224 225 if remote or self.background: 226 inout = file(os.devnull, "r+") 227 else: 228 # for TTY browsers, we need stdin/out 229 inout = None 230 # if possible, put browser in separate process group, so 231 # keyboard interrupts don't affect browser as well as Python 232 setsid = getattr(os, 'setsid', None) 233 if not setsid: 234 setsid = getattr(os, 'setpgrp', None) 235 236 p = subprocess.Popen(cmdline, close_fds=True, stdin=inout, 237 stdout=(self.redirect_stdout and inout or None), 238 stderr=inout, preexec_fn=setsid) 239 if remote: 240 # wait five secons. If the subprocess is not finished, the 241 # remote invocation has (hopefully) started a new instance. 242 time.sleep(1) 243 rc = p.poll() 244 if rc is None: 245 time.sleep(4) 246 rc = p.poll() 247 if rc is None: 248 return True 249 # if remote call failed, open() will try direct invocation 250 return not rc 251 elif self.background: 252 if p.poll() is None: 253 return True 254 else: 255 return False 256 else: 257 return not p.wait() 258 259 def open(self, url, new=0, autoraise=True): 260 if new == 0: 261 action = self.remote_action 262 elif new == 1: 263 action = self.remote_action_newwin 264 elif new == 2: 265 if self.remote_action_newtab is None: 266 action = self.remote_action_newwin 267 else: 268 action = self.remote_action_newtab 269 else: 270 raise Error("Bad 'new' parameter to open(); " + 271 "expected 0, 1, or 2, got %s" % new) 272 273 args = [arg.replace("%s", url).replace("%action", action) 274 for arg in self.remote_args] 275 success = self._invoke(args, True, autoraise) 276 if not success: 277 # remote invocation failed, try straight way 278 args = [arg.replace("%s", url) for arg in self.args] 279 return self._invoke(args, False, False) 280 else: 281 return True 282 283 284class Mozilla(UnixBrowser): 285 """Launcher class for Mozilla/Netscape browsers.""" 286 287 raise_opts = ["-noraise", "-raise"] 288 remote_args = ['-remote', 'openURL(%s%action)'] 289 remote_action = "" 290 remote_action_newwin = ",new-window" 291 remote_action_newtab = ",new-tab" 292 background = True 293 294Netscape = Mozilla 295 296 297class Galeon(UnixBrowser): 298 """Launcher class for Galeon/Epiphany browsers.""" 299 300 raise_opts = ["-noraise", ""] 301 remote_args = ['%action', '%s'] 302 remote_action = "-n" 303 remote_action_newwin = "-w" 304 background = True 305 306 307class Opera(UnixBrowser): 308 "Launcher class for Opera browser." 309 310 raise_opts = ["-noraise", ""] 311 remote_args = ['-remote', 'openURL(%s%action)'] 312 remote_action = "" 313 remote_action_newwin = ",new-window" 314 remote_action_newtab = ",new-page" 315 background = True 316 317 318class Elinks(UnixBrowser): 319 "Launcher class for Elinks browsers." 320 321 remote_args = ['-remote', 'openURL(%s%action)'] 322 remote_action = "" 323 remote_action_newwin = ",new-window" 324 remote_action_newtab = ",new-tab" 325 background = False 326 327 # elinks doesn't like its stdout to be redirected - 328 # it uses redirected stdout as a signal to do -dump 329 redirect_stdout = False 330 331 332class Konqueror(BaseBrowser): 333 """Controller for the KDE File Manager (kfm, or Konqueror). 334 335 See the output of ``kfmclient --commands`` 336 for more information on the Konqueror remote-control interface. 337 """ 338 339 def open(self, url, new=0, autoraise=True): 340 # XXX Currently I know no way to prevent KFM from opening a new win. 341 if new == 2: 342 action = "newTab" 343 else: 344 action = "openURL" 345 346 devnull = file(os.devnull, "r+") 347 # if possible, put browser in separate process group, so 348 # keyboard interrupts don't affect browser as well as Python 349 setsid = getattr(os, 'setsid', None) 350 if not setsid: 351 setsid = getattr(os, 'setpgrp', None) 352 353 try: 354 p = subprocess.Popen(["kfmclient", action, url], 355 close_fds=True, stdin=devnull, 356 stdout=devnull, stderr=devnull) 357 except OSError: 358 # fall through to next variant 359 pass 360 else: 361 p.wait() 362 # kfmclient's return code unfortunately has no meaning as it seems 363 return True 364 365 try: 366 p = subprocess.Popen(["konqueror", "--silent", url], 367 close_fds=True, stdin=devnull, 368 stdout=devnull, stderr=devnull, 369 preexec_fn=setsid) 370 except OSError: 371 # fall through to next variant 372 pass 373 else: 374 if p.poll() is None: 375 # Should be running now. 376 return True 377 378 try: 379 p = subprocess.Popen(["kfm", "-d", url], 380 close_fds=True, stdin=devnull, 381 stdout=devnull, stderr=devnull, 382 preexec_fn=setsid) 383 except OSError: 384 return False 385 else: 386 return (p.poll() is None) 387 388 389class Grail(BaseBrowser): 390 # There should be a way to maintain a connection to Grail, but the 391 # Grail remote control protocol doesn't really allow that at this 392 # point. It probably never will! 393 def _find_grail_rc(self): 394 import glob 395 import pwd 396 import socket 397 import tempfile 398 tempdir = os.path.join(tempfile.gettempdir(), 399 ".grail-unix") 400 user = pwd.getpwuid(os.getuid())[0] 401 filename = os.path.join(tempdir, user + "-*") 402 maybes = glob.glob(filename) 403 if not maybes: 404 return None 405 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 406 for fn in maybes: 407 # need to PING each one until we find one that's live 408 try: 409 s.connect(fn) 410 except socket.error: 411 # no good; attempt to clean it out, but don't fail: 412 try: 413 os.unlink(fn) 414 except IOError: 415 pass 416 else: 417 return s 418 419 def _remote(self, action): 420 s = self._find_grail_rc() 421 if not s: 422 return 0 423 s.send(action) 424 s.close() 425 return 1 426 427 def open(self, url, new=0, autoraise=True): 428 if new: 429 ok = self._remote("LOADNEW " + url) 430 else: 431 ok = self._remote("LOAD " + url) 432 return ok 433 434 435# 436# Platform support for Unix 437# 438 439# These are the right tests because all these Unix browsers require either 440# a console terminal or an X display to run. 441 442def register_X_browsers(): 443 444 # The default GNOME browser 445 if "GNOME_DESKTOP_SESSION_ID" in os.environ and _iscommand("gnome-open"): 446 register("gnome-open", None, BackgroundBrowser("gnome-open")) 447 448 # The default KDE browser 449 if "KDE_FULL_SESSION" in os.environ and _iscommand("kfmclient"): 450 register("kfmclient", Konqueror, Konqueror("kfmclient")) 451 452 # The Mozilla/Netscape browsers 453 for browser in ("mozilla-firefox", "firefox", 454 "mozilla-firebird", "firebird", 455 "seamonkey", "mozilla", "netscape"): 456 if _iscommand(browser): 457 register(browser, None, Mozilla(browser)) 458 459 # Konqueror/kfm, the KDE browser. 460 if _iscommand("kfm"): 461 register("kfm", Konqueror, Konqueror("kfm")) 462 elif _iscommand("konqueror"): 463 register("konqueror", Konqueror, Konqueror("konqueror")) 464 465 # Gnome's Galeon and Epiphany 466 for browser in ("galeon", "epiphany"): 467 if _iscommand(browser): 468 register(browser, None, Galeon(browser)) 469 470 # Skipstone, another Gtk/Mozilla based browser 471 if _iscommand("skipstone"): 472 register("skipstone", None, BackgroundBrowser("skipstone")) 473 474 # Opera, quite popular 475 if _iscommand("opera"): 476 register("opera", None, Opera("opera")) 477 478 # Next, Mosaic -- old but still in use. 479 if _iscommand("mosaic"): 480 register("mosaic", None, BackgroundBrowser("mosaic")) 481 482 # Grail, the Python browser. Does anybody still use it? 483 if _iscommand("grail"): 484 register("grail", Grail, None) 485 486# Prefer X browsers if present 487if os.environ.get("DISPLAY"): 488 register_X_browsers() 489 490# Also try console browsers 491if os.environ.get("TERM"): 492 # The Links/elinks browsers <http://artax.karlin.mff.cuni.cz/~mikulas/links/> 493 if _iscommand("links"): 494 register("links", None, GenericBrowser("links")) 495 if _iscommand("elinks"): 496 register("elinks", None, Elinks("elinks")) 497 # The Lynx browser <http://lynx.isc.org/>, <http://lynx.browser.org/> 498 if _iscommand("lynx"): 499 register("lynx", None, GenericBrowser("lynx")) 500 # The w3m browser <http://w3m.sourceforge.net/> 501 if _iscommand("w3m"): 502 register("w3m", None, GenericBrowser("w3m")) 503 504# 505# Platform support for Windows 506# 507 508if sys.platform[:3] == "win": 509 class WindowsDefault(BaseBrowser): 510 def open(self, url, new=0, autoraise=True): 511 try: 512 os.startfile(url) 513 except WindowsError: 514 # [Error 22] No application is associated with the specified 515 # file for this operation: '<URL>' 516 return False 517 else: 518 return True 519 520 _tryorder = [] 521 _browsers = {} 522 523 # First try to use the default Windows browser 524 register("windows-default", WindowsDefault) 525 526 # Detect some common Windows browsers, fallback to IE 527 iexplore = os.path.join(os.environ.get("PROGRAMFILES", "C:\\Program Files"), 528 "Internet Explorer\\IEXPLORE.EXE") 529 for browser in ("firefox", "firebird", "seamonkey", "mozilla", 530 "netscape", "opera", iexplore): 531 if _iscommand(browser): 532 register(browser, None, BackgroundBrowser(browser)) 533 534# 535# Platform support for MacOS 536# 537 538if sys.platform == 'darwin': 539 # Adapted from patch submitted to SourceForge by Steven J. Burr 540 class MacOSX(BaseBrowser): 541 """Launcher class for Aqua browsers on Mac OS X 542 543 Optionally specify a browser name on instantiation. Note that this 544 will not work for Aqua browsers if the user has moved the application 545 package after installation. 546 547 If no browser is specified, the default browser, as specified in the 548 Internet System Preferences panel, will be used. 549 """ 550 def __init__(self, name): 551 self.name = name 552 553 def open(self, url, new=0, autoraise=True): 554 assert "'" not in url 555 # hack for local urls 556 if not ':' in url: 557 url = 'file:'+url 558 559 # new must be 0 or 1 560 new = int(bool(new)) 561 if self.name == "default": 562 # User called open, open_new or get without a browser parameter 563 script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser 564 else: 565 # User called get and chose a browser 566 if self.name == "OmniWeb": 567 toWindow = "" 568 else: 569 # Include toWindow parameter of OpenURL command for browsers 570 # that support it. 0 == new window; -1 == existing 571 toWindow = "toWindow %d" % (new - 1) 572 cmd = 'OpenURL "%s"' % url.replace('"', '%22') 573 script = '''tell application "%s" 574 activate 575 %s %s 576 end tell''' % (self.name, cmd, toWindow) 577 # Open pipe to AppleScript through osascript command 578 osapipe = os.popen("osascript", "w") 579 if osapipe is None: 580 return False 581 # Write script to osascript's stdin 582 osapipe.write(script) 583 rc = osapipe.close() 584 return not rc 585 586 class MacOSXOSAScript(BaseBrowser): 587 def __init__(self, name): 588 self._name = name 589 590 def open(self, url, new=0, autoraise=True): 591 if self._name == 'default': 592 script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser 593 else: 594 script = ''' 595 tell application "%s" 596 activate 597 open location "%s" 598 end 599 '''%(self._name, url.replace('"', '%22')) 600 601 osapipe = os.popen("osascript", "w") 602 if osapipe is None: 603 return False 604 605 osapipe.write(script) 606 rc = osapipe.close() 607 return not rc 608 609 610 # Don't clear _tryorder or _browsers since OS X can use above Unix support 611 # (but we prefer using the OS X specific stuff) 612 register("safari", None, MacOSXOSAScript('safari'), -1) 613 register("firefox", None, MacOSXOSAScript('firefox'), -1) 614 register("MacOSX", None, MacOSXOSAScript('default'), -1) 615 616 617# 618# Platform support for OS/2 619# 620 621if sys.platform[:3] == "os2" and _iscommand("netscape"): 622 _tryorder = [] 623 _browsers = {} 624 register("os2netscape", None, 625 GenericBrowser(["start", "netscape", "%s"]), -1) 626 627 628# OK, now that we know what the default preference orders for each 629# platform are, allow user to override them with the BROWSER variable. 630if "BROWSER" in os.environ: 631 _userchoices = os.environ["BROWSER"].split(os.pathsep) 632 _userchoices.reverse() 633 634 # Treat choices in same way as if passed into get() but do register 635 # and prepend to _tryorder 636 for cmdline in _userchoices: 637 if cmdline != '': 638 cmd = _synthesize(cmdline, -1) 639 if cmd[1] is None: 640 register(cmdline, None, GenericBrowser(cmdline), -1) 641 cmdline = None # to make del work if _userchoices was empty 642 del cmdline 643 del _userchoices 644 645# what to do if _tryorder is now empty? 646 647 648def main(): 649 import getopt 650 usage = """Usage: %s [-n | -t] url 651 -n: open new window 652 -t: open new tab""" % sys.argv[0] 653 try: 654 opts, args = getopt.getopt(sys.argv[1:], 'ntd') 655 except getopt.error, msg: 656 print >>sys.stderr, msg 657 print >>sys.stderr, usage 658 sys.exit(1) 659 new_win = 0 660 for o, a in opts: 661 if o == '-n': new_win = 1 662 elif o == '-t': new_win = 2 663 if len(args) != 1: 664 print >>sys.stderr, usage 665 sys.exit(1) 666 667 url = args[0] 668 open(url, new_win) 669 670 print "\a" 671 672if __name__ == "__main__": 673 main() 674