371 lines
11 KiB
Python
371 lines
11 KiB
Python
"""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 is not None:
|
|
alternatives = [using]
|
|
else:
|
|
alternatives = _tryorder
|
|
for browser in alternatives:
|
|
if '%s' in browser:
|
|
# 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 Galeon:
|
|
"""Launcher class for Galeon browsers."""
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.basename = os.path.basename(name)
|
|
|
|
def _remote(self, action, autoraise):
|
|
raise_opt = ("--noraise", "")[autoraise]
|
|
cmd = "%s %s %s >/dev/null 2>&1" % (self.name, raise_opt, action)
|
|
rc = os.system(cmd)
|
|
if rc:
|
|
import time
|
|
os.system("%s >/dev/null 2>&1 &" % 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("-w '%s'" % url, autoraise)
|
|
else:
|
|
self._remote("-n '%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):
|
|
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.
|
|
assert "'" not in url
|
|
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 = ["links", "lynx", "w3m"]
|
|
|
|
# Easy cases first -- register console browsers if we have them.
|
|
if os.environ.get("TERM"):
|
|
# The Links browser <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
|
|
if _iscommand("links"):
|
|
register("links", None, GenericBrowser("links '%s'"))
|
|
# The Lynx browser <http://lynx.browser.org/>
|
|
if _iscommand("lynx"):
|
|
register("lynx", None, GenericBrowser("lynx '%s'"))
|
|
# The w3m browser <http://ei5nazha.yz.yamagata-u.ac.jp/~aito/w3m/eng/>
|
|
if _iscommand("w3m"):
|
|
register("w3m", None, GenericBrowser("w3m '%s'"))
|
|
|
|
# X browsers have more in the way of options
|
|
if os.environ.get("DISPLAY"):
|
|
_tryorder = ["galeon", "skipstone",
|
|
"mozilla-firefox", "mozilla-firebird", "mozilla", "netscape",
|
|
"kfm", "grail"] + _tryorder
|
|
|
|
# First, the Netscape series
|
|
for browser in ("mozilla-firefox", "mozilla-firebird",
|
|
"mozilla", "netscape"):
|
|
if _iscommand(browser):
|
|
register(browser, None, Netscape(browser))
|
|
|
|
# Next, Mosaic -- old but still in use.
|
|
if _iscommand("mosaic"):
|
|
register("mosaic", None, GenericBrowser(
|
|
"mosaic '%s' >/dev/null &"))
|
|
|
|
# Gnome's Galeon
|
|
if _iscommand("galeon"):
|
|
register("galeon", None, Galeon("galeon"))
|
|
|
|
# Skipstone, another Gtk/Mozilla based browser
|
|
if _iscommand("skipstone"):
|
|
register("skipstone", None, GenericBrowser(
|
|
"skipstone '%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[0:0] = 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?
|