"""Interfaces for launching and remotely controlling Web browsers."""
# Maintained by Georg Brandl.
__all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
_browsers = {} # Dictionary of available browser controllers
_tryorder = [] # Preference order of available browsers
def register(name, klass, instance=None, update_tryorder=1):
"""Register a browser connector and, optionally, connection."""
_browsers[name.lower()] = [klass, instance]
elif update_tryorder < 0:
_tryorder.insert(0, name)
"""Return a browser launcher instance appropriate for the environment."""
for browser in alternatives:
# User gave us a command line, split it into name and args
browser = shlex.split(browser)
return BackgroundBrowser(browser[:-1])
return GenericBrowser(browser)
# User gave us a browser name or path.
command = _browsers[browser.lower()]
command = _synthesize(browser)
if command[1] is not None:
elif command[0] is not None:
raise Error("could not locate runnable browser")
# Please note: the following definition hides a builtin function.
# It is recommended one does "import webbrowser" and uses webbrowser.open(url)
# instead of "from webbrowser import *".
def open(url, new=0, autoraise=True):
if browser.open(url, new, autoraise):
def _synthesize(browser, update_tryorder=1):
"""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
If we can't create a controller in this way, or if there is no
executable for the requested browser, return [None, None].
name = os.path.basename(cmd)
command = _browsers[name.lower()]
# now attempt to clone to fit the new name:
if controller and name.lower() == controller.basename:
controller = copy.copy(controller)
controller.name = browser
controller.basename = os.path.basename(browser)
register(browser, None, controller, update_tryorder)
return [None, controller]
if sys.platform[:3] == "win":
if os.path.isfile(cmd) and cmd.endswith((".exe", ".bat")):
for ext in ".exe", ".bat":
if os.path.isfile(cmd + ext):
mode = os.stat(cmd)[stat.ST_MODE]
if mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH:
"""Return True if cmd is executable or can be found on the executable
path = os.environ.get("PATH")
for d in path.split(os.pathsep):
exe = os.path.join(d, cmd)
class BaseBrowser(object):
"""Parent class for all browsers. Do not use directly."""
def __init__(self, name=""):
def open(self, url, new=0, autoraise=True):
raise NotImplementedError
def open_new_tab(self, url):
class GenericBrowser(BaseBrowser):
"""Class for all browsers started with a command
and without remote functionality."""
def __init__(self, name):
if isinstance(name, basestring):
# name should be a list with arguments
self.basename = os.path.basename(self.name)
def open(self, url, new=0, autoraise=True):
cmdline = [self.name] + [arg.replace("%s", url)
if sys.platform[:3] == 'win':
p = subprocess.Popen(cmdline)
p = subprocess.Popen(cmdline, close_fds=True)
class BackgroundBrowser(GenericBrowser):
"""Class for all browsers which are to be started in the
def open(self, url, new=0, autoraise=True):
cmdline = [self.name] + [arg.replace("%s", url)
if sys.platform[:3] == 'win':
p = subprocess.Popen(cmdline)
setsid = getattr(os, 'setsid', None)
setsid = getattr(os, 'setpgrp', None)
p = subprocess.Popen(cmdline, close_fds=True, preexec_fn=setsid)
return (p.poll() is None)
class UnixBrowser(BaseBrowser):
"""Parent class for all Unix browsers with remote functionality."""
remote_args = ['%action', '%s']
remote_action_newwin = None
remote_action_newtab = None
def _invoke(self, args, remote, autoraise):
if remote and self.raise_opts:
# use autoraise argument only for remote invocation
autoraise = int(autoraise)
opt = self.raise_opts[autoraise]
if opt: raise_opt = [opt]
cmdline = [self.name] + raise_opt + args
if remote or self.background:
inout = file(os.devnull, "r+")
# for TTY browsers, we need stdin/out
# if possible, put browser in separate process group, so
# keyboard interrupts don't affect browser as well as Python
setsid = getattr(os, 'setsid', None)
setsid = getattr(os, 'setpgrp', None)
p = subprocess.Popen(cmdline, close_fds=True, stdin=inout,
stdout=(self.redirect_stdout and inout or None),
stderr=inout, preexec_fn=setsid)
# wait five seconds. If the subprocess is not finished, the
# remote invocation has (hopefully) started a new instance.
# if remote call failed, open() will try direct invocation
def open(self, url, new=0, autoraise=True):
action = self.remote_action
action = self.remote_action_newwin
if self.remote_action_newtab is None:
action = self.remote_action_newwin
action = self.remote_action_newtab
raise Error("Bad 'new' parameter to open(); " +
"expected 0, 1, or 2, got %s" % new)
args = [arg.replace("%s", url).replace("%action", action)
for arg in self.remote_args]
success = self._invoke(args, True, autoraise)
# remote invocation failed, try straight way
args = [arg.replace("%s", url) for arg in self.args]
return self._invoke(args, False, False)
class Mozilla(UnixBrowser):
"""Launcher class for Mozilla/Netscape browsers."""
raise_opts = ["-noraise", "-raise"]
remote_args = ['-remote', 'openURL(%s%action)']
remote_action_newwin = ",new-window"
remote_action_newtab = ",new-tab"
class Galeon(UnixBrowser):
"""Launcher class for Galeon/Epiphany browsers."""
raise_opts = ["-noraise", ""]
remote_args = ['%action', '%s']
remote_action_newwin = "-w"
class Chrome(UnixBrowser):
"Launcher class for Google Chrome browser."
remote_args = ['%action', '%s']
remote_action_newwin = "--new-window"
remote_action_newtab = ""
class Opera(UnixBrowser):
"Launcher class for Opera browser."
raise_opts = ["-noraise", ""]
remote_args = ['-remote', 'openURL(%s%action)']
remote_action_newwin = ",new-window"
remote_action_newtab = ",new-page"
class Elinks(UnixBrowser):
"Launcher class for Elinks browsers."
remote_args = ['-remote', 'openURL(%s%action)']
remote_action_newwin = ",new-window"
remote_action_newtab = ",new-tab"
# elinks doesn't like its stdout to be redirected -
# it uses redirected stdout as a signal to do -dump
class Konqueror(BaseBrowser):
"""Controller for the KDE File Manager (kfm, or Konqueror).
See the output of ``kfmclient --commands``
for more information on the Konqueror remote-control interface.
def open(self, url, new=0, autoraise=True):
# XXX Currently I know no way to prevent KFM from opening a new win.
devnull = file(os.devnull, "r+")
# if possible, put browser in separate process group, so
# keyboard interrupts don't affect browser as well as Python
setsid = getattr(os, 'setsid', None)
setsid = getattr(os, 'setpgrp', None)
p = subprocess.Popen(["kfmclient", action, url],
close_fds=True, stdin=devnull,
stdout=devnull, stderr=devnull)
# fall through to next variant
# kfmclient's return code unfortunately has no meaning as it seems
p = subprocess.Popen(["konqueror", "--silent", url],
close_fds=True, stdin=devnull,
stdout=devnull, stderr=devnull,
# fall through to next variant
p = subprocess.Popen(["kfm", "-d", url],
close_fds=True, stdin=devnull,
stdout=devnull, stderr=devnull,
return (p.poll() is None)
class Grail(BaseBrowser):
# 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):
tempdir = os.path.join(tempfile.gettempdir(),
user = pwd.getpwuid(os.getuid())[0]
filename = os.path.join(tempdir, user + "-*")
maybes = glob.glob(filename)
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
# need to PING each one until we find one that's live
# no good; attempt to clean it out, but don't fail:
def _remote(self, action):
s = self._find_grail_rc()
def open(self, url, new=0, autoraise=True):
ok = self._remote("LOADNEW " + url)
ok = self._remote("LOAD " + url)
# Platform support for Unix
# These are the right tests because all these Unix browsers require either
# a console terminal or an X display to run.
def register_X_browsers():
if _iscommand("xdg-open"):
register("xdg-open", None, BackgroundBrowser("xdg-open"))
# The default GNOME3 browser
if "GNOME_DESKTOP_SESSION_ID" in os.environ and _iscommand("gvfs-open"):
register("gvfs-open", None, BackgroundBrowser("gvfs-open"))
# The default GNOME browser
if "GNOME_DESKTOP_SESSION_ID" in os.environ and _iscommand("gnome-open"):
register("gnome-open", None, BackgroundBrowser("gnome-open"))
# The default KDE browser
if "KDE_FULL_SESSION" in os.environ and _iscommand("kfmclient"):
register("kfmclient", Konqueror, Konqueror("kfmclient"))
if _iscommand("x-www-browser"):
register("x-www-browser", None, BackgroundBrowser("x-www-browser"))
# The Mozilla/Netscape browsers
for browser in ("mozilla-firefox", "firefox",
"mozilla-firebird", "firebird",
"seamonkey", "mozilla", "netscape"):
register(browser, None, Mozilla(browser))
# Konqueror/kfm, the KDE browser.
register("kfm", Konqueror, Konqueror("kfm"))
elif _iscommand("konqueror"):
register("konqueror", Konqueror, Konqueror("konqueror"))
# Gnome's Galeon and Epiphany
for browser in ("galeon", "epiphany"):
register(browser, None, Galeon(browser))
# Skipstone, another Gtk/Mozilla based browser
if _iscommand("skipstone"):
register("skipstone", None, BackgroundBrowser("skipstone"))
# Google Chrome/Chromium browsers
for browser in ("google-chrome", "chrome", "chromium", "chromium-browser"):