Rewrite webbrowser.py to fix various bugs following Ka-Ping Yee's

complaints.  The new version moves most of its initialization to
package load time; it's simpler, faster, smaller, and adds support for
Mozilla and Links.  Interpretation of the BROWSER variable now works
and is documented.  The open_new entry point and methods are marked
"deprecated; may be removed in 2.1".
This commit is contained in:
Eric S. Raymond 2001-01-23 13:16:32 +00:00
parent f87d857080
commit f7f185116a
2 changed files with 245 additions and 180 deletions

View File

@ -15,6 +15,15 @@ browsers will be used if graphical browsers are not available or an X11
display isn't available. If text-mode browsers are used, the calling
process will block until the user exits the browser.
Under \UNIX, if the environment variable \envvar{BROWSER} exists, it
is interpreted to override the platform default browser, as a
colon-separated list of browsers to try in order. When the value of
a list part contains the string \code{\%s}, then it is interpreted as
a literal browser command line to be used with the argument URL
substituted for the \code{\%s}; if the part does not contain,
\code{\%s}, it is simply interpreted as the name of the browser to
launch.
For non-\UNIX{} platforms, or when X11 browsers are available on
\UNIX, the controlling process will not wait for the user to finish
with the browser, but allow the browser to maintain its own window on
@ -35,11 +44,14 @@ The following functions are defined:
\begin{funcdesc}{open_new}{url}
Open \var{url} in a new window of the default browser, if possible,
otherwise, open \var{url} in the only browser window.
otherwise, open \var{url} in the only browser window. (This entry
point is deprecated and may be removed in 2.1.)
\end{funcdesc}
\begin{funcdesc}{get}{\optional{name}}
Return a controller object for the browser type \var{name}.
Return a controller object for the browser type \var{name}. If
\var{name} is empty, return a controller for a default browser
appriopriate
\end{funcdesc}
\begin{funcdesc}{register}{name, constructor\optional{, instance}}
@ -49,6 +61,10 @@ The following functions are defined:
\code{None}, \var{constructor} will be called without parameters to
create an instance when needed. If \var{instance} is provided,
\var{constructor} will never be called, and may be \code{None}.
This entry point is only useful if you plan to either set the
\envvar{BROWSER} variable or call \function{get} with a nonempty
argument matching the name of a handler you declare.
\end{funcdesc}
Several browser types are defined. This table gives the type names
@ -56,12 +72,16 @@ that may be passed to the \function{get()} function and the names of
the implementation classes, all defined in this module.
\begin{tableiii}{l|l|c}{code}{Type Name}{Class Name}{Notes}
\lineiii{'mozilla'}{\class{Mozilla}}{}
\lineiii{'netscape'}{\class{Netscape}}{}
\lineiii{'mosaic'}{\class{Mosaic}}{}
\lineiii{'kfm'}{\class{Konquerer}}{(1)}
\lineiii{'grail'}{\class{Grail}}{}
\lineiii{'links'}{\class{links}}{}
\lineiii{'lynx'}{\class{Lynx}}{}
\lineiii{'w3m'}{\class{w3m}}{}
\lineiii{'windows-default'}{\class{WindowsDefault}}{(2)}
\lineiii{'internet-config'}{\class{InternetConfig}}{(3)}
\lineiii{'command-line'}{\class{CommandLineBrowser}}{}
\end{tableiii}
\noindent
@ -98,5 +118,6 @@ module-level convenience functions:
\begin{funcdesc}{open_new}{url}
Open \var{url} in a new window of the browser handled by this
controller, if possible, otherwise, open \var{url} in the only
browser window.
browser window. (This method is deprecated and may be removed in
2.1.)
\end{funcdesc}

View File

@ -1,212 +1,239 @@
"""Remote-control interfaces to some browsers."""
"""Remote-control interfaces to common browsers."""
import os
import sys
PROCESS_CREATION_DELAY = 4
class Error(Exception):
pass
_browsers = {}
_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(name=None):
"""Retrieve a connection to a browser by type name, or the default
browser."""
name = name or DEFAULT_BROWSER
try:
L = _browsers[name.lower()]
except KeyError:
raise ValueError, "unknown browser type: " + `name`
if L[1] is None:
L[1] = L[0]()
return L[1]
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 browser
else:
# User gave us a browser name.
command = _browsers[browser.lower()]
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):
get().open(url, new)
def open_new(url): # Marked deprecated. May be removed in 2.1.
get().open(url, 1)
def open_new(url):
get().open_new(url)
#
# Everything after this point initializes _browsers and _tryorder,
# then disappears. Some class definitions and instances remain
# live through these globals, but only the minimum set needed to
# support the user's platform.
#
#
# Platform support for Unix
#
def _iscommand(cmd):
"""Return true if cmd can be found on the executable search path."""
path = os.environ.get("PATH")
if not path:
return 0
for d in path.split(os.pathsep):
exe = os.path.join(d, cmd)
if os.path.isfile(exe):
return 1
return 0
# 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"):
PROCESS_CREATION_DELAY = 4
global tryorder
_tryorder = ("mozilla","netscape","kfm","grail","links","lynx","w3m")
class CommandLineBrowser:
_browsers = []
if os.environ.get("DISPLAY"):
_browsers.extend([
("netscape", "netscape %s >/dev/null &"),
("mosaic", "mosaic %s >/dev/null &"),
])
_browsers.extend([
("lynx", "lynx %s"),
("w3m", "w3m %s"),
])
def open(self, url, new=0):
for exe, cmd in self._browsers:
if _iscommand(exe):
os.system(cmd % url)
return
raise Error("could not locate runnable browser")
def open_new(self, url):
self.open(url)
register("command-line", CommandLineBrowser)
class Netscape:
autoRaise = 1
def _remote(self, action):
raise_opt = ("-noraise", "-raise")[self.autoRaise]
cmd = "netscape %s -remote '%s' >/dev/null 2>&1" % (raise_opt, action)
rc = os.system(cmd)
if rc:
import time
os.system("netscape -no-about-splash &")
time.sleep(PROCESS_CREATION_DELAY)
rc = os.system(cmd)
return not rc
def open(self, url, new=0):
if new:
self.open_new(url)
else:
self._remote("openURL(%s)" % url)
def open_new(self, url):
self._remote("openURL(%s, new-window)" % url)
register("netscape", Netscape)
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 _remote(self, action):
cmd = "kfmclient %s >/dev/null 2>&1" % action
rc = os.system(cmd)
if rc:
import time
os.system("kfm -d &")
time.sleep(PROCESS_CREATION_DELAY)
rc = os.system(cmd)
return not rc
def open(self, url, new=1):
# XXX currently I know no way to prevent KFM from opening a new win.
self.open_new(url)
def open_new(self, url):
self._remote("openURL %s" % url)
register("kfm", Konqueror)
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 never will!
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:
def _iscommand(cmd):
"""Return true if cmd can be found on the executable search path."""
path = os.environ.get("PATH")
if not path:
return 0
s.send(action)
s.close()
return 1
for d in path.split(os.pathsep):
exe = os.path.join(d, cmd)
if os.path.isfile(exe):
return 1
return 0
def open(self, url, new=0):
if new:
self.open_new(url)
else:
self._remote("LOAD " + url)
class GenericBrowser:
def __init__(self, cmd):
self.command = cmd
def open_new(self, url):
self._remote("LOADNEW " + url)
def open(self, url, new=0):
os.system(self.command % url)
register("grail", Grail)
def open_new(self, url): # Deprecated. May be removed in 2.1.
self.open(url)
# 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"))
class WindowsDefault:
def open(self, url, new=0):
os.startfile(url)
# X browsers have mre in the way of options
if os.environ.get("DISPLAY"):
# First, the Netscape series
if _iscommand("netscape") or _iscommand("mozilla"):
class Netscape:
"Launcher class for Netscape browsers."
autoRaise = 1
def open_new(self, url):
self.open(url)
def __init__(self, name):
self.name = name
def _remote(self, action):
raise_opt = ("-noraise", "-raise")[self.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 -no-about-splash &" % self.name)
time.sleep(PROCESS_CREATION_DELAY)
rc = os.system(cmd)
return not rc
DEFAULT_BROWSER = "command-line"
def open(self, url, new=0):
if new:
self._remote("openURL(%s, new-window)" % url)
else:
self._remote("openURL(%s)" % url)
# Deprecated. May be removed in 2.1.
def open_new(self, url):
self.open(url, 1)
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"):
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 _remote(self, action):
cmd = "kfmclient %s >/dev/null 2>&1" % action
rc = os.system(cmd)
if rc:
import time
os.system("kfm -d &")
time.sleep(PROCESS_CREATION_DELAY)
rc = os.system(cmd)
return not rc
def open(self, url, new=1):
# XXX Currently I know no way to prevent KFM from opening a new win.
self._remote("openURL %s" % url)
# Deprecated. May be removed in 2.1.
open_new = open
register("kfm", Konqueror, None)
# Grail, the Python browser.
if _iscommand("grail"):
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):
if new:
self._remote("LOADNEW " + url)
else:
self._remote("LOAD " + url)
# Deprecated. May be removed in 2.1.
def open_new(self, url):
self.open(url, 1)
register("grail", Grail, None)
#
# Platform support for Windows
#
if sys.platform[:3] == "win":
del _browsers["kfm"]
global _tryorder
_tryorder = ("netscape", "windows-default")
class WindowsDefault:
def open(self, url, new=0):
os.startfile(url)
def open_new(self, url): # Deprecated. May be removed in 2.1.
self.open(url)
register("windows-default", WindowsDefault)
DEFAULT_BROWSER = "windows-default"
elif os.environ.get("DISPLAY"):
if _iscommand("netscape"):
DEFAULT_BROWSER = "netscape"
# If the $BROWSER environment variable is set and true, let that be
# the name of the browser to use:
#
DEFAULT_BROWSER = os.environ.get("BROWSER") or DEFAULT_BROWSER
# Now try to support the MacOS world. This is the only supported
# controller on that platform, so don't mess with the default!
# Platform support for MacOS
#
try:
import ic
@ -217,9 +244,26 @@ else:
def open(self, url, new=0):
ic.launchurl(url)
def open_new(self, url):
def open_new(self, url): # Deprecated. May be removed in 2.1.
self.open(url)
_browsers.clear()
# internet-config is the only supported controller on MacOS,
# so don't mess with the default!
_tryorder = ("internet-config")
register("internet-config", InternetConfig)
DEFAULT_BROWSER = "internet-config"
# OK, now that we know what the default preference orders for each
# platform are, allow user to override them with the BROWSER variable.
#
if os.environ.has_key("BROWSER"):
# 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(":")
else:
# Optimization: filter out alternatives that aren't available, so we can
# avoid has_key() tests at runtime. (This may also allow some unused
# classes and class-instance storage to be garbage-collected.)
_tryorder = filter(lambda x: _browsers.has_key(x.lower()) or x.find("%s")>-1,\
_tryorder)
# end