"""Interfaces for launching and remotely controlling Web browsers."""
import os
import sys
__all__ = ["Error", "open", "get", "register"]
class Error(Exception):
pass
_browsers = {} # Dictionary of available browser controllers
_tryorder = [] # Preference order of available browsers
def register(name, klass, instance=None):
"""Register a browser connector and, optionally, connection."""
_browsers[name.lower()] = [klass, instance]
def get(using=None):
"""Return a browser launcher instance appropriate for the environment."""
if using:
alternatives = [using]
else:
alternatives = _tryorder
for browser in alternatives:
if browser.find('%s') > -1:
# User gave us a command line, don't mess with it.
return GenericBrowser(browser)
else:
# User gave us a browser name.
try:
command = _browsers[browser.lower()]
except KeyError:
command = _synthesize(browser)
if command[1] is None:
return command[0]()
else:
return command[1]
raise Error("could not locate runnable browser")
# Please note: the following definition hides a builtin function.
def open(url, new=0, autoraise=1):
get().open(url, new, autoraise)
def open_new(url):
get().open(url, 1)
def _synthesize(browser):
"""Attempt to synthesize a controller base on existing controllers.
This is useful to create a controller when a user specifies a path to
an entry in the BROWSER environment variable -- we can copy a general
controller to operate using a specific installation of the desired
browser in this way.
If we can't create a controller in this way, or if there is no
executable for the requested browser, return [None, None].
"""
if not os.path.exists(browser):
return [None, None]
name = os.path.basename(browser)
try:
command = _browsers[name.lower()]
except KeyError:
return [None, None]
# now attempt to clone to fit the new name:
controller = command[1]
if controller and name.lower() == controller.basename:
import copy
controller = copy.copy(controller)
controller.name = browser
controller.basename = os.path.basename(browser)
register(browser, None, controller)
return [None, controller]
return [None, None]
def _iscommand(cmd):
"""Return True if cmd can be found on the executable search path."""
path = os.environ.get("PATH")
if not path:
return False
for d in path.split(os.pathsep):
exe = os.path.join(d, cmd)
if os.path.isfile(exe):
return True
return False
PROCESS_CREATION_DELAY = 4
class GenericBrowser:
def __init__(self, cmd):
self.name, self.args = cmd.split(None, 1)
self.basename = os.path.basename(self.name)
def open(self, url, new=0, autoraise=1):
assert "'" not in url
command = "%s %s" % (self.name, self.args)
os.system(command % url)
def open_new(self, url):
self.open(url)
class Netscape:
"Launcher class for Netscape browsers."
def __init__(self, name):
self.name = name
self.basename = os.path.basename(name)
def _remote(self, action, autoraise):
raise_opt = ("-noraise", "-raise")[autoraise]
cmd = "%s %s -remote '%s' >/dev/null 2>&1" % (self.name,
raise_opt,
action)
rc = os.system(cmd)
if rc:
import time
os.system("%s &" % self.name)
time.sleep(PROCESS_CREATION_DELAY)
rc = os.system(cmd)
return not rc
def open(self, url, new=0, autoraise=1):
if new:
self._remote("openURL(%s, new-window)"%url, autoraise)
else:
self._remote("openURL(%s)" % url, autoraise)
def open_new(self, url):
self.open(url, 1)
class Konqueror:
"""Controller for the KDE File Manager (kfm, or Konqueror).
See http://developer.kde.org/documentation/other/kfmclient.html
for more information on the Konqueror remote-control interface.
"""
def __init__(self):
if _iscommand("konqueror"):
self.name = self.basename = "konqueror"
else:
self.name = self.basename = "kfm"
def _remote(self, action):
assert "'" not in action
cmd = "kfmclient '%s' >/dev/null 2>&1" % action
rc = os.system(cmd)
if rc:
import time
if self.basename == "konqueror":
os.system(self.name + " --silent &")
else:
os.system(self.name + " -d &")
time.sleep(PROCESS_CREATION_DELAY)
rc = os.system(cmd)
return not rc
def open(self, url, new=1, autoraise=1):
# XXX Currently I know no way to prevent KFM from
# opening a new win.
self._remote("openURL '%s'" % url)
open_new = open
class Grail:
# There should be a way to maintain a connection to Grail, but the
# Grail remote control protocol doesn't really allow that at this
# point. It probably neverwill!
def _find_grail_rc(self):
import glob
import pwd
import socket
import tempfile
tempdir = os.path.join(tempfile.gettempdir(),
".grail-unix")
user = pwd.getpwuid(os.getuid())[0]
filename = os.path.join(tempdir, user + "-*")
maybes = glob.glob(filename)
if not maybes:
return None
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
for fn in maybes:
# need to PING each one until we find one that's live
try:
s.connect(fn)
except socket.error:
# no good; attempt to clean it out, but don't fail:
try:
os.unlink(fn)
except IOError:
pass
else:
return s
def _remote(self, action):
s = self._find_grail_rc()
if not s:
return 0
s.send(action)
s.close()
return 1
def open(self, url, new=0, autoraise=1):
if new:
self._remote("LOADNEW " + url)
else:
self._remote("LOAD " + url)
def open_new(self, url):
self.open(url, 1)
class WindowsDefault:
def open(self, url, new=0, autoraise=1):
os.startfile(url)
def open_new(self, url):
self.open(url)
#
# Platform support for Unix
#
# This is the right test because all these Unix browsers require either
# a console terminal of an X display to run. Note that we cannot split
# the TERM and DISPLAY cases, because we might be running Python from inside
# an xterm.
if os.environ.get("TERM") or os.environ.get("DISPLAY"):
_tryorder = ["mozilla","netscape","kfm","grail","links","lynx","w3m"]
# Easy cases first -- register console browsers if we have them.
if os.environ.get("TERM"):
# The Links browser
if _iscommand("links"):
register("links", None, GenericBrowser("links '%s'"))
# The Lynx browser
if _iscommand("lynx"):
register("lynx", None, GenericBrowser("lynx '%s'"))
# The w3m browser
if _iscommand("w3m"):
register("w3m", None, GenericBrowser("w3m '%s'"))
# X browsers have more in the way of options
if os.environ.get("DISPLAY"):
# First, the Netscape series
if _iscommand("mozilla"):
register("mozilla", None, Netscape("mozilla"))
if _iscommand("netscape"):
register("netscape", None, Netscape("netscape"))
# Next, Mosaic -- old but still in use.
if _iscommand("mosaic"):
register("mosaic", None, GenericBrowser(
"mosaic '%s' >/dev/null &"))
# Konqueror/kfm, the KDE browser.
if _iscommand("kfm") or _iscommand("konqueror"):
register("kfm", Konqueror, Konqueror())
# Grail, the Python browser.
if _iscommand("grail"):
register("grail", Grail, None)
class InternetConfig:
def open(self, url, new=0, autoraise=1):
ic.launchurl(url)
def open_new(self, url):
self.open(url)
#
# Platform support for Windows
#
if sys.platform[:3] == "win":
_tryorder = ["netscape", "windows-default"]
register("windows-default", WindowsDefault)
#
# Platform support for MacOS
#
try:
import ic
except ImportError:
pass
else:
# internet-config is the only supported controller on MacOS,
# so don't mess with the default!
_tryorder = ["internet-config"]
register("internet-config", InternetConfig)
#
# Platform support for OS/2
#
if sys.platform[:3] == "os2" and _iscommand("netscape.exe"):
_tryorder = ["os2netscape"]
register("os2netscape", None,
GenericBrowser("start netscape.exe %s"))
# OK, now that we know what the default preference orders for each
# platform are, allow user to override them with the BROWSER variable.
#
if "BROWSER" in os.environ:
# It's the user's responsibility to register handlers for any unknown
# browser referenced by this value, before calling open().
_tryorder = os.environ["BROWSER"].split(os.pathsep)
for cmd in _tryorder:
if not cmd.lower() in _browsers:
if _iscommand(cmd.lower()):
register(cmd.lower(), None, GenericBrowser(
"%s '%%s'" % cmd.lower()))
cmd = None # to make del work if _tryorder was empty
del cmd
_tryorder = filter(lambda x: x.lower() in _browsers
or x.find("%s") > -1, _tryorder)
# what to do if _tryorder is now empty?