Home | History | Annotate | Download | only in Lib
      1 #! /usr/bin/env python

      2 """Interfaces for launching and remotely controlling Web browsers."""
      3 # Maintained by Georg Brandl.

      4 
      5 import os
      6 import shlex
      7 import sys
      8 import stat
      9 import subprocess
     10 import time
     11 
     12 __all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
     13 
     14 class Error(Exception):
     15     pass
     16 
     17 _browsers = {}          # Dictionary of available browser controllers

     18 _tryorder = []          # Preference order of available browsers

     19 
     20 def 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 
     28 def 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 
     58 def 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 
     65 def open_new(url):
     66     return open(url, 1)
     67 
     68 def open_new_tab(url):
     69     return open(url, 2)
     70 
     71 
     72 def _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 
    104 if 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
    113 else:
    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 
    121 def _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 
    138 class 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 
    157 class 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 
    184 class 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 
    204 class 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 
    284 class 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 
    294 Netscape = Mozilla
    295 
    296 
    297 class 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 
    307 class 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 
    318 class 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 
    332 class 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 
    389 class 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 
    442 def 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

    487 if os.environ.get("DISPLAY"):
    488     register_X_browsers()
    489 
    490 # Also try console browsers

    491 if 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 
    508 if 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 
    538 if 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 
    621 if 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.

    630 if "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 
    648 def 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 
    672 if __name__ == "__main__":
    673     main()
    674