webbrowser.py revision 8719ad5ddefadbc08b56a0af91515f050c89c678
1e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney#! /usr/bin/env python
2e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney"""Interfaces for launching and remotely controlling Web browsers."""
3e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney# Maintained by Georg Brandl.
4bf14e1077aa66ef1cb49bdaf06181de48bb2477fZeng, Star
5e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyimport io
6e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyimport os
7e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyimport shlex
8e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyimport sys
9e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyimport stat
10e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyimport subprocess
11e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyimport time
12e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
13e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney__all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
14e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
15e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyclass Error(Exception):
16e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    pass
17e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
183c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen_browsers = {}          # Dictionary of available browser controllers
193c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen_tryorder = []          # Preference order of available browsers
203c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen
213c447c2760b3438a6d5ff0a7f2dbd580526452e5jchendef register(name, klass, instance=None, update_tryorder=1):
223c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    """Register a browser connector and, optionally, connection."""
23e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    _browsers[name.lower()] = [klass, instance]
24e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    if update_tryorder > 0:
25e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        _tryorder.append(name)
26e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    elif update_tryorder < 0:
27e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        _tryorder.insert(0, name)
28e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
29e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneydef get(using=None):
30e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    """Return a browser launcher instance appropriate for the environment."""
31e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    if using is not None:
32e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        alternatives = [using]
33e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    else:
34e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        alternatives = _tryorder
35e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    for browser in alternatives:
36e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        if '%s' in browser:
373c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen            # User gave us a command line, split it into name and args
383c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen            browser = shlex.split(browser)
393c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen            if browser[-1] == '&':
403c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen                return BackgroundBrowser(browser[:-1])
41e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            else:
42e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                return GenericBrowser(browser)
43e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        else:
44e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            # User gave us a browser name or path.
45e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            try:
46e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                command = _browsers[browser.lower()]
47e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            except KeyError:
483c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen                command = _synthesize(browser)
493c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen            if command[1] is not None:
503c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen                return command[1]
513c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen            elif command[0] is not None:
523c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen                return command[0]()
533c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    raise Error("could not locate runnable browser")
543c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen
553c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen# Please note: the following definition hides a builtin function.
563c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen# It is recommended one does "import webbrowser" and uses webbrowser.open(url)
573c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen# instead of "from webbrowser import *".
583c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen
593c447c2760b3438a6d5ff0a7f2dbd580526452e5jchendef open(url, new=0, autoraise=True):
603c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    for name in _tryorder:
613c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen        browser = get(name)
622c0f06f0b80eebec5ec4a19d6d4440ba17bbcb0drsun        if browser.open(url, new, autoraise):
632c0f06f0b80eebec5ec4a19d6d4440ba17bbcb0drsun            return True
642c0f06f0b80eebec5ec4a19d6d4440ba17bbcb0drsun    return False
652c0f06f0b80eebec5ec4a19d6d4440ba17bbcb0drsun
662c0f06f0b80eebec5ec4a19d6d4440ba17bbcb0drsundef open_new(url):
672c0f06f0b80eebec5ec4a19d6d4440ba17bbcb0drsun    return open(url, 1)
682c0f06f0b80eebec5ec4a19d6d4440ba17bbcb0drsun
693c447c2760b3438a6d5ff0a7f2dbd580526452e5jchendef open_new_tab(url):
703c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    return open(url, 2)
713c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen
723c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen
733c447c2760b3438a6d5ff0a7f2dbd580526452e5jchendef _synthesize(browser, update_tryorder=1):
743c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    """Attempt to synthesize a controller base on existing controllers.
753c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen
763c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    This is useful to create a controller when a user specifies a path to
773c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    an entry in the BROWSER environment variable -- we can copy a general
783c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    controller to operate using a specific installation of the desired
793c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    browser in this way.
803c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen
813c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    If we can't create a controller in this way, or if there is no
823c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    executable for the requested browser, return [None, None].
833c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen
843c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    """
853c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    cmd = browser.split()[0]
863c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen    if not _iscommand(cmd):
87e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        return [None, None]
88e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    name = os.path.basename(cmd)
89e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    try:
90e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        command = _browsers[name.lower()]
91e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    except KeyError:
92e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        return [None, None]
93e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    # now attempt to clone to fit the new name:
94e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    controller = command[1]
95e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    if controller and name.lower() == controller.basename:
96e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        import copy
97e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        controller = copy.copy(controller)
983c447c2760b3438a6d5ff0a7f2dbd580526452e5jchen        controller.name = browser
99e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        controller.basename = os.path.basename(browser)
100e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        register(browser, None, controller, update_tryorder)
101e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        return [None, controller]
102e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    return [None, None]
103e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
104e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
105e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyif sys.platform[:3] == "win":
106e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    def _isexecutable(cmd):
107e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        cmd = cmd.lower()
108e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        if os.path.isfile(cmd) and cmd.endswith((".exe", ".bat")):
109e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            return True
110e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        for ext in ".exe", ".bat":
111e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            if os.path.isfile(cmd + ext):
112e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                return True
113e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        return False
114e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyelse:
115e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    def _isexecutable(cmd):
116e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        if os.path.isfile(cmd):
11784edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            mode = os.stat(cmd)[stat.ST_MODE]
11884edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            if mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH:
11984edd20bd0756ef5719835498d4283435d6b5e77Star Zeng                return True
120e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        return False
12152c0d06b94665def4977e13ea329dccb17f46da5hhuan
122e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneydef _iscommand(cmd):
123bf14e1077aa66ef1cb49bdaf06181de48bb2477fZeng, Star    """Return True if cmd is executable or can be found on the executable
124e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    search path."""
12584edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    if _isexecutable(cmd):
12684edd20bd0756ef5719835498d4283435d6b5e77Star Zeng        return True
1275b422a7bbd06015d76cdb41cf8c377f7a898efa9hhuan    path = os.environ.get("PATH")
128e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    if not path:
12984edd20bd0756ef5719835498d4283435d6b5e77Star Zeng        return False
130e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    for d in path.split(os.pathsep):
131e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        exe = os.path.join(d, cmd)
132e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        if _isexecutable(exe):
133e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            return True
134e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    return False
135e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
136e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
137e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney# General parent classes
138e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
139e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyclass BaseBrowser(object):
140e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    """Parent class for all browsers. Do not use directly."""
141e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
142e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    args = ['%s']
143e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
144e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    def __init__(self, name=""):
145e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        self.name = name
146e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        self.basename = name
147e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
148e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    def open(self, url, new=0, autoraise=True):
149e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        raise NotImplementedError
150e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
151e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    def open_new(self, url):
152e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        return self.open(url, 1)
153e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
154e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    def open_new_tab(self, url):
155e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        return self.open(url, 2)
156e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
157e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
158e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyclass GenericBrowser(BaseBrowser):
159e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    """Class for all browsers started with a command
160e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney       and without remote functionality."""
161e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
162e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    def __init__(self, name):
163e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        if isinstance(name, str):
164e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            self.name = name
165e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            self.args = ["%s"]
166e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        else:
167e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            # name should be a list with arguments
168e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            self.name = name[0]
169e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            self.args = name[1:]
170e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        self.basename = os.path.basename(self.name)
171fbe12b79aef4c2706e90078cc75b94dcf7926ba8ydong
172e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    def open(self, url, new=0, autoraise=True):
1735b422a7bbd06015d76cdb41cf8c377f7a898efa9hhuan        cmdline = [self.name] + [arg.replace("%s", url)
174e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                                 for arg in self.args]
175e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        try:
176e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            if sys.platform[:3] == 'win':
177e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                p = subprocess.Popen(cmdline)
178e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            else:
179e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                p = subprocess.Popen(cmdline, close_fds=True)
180e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            return not p.wait()
181e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        except OSError:
182e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            return False
183e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
184e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
185e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyclass BackgroundBrowser(GenericBrowser):
186e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    """Class for all browsers which are to be started in the
187e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney       background."""
188e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
189e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    def open(self, url, new=0, autoraise=True):
190e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        cmdline = [self.name] + [arg.replace("%s", url)
191e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                                 for arg in self.args]
192e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        try:
19384edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            if sys.platform[:3] == 'win':
194e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                p = subprocess.Popen(cmdline)
195e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            else:
196e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                setsid = getattr(os, 'setsid', None)
197e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                if not setsid:
198e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                    setsid = getattr(os, 'setpgrp', None)
199e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                p = subprocess.Popen(cmdline, close_fds=True, preexec_fn=setsid)
200e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            return (p.poll() is None)
201e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        except OSError:
202e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            return False
203e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
204e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
205e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyclass UnixBrowser(BaseBrowser):
206e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    """Parent class for all Unix browsers with remote functionality."""
207e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
208e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    raise_opts = None
209e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    remote_args = ['%action', '%s']
210e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    remote_action = None
211e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    remote_action_newwin = None
212e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    remote_action_newtab = None
21384edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    background = False
214e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    redirect_stdout = True
215e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
216e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    def _invoke(self, args, remote, autoraise):
217e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        raise_opt = []
218e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        if remote and self.raise_opts:
219e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            # use autoraise argument only for remote invocation
220e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            autoraise = int(autoraise)
221e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            opt = self.raise_opts[autoraise]
222e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            if opt: raise_opt = [opt]
223e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
224e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        cmdline = [self.name] + raise_opt + args
225e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
226fbe12b79aef4c2706e90078cc75b94dcf7926ba8ydong        if remote or self.background:
227e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            inout = io.open(os.devnull, "r+")
228e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        else:
229e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            # for TTY browsers, we need stdin/out
230e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            inout = None
231e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        # if possible, put browser in separate process group, so
232bf14e1077aa66ef1cb49bdaf06181de48bb2477fZeng, Star        # keyboard interrupts don't affect browser as well as Python
233bf14e1077aa66ef1cb49bdaf06181de48bb2477fZeng, Star        setsid = getattr(os, 'setsid', None)
234bf14e1077aa66ef1cb49bdaf06181de48bb2477fZeng, Star        if not setsid:
235e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            setsid = getattr(os, 'setpgrp', None)
236e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
237e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        p = subprocess.Popen(cmdline, close_fds=True, stdin=inout,
238e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                             stdout=(self.redirect_stdout and inout or None),
23984edd20bd0756ef5719835498d4283435d6b5e77Star Zeng                             stderr=inout, preexec_fn=setsid)
24084edd20bd0756ef5719835498d4283435d6b5e77Star Zeng        if remote:
24184edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            # wait five secons. If the subprocess is not finished, the
24284edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            # remote invocation has (hopefully) started a new instance.
24384edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            time.sleep(1)
24484edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            rc = p.poll()
24584edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            if rc is None:
24684edd20bd0756ef5719835498d4283435d6b5e77Star Zeng                time.sleep(4)
24784edd20bd0756ef5719835498d4283435d6b5e77Star Zeng                rc = p.poll()
24884edd20bd0756ef5719835498d4283435d6b5e77Star Zeng                if rc is None:
24984edd20bd0756ef5719835498d4283435d6b5e77Star Zeng                    return True
25084edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            # if remote call failed, open() will try direct invocation
25184edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            return not rc
25284edd20bd0756ef5719835498d4283435d6b5e77Star Zeng        elif self.background:
25384edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            if p.poll() is None:
25484edd20bd0756ef5719835498d4283435d6b5e77Star Zeng                return True
25584edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            else:
25684edd20bd0756ef5719835498d4283435d6b5e77Star Zeng                return False
25784edd20bd0756ef5719835498d4283435d6b5e77Star Zeng        else:
25884edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            return not p.wait()
25984edd20bd0756ef5719835498d4283435d6b5e77Star Zeng
26084edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    def open(self, url, new=0, autoraise=True):
26184edd20bd0756ef5719835498d4283435d6b5e77Star Zeng        if new == 0:
26284edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            action = self.remote_action
26384edd20bd0756ef5719835498d4283435d6b5e77Star Zeng        elif new == 1:
26484edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            action = self.remote_action_newwin
26584edd20bd0756ef5719835498d4283435d6b5e77Star Zeng        elif new == 2:
26684edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            if self.remote_action_newtab is None:
26784edd20bd0756ef5719835498d4283435d6b5e77Star Zeng                action = self.remote_action_newwin
26884edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            else:
269e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                action = self.remote_action_newtab
270e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        else:
271e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            raise Error("Bad 'new' parameter to open(); " +
272e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                        "expected 0, 1, or 2, got %s" % new)
273e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
274e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        args = [arg.replace("%s", url).replace("%action", action)
275e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney                for arg in self.remote_args]
276e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        success = self._invoke(args, True, autoraise)
277e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        if not success:
278e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            # remote invocation failed, try straight way
27984edd20bd0756ef5719835498d4283435d6b5e77Star Zeng            args = [arg.replace("%s", url) for arg in self.args]
280e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            return self._invoke(args, False, False)
281e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney        else:
282e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney            return True
283e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
284e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
285e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyclass Mozilla(UnixBrowser):
286e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    """Launcher class for Mozilla/Netscape browsers."""
287e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
288e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    raise_opts = ["-noraise", "-raise"]
289e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
290e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    remote_args = ['-remote', 'openURL(%s%action)']
291e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    remote_action = ""
292e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    remote_action_newwin = ",new-window"
293e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    remote_action_newtab = ",new-tab"
294e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
29584edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    background = True
296e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
297e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyNetscape = Mozilla
298e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
299e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney
300e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinneyclass Galeon(UnixBrowser):
301e42e94041f7c71a5e2e57154bd568f3c14fd6eecmdkinney    """Launcher class for Galeon/Epiphany browsers."""
30284edd20bd0756ef5719835498d4283435d6b5e77Star Zeng
30384edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    raise_opts = ["-noraise", ""]
30484edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    remote_args = ['%action', '%s']
30584edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    remote_action = "-n"
30684edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    remote_action_newwin = "-w"
30784edd20bd0756ef5719835498d4283435d6b5e77Star Zeng
30884edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    background = True
30984edd20bd0756ef5719835498d4283435d6b5e77Star Zeng
31084edd20bd0756ef5719835498d4283435d6b5e77Star Zeng
31184edd20bd0756ef5719835498d4283435d6b5e77Star Zengclass Opera(UnixBrowser):
31284edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    "Launcher class for Opera browser."
31384edd20bd0756ef5719835498d4283435d6b5e77Star Zeng
31484edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    raise_opts = ["", "-raise"]
31584edd20bd0756ef5719835498d4283435d6b5e77Star Zeng
31684edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    remote_args = ['-remote', 'openURL(%s%action)']
31784edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    remote_action = ""
31884edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    remote_action_newwin = ",new-window"
31984edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    remote_action_newtab = ",new-page"
32084edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    background = True
32184edd20bd0756ef5719835498d4283435d6b5e77Star Zeng
32284edd20bd0756ef5719835498d4283435d6b5e77Star Zeng
32384edd20bd0756ef5719835498d4283435d6b5e77Star Zengclass Elinks(UnixBrowser):
32484edd20bd0756ef5719835498d4283435d6b5e77Star Zeng    "Launcher class for Elinks browsers."
32584edd20bd0756ef5719835498d4283435d6b5e77Star Zeng
326    remote_args = ['-remote', 'openURL(%s%action)']
327    remote_action = ""
328    remote_action_newwin = ",new-window"
329    remote_action_newtab = ",new-tab"
330    background = False
331
332    # elinks doesn't like its stdout to be redirected -
333    # it uses redirected stdout as a signal to do -dump
334    redirect_stdout = False
335
336
337class Konqueror(BaseBrowser):
338    """Controller for the KDE File Manager (kfm, or Konqueror).
339
340    See the output of ``kfmclient --commands``
341    for more information on the Konqueror remote-control interface.
342    """
343
344    def open(self, url, new=0, autoraise=True):
345        # XXX Currently I know no way to prevent KFM from opening a new win.
346        if new == 2:
347            action = "newTab"
348        else:
349            action = "openURL"
350
351        devnull = io.open(os.devnull, "r+")
352        # if possible, put browser in separate process group, so
353        # keyboard interrupts don't affect browser as well as Python
354        setsid = getattr(os, 'setsid', None)
355        if not setsid:
356            setsid = getattr(os, 'setpgrp', None)
357
358        try:
359            p = subprocess.Popen(["kfmclient", action, url],
360                                 close_fds=True, stdin=devnull,
361                                 stdout=devnull, stderr=devnull)
362        except OSError:
363            # fall through to next variant
364            pass
365        else:
366            p.wait()
367            # kfmclient's return code unfortunately has no meaning as it seems
368            return True
369
370        try:
371            p = subprocess.Popen(["konqueror", "--silent", url],
372                                 close_fds=True, stdin=devnull,
373                                 stdout=devnull, stderr=devnull,
374                                 preexec_fn=setsid)
375        except OSError:
376            # fall through to next variant
377            pass
378        else:
379            if p.poll() is None:
380                # Should be running now.
381                return True
382
383        try:
384            p = subprocess.Popen(["kfm", "-d", url],
385                                 close_fds=True, stdin=devnull,
386                                 stdout=devnull, stderr=devnull,
387                                 preexec_fn=setsid)
388        except OSError:
389            return False
390        else:
391            return (p.poll() is None)
392
393
394class Grail(BaseBrowser):
395    # There should be a way to maintain a connection to Grail, but the
396    # Grail remote control protocol doesn't really allow that at this
397    # point.  It probably never will!
398    def _find_grail_rc(self):
399        import glob
400        import pwd
401        import socket
402        import tempfile
403        tempdir = os.path.join(tempfile.gettempdir(),
404                               ".grail-unix")
405        user = pwd.getpwuid(os.getuid())[0]
406        filename = os.path.join(tempdir, user + "-*")
407        maybes = glob.glob(filename)
408        if not maybes:
409            return None
410        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
411        for fn in maybes:
412            # need to PING each one until we find one that's live
413            try:
414                s.connect(fn)
415            except socket.error:
416                # no good; attempt to clean it out, but don't fail:
417                try:
418                    os.unlink(fn)
419                except IOError:
420                    pass
421            else:
422                return s
423
424    def _remote(self, action):
425        s = self._find_grail_rc()
426        if not s:
427            return 0
428        s.send(action)
429        s.close()
430        return 1
431
432    def open(self, url, new=0, autoraise=True):
433        if new:
434            ok = self._remote("LOADNEW " + url)
435        else:
436            ok = self._remote("LOAD " + url)
437        return ok
438
439
440#
441# Platform support for Unix
442#
443
444# These are the right tests because all these Unix browsers require either
445# a console terminal or an X display to run.
446
447def register_X_browsers():
448
449    # The default GNOME browser
450    if "GNOME_DESKTOP_SESSION_ID" in os.environ and _iscommand("gnome-open"):
451        register("gnome-open", None, BackgroundBrowser("gnome-open"))
452
453    # The default KDE browser
454    if "KDE_FULL_SESSION" in os.environ and _iscommand("kfmclient"):
455        register("kfmclient", Konqueror, Konqueror("kfmclient"))
456
457    # The Mozilla/Netscape browsers
458    for browser in ("mozilla-firefox", "firefox",
459                    "mozilla-firebird", "firebird",
460                    "seamonkey", "mozilla", "netscape"):
461        if _iscommand(browser):
462            register(browser, None, Mozilla(browser))
463
464    # Konqueror/kfm, the KDE browser.
465    if _iscommand("kfm"):
466        register("kfm", Konqueror, Konqueror("kfm"))
467    elif _iscommand("konqueror"):
468        register("konqueror", Konqueror, Konqueror("konqueror"))
469
470    # Gnome's Galeon and Epiphany
471    for browser in ("galeon", "epiphany"):
472        if _iscommand(browser):
473            register(browser, None, Galeon(browser))
474
475    # Skipstone, another Gtk/Mozilla based browser
476    if _iscommand("skipstone"):
477        register("skipstone", None, BackgroundBrowser("skipstone"))
478
479    # Opera, quite popular
480    if _iscommand("opera"):
481        register("opera", None, Opera("opera"))
482
483    # Next, Mosaic -- old but still in use.
484    if _iscommand("mosaic"):
485        register("mosaic", None, BackgroundBrowser("mosaic"))
486
487    # Grail, the Python browser. Does anybody still use it?
488    if _iscommand("grail"):
489        register("grail", Grail, None)
490
491# Prefer X browsers if present
492if os.environ.get("DISPLAY"):
493    register_X_browsers()
494
495# Also try console browsers
496if os.environ.get("TERM"):
497    # The Links/elinks browsers <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
498    if _iscommand("links"):
499        register("links", None, GenericBrowser("links"))
500    if _iscommand("elinks"):
501        register("elinks", None, Elinks("elinks"))
502    # The Lynx browser <http://lynx.isc.org/>, <http://lynx.browser.org/>
503    if _iscommand("lynx"):
504        register("lynx", None, GenericBrowser("lynx"))
505    # The w3m browser <http://w3m.sourceforge.net/>
506    if _iscommand("w3m"):
507        register("w3m", None, GenericBrowser("w3m"))
508
509#
510# Platform support for Windows
511#
512
513if sys.platform[:3] == "win":
514    class WindowsDefault(BaseBrowser):
515        def open(self, url, new=0, autoraise=True):
516            try:
517                os.startfile(url)
518            except WindowsError:
519                # [Error 22] No application is associated with the specified
520                # file for this operation: '<URL>'
521                return False
522            else:
523                return True
524
525    _tryorder = []
526    _browsers = {}
527
528    # First try to use the default Windows browser
529    register("windows-default", WindowsDefault)
530
531    # Detect some common Windows browsers, fallback to IE
532    iexplore = os.path.join(os.environ.get("PROGRAMFILES", "C:\\Program Files"),
533                            "Internet Explorer\\IEXPLORE.EXE")
534    for browser in ("firefox", "firebird", "seamonkey", "mozilla",
535                    "netscape", "opera", iexplore):
536        if _iscommand(browser):
537            register(browser, None, BackgroundBrowser(browser))
538
539#
540# Platform support for MacOS
541#
542
543try:
544    import ic
545except ImportError:
546    pass
547else:
548    class InternetConfig(BaseBrowser):
549        def open(self, url, new=0, autoraise=True):
550            ic.launchurl(url)
551            return True # Any way to get status?
552
553    register("internet-config", InternetConfig, update_tryorder=-1)
554
555if sys.platform == 'darwin':
556    # Adapted from patch submitted to SourceForge by Steven J. Burr
557    class MacOSX(BaseBrowser):
558        """Launcher class for Aqua browsers on Mac OS X
559
560        Optionally specify a browser name on instantiation.  Note that this
561        will not work for Aqua browsers if the user has moved the application
562        package after installation.
563
564        If no browser is specified, the default browser, as specified in the
565        Internet System Preferences panel, will be used.
566        """
567        def __init__(self, name):
568            self.name = name
569
570        def open(self, url, new=0, autoraise=True):
571            assert "'" not in url
572            # hack for local urls
573            if not ':' in url:
574                url = 'file:'+url
575
576            # new must be 0 or 1
577            new = int(bool(new))
578            if self.name == "default":
579                # User called open, open_new or get without a browser parameter
580                script = 'open location "%s"' % url.replace('"', '%22') # opens in default browser
581            else:
582                # User called get and chose a browser
583                if self.name == "OmniWeb":
584                    toWindow = ""
585                else:
586                    # Include toWindow parameter of OpenURL command for browsers
587                    # that support it.  0 == new window; -1 == existing
588                    toWindow = "toWindow %d" % (new - 1)
589                cmd = 'OpenURL "%s"' % url.replace('"', '%22')
590                script = '''tell application "%s"
591                                activate
592                                %s %s
593                            end tell''' % (self.name, cmd, toWindow)
594            # Open pipe to AppleScript through osascript command
595            osapipe = os.popen("osascript", "w")
596            if osapipe is None:
597                return False
598            # Write script to osascript's stdin
599            osapipe.write(script)
600            rc = osapipe.close()
601            return not rc
602
603    # Don't clear _tryorder or _browsers since OS X can use above Unix support
604    # (but we prefer using the OS X specific stuff)
605    register("MacOSX", None, MacOSX('default'), -1)
606
607
608#
609# Platform support for OS/2
610#
611
612if sys.platform[:3] == "os2" and _iscommand("netscape"):
613    _tryorder = []
614    _browsers = {}
615    register("os2netscape", None,
616             GenericBrowser(["start", "netscape", "%s"]), -1)
617
618
619# OK, now that we know what the default preference orders for each
620# platform are, allow user to override them with the BROWSER variable.
621if "BROWSER" in os.environ:
622    _userchoices = os.environ["BROWSER"].split(os.pathsep)
623    _userchoices.reverse()
624
625    # Treat choices in same way as if passed into get() but do register
626    # and prepend to _tryorder
627    for cmdline in _userchoices:
628        if cmdline != '':
629            cmd = _synthesize(cmdline, -1)
630            if cmd[1] is None:
631                register(cmdline, None, GenericBrowser(cmdline), -1)
632    cmdline = None # to make del work if _userchoices was empty
633    del cmdline
634    del _userchoices
635
636# what to do if _tryorder is now empty?
637
638
639def main():
640    import getopt
641    usage = """Usage: %s [-n | -t] url
642    -n: open new window
643    -t: open new tab""" % sys.argv[0]
644    try:
645        opts, args = getopt.getopt(sys.argv[1:], 'ntd')
646    except getopt.error as msg:
647        print(msg, file=sys.stderr)
648        print(usage, file=sys.stderr)
649        sys.exit(1)
650    new_win = 0
651    for o, a in opts:
652        if o == '-n': new_win = 1
653        elif o == '-t': new_win = 2
654    if len(args) != 1:
655        print(usage, file=sys.stderr)
656        sys.exit(1)
657
658    url = args[0]
659    open(url, new_win)
660
661    print("\a")
662
663if __name__ == "__main__":
664    main()
665