Commit | Line | Data |
---|---|---|
86530b38 AT |
1 | """Interfaces for launching and remotely controlling Web browsers.""" |
2 | ||
3 | import os | |
4 | import sys | |
5 | ||
6 | __all__ = ["Error", "open", "get", "register"] | |
7 | ||
8 | class Error(Exception): | |
9 | pass | |
10 | ||
11 | _browsers = {} # Dictionary of available browser controllers | |
12 | _tryorder = [] # Preference order of available browsers | |
13 | ||
14 | def register(name, klass, instance=None): | |
15 | """Register a browser connector and, optionally, connection.""" | |
16 | _browsers[name.lower()] = [klass, instance] | |
17 | ||
18 | def get(using=None): | |
19 | """Return a browser launcher instance appropriate for the environment.""" | |
20 | if using is not None: | |
21 | alternatives = [using] | |
22 | else: | |
23 | alternatives = _tryorder | |
24 | for browser in alternatives: | |
25 | if '%s' in browser: | |
26 | # User gave us a command line, don't mess with it. | |
27 | return GenericBrowser(browser) | |
28 | else: | |
29 | # User gave us a browser name. | |
30 | try: | |
31 | command = _browsers[browser.lower()] | |
32 | except KeyError: | |
33 | command = _synthesize(browser) | |
34 | if command[1] is None: | |
35 | return command[0]() | |
36 | else: | |
37 | return command[1] | |
38 | raise Error("could not locate runnable browser") | |
39 | ||
40 | # Please note: the following definition hides a builtin function. | |
41 | ||
42 | def open(url, new=0, autoraise=1): | |
43 | get().open(url, new, autoraise) | |
44 | ||
45 | def open_new(url): | |
46 | get().open(url, 1) | |
47 | ||
48 | ||
49 | def _synthesize(browser): | |
50 | """Attempt to synthesize a controller base on existing controllers. | |
51 | ||
52 | This is useful to create a controller when a user specifies a path to | |
53 | an entry in the BROWSER environment variable -- we can copy a general | |
54 | controller to operate using a specific installation of the desired | |
55 | browser in this way. | |
56 | ||
57 | If we can't create a controller in this way, or if there is no | |
58 | executable for the requested browser, return [None, None]. | |
59 | ||
60 | """ | |
61 | if not os.path.exists(browser): | |
62 | return [None, None] | |
63 | name = os.path.basename(browser) | |
64 | try: | |
65 | command = _browsers[name.lower()] | |
66 | except KeyError: | |
67 | return [None, None] | |
68 | # now attempt to clone to fit the new name: | |
69 | controller = command[1] | |
70 | if controller and name.lower() == controller.basename: | |
71 | import copy | |
72 | controller = copy.copy(controller) | |
73 | controller.name = browser | |
74 | controller.basename = os.path.basename(browser) | |
75 | register(browser, None, controller) | |
76 | return [None, controller] | |
77 | return [None, None] | |
78 | ||
79 | ||
80 | def _iscommand(cmd): | |
81 | """Return True if cmd can be found on the executable search path.""" | |
82 | path = os.environ.get("PATH") | |
83 | if not path: | |
84 | return False | |
85 | for d in path.split(os.pathsep): | |
86 | exe = os.path.join(d, cmd) | |
87 | if os.path.isfile(exe): | |
88 | return True | |
89 | return False | |
90 | ||
91 | ||
92 | PROCESS_CREATION_DELAY = 4 | |
93 | ||
94 | ||
95 | class GenericBrowser: | |
96 | def __init__(self, cmd): | |
97 | self.name, self.args = cmd.split(None, 1) | |
98 | self.basename = os.path.basename(self.name) | |
99 | ||
100 | def open(self, url, new=0, autoraise=1): | |
101 | assert "'" not in url | |
102 | command = "%s %s" % (self.name, self.args) | |
103 | os.system(command % url) | |
104 | ||
105 | def open_new(self, url): | |
106 | self.open(url) | |
107 | ||
108 | ||
109 | class Netscape: | |
110 | "Launcher class for Netscape browsers." | |
111 | def __init__(self, name): | |
112 | self.name = name | |
113 | self.basename = os.path.basename(name) | |
114 | ||
115 | def _remote(self, action, autoraise): | |
116 | raise_opt = ("-noraise", "-raise")[autoraise] | |
117 | cmd = "%s %s -remote '%s' >/dev/null 2>&1" % (self.name, | |
118 | raise_opt, | |
119 | action) | |
120 | rc = os.system(cmd) | |
121 | if rc: | |
122 | import time | |
123 | os.system("%s &" % self.name) | |
124 | time.sleep(PROCESS_CREATION_DELAY) | |
125 | rc = os.system(cmd) | |
126 | return not rc | |
127 | ||
128 | def open(self, url, new=0, autoraise=1): | |
129 | if new: | |
130 | self._remote("openURL(%s,new-window)"%url, autoraise) | |
131 | else: | |
132 | self._remote("openURL(%s)" % url, autoraise) | |
133 | ||
134 | def open_new(self, url): | |
135 | self.open(url, 1) | |
136 | ||
137 | ||
138 | class Galeon: | |
139 | """Launcher class for Galeon browsers.""" | |
140 | def __init__(self, name): | |
141 | self.name = name | |
142 | self.basename = os.path.basename(name) | |
143 | ||
144 | def _remote(self, action, autoraise): | |
145 | raise_opt = ("--noraise", "")[autoraise] | |
146 | cmd = "%s %s %s >/dev/null 2>&1" % (self.name, raise_opt, action) | |
147 | rc = os.system(cmd) | |
148 | if rc: | |
149 | import time | |
150 | os.system("%s >/dev/null 2>&1 &" % self.name) | |
151 | time.sleep(PROCESS_CREATION_DELAY) | |
152 | rc = os.system(cmd) | |
153 | return not rc | |
154 | ||
155 | def open(self, url, new=0, autoraise=1): | |
156 | if new: | |
157 | self._remote("-w '%s'" % url, autoraise) | |
158 | else: | |
159 | self._remote("-n '%s'" % url, autoraise) | |
160 | ||
161 | def open_new(self, url): | |
162 | self.open(url, 1) | |
163 | ||
164 | ||
165 | class Konqueror: | |
166 | """Controller for the KDE File Manager (kfm, or Konqueror). | |
167 | ||
168 | See http://developer.kde.org/documentation/other/kfmclient.html | |
169 | for more information on the Konqueror remote-control interface. | |
170 | ||
171 | """ | |
172 | def __init__(self): | |
173 | if _iscommand("konqueror"): | |
174 | self.name = self.basename = "konqueror" | |
175 | else: | |
176 | self.name = self.basename = "kfm" | |
177 | ||
178 | def _remote(self, action): | |
179 | cmd = "kfmclient %s >/dev/null 2>&1" % action | |
180 | rc = os.system(cmd) | |
181 | if rc: | |
182 | import time | |
183 | if self.basename == "konqueror": | |
184 | os.system(self.name + " --silent &") | |
185 | else: | |
186 | os.system(self.name + " -d &") | |
187 | time.sleep(PROCESS_CREATION_DELAY) | |
188 | rc = os.system(cmd) | |
189 | return not rc | |
190 | ||
191 | def open(self, url, new=1, autoraise=1): | |
192 | # XXX Currently I know no way to prevent KFM from | |
193 | # opening a new win. | |
194 | assert "'" not in url | |
195 | self._remote("openURL '%s'" % url) | |
196 | ||
197 | open_new = open | |
198 | ||
199 | ||
200 | class Grail: | |
201 | # There should be a way to maintain a connection to Grail, but the | |
202 | # Grail remote control protocol doesn't really allow that at this | |
203 | # point. It probably neverwill! | |
204 | def _find_grail_rc(self): | |
205 | import glob | |
206 | import pwd | |
207 | import socket | |
208 | import tempfile | |
209 | tempdir = os.path.join(tempfile.gettempdir(), | |
210 | ".grail-unix") | |
211 | user = pwd.getpwuid(os.getuid())[0] | |
212 | filename = os.path.join(tempdir, user + "-*") | |
213 | maybes = glob.glob(filename) | |
214 | if not maybes: | |
215 | return None | |
216 | s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | |
217 | for fn in maybes: | |
218 | # need to PING each one until we find one that's live | |
219 | try: | |
220 | s.connect(fn) | |
221 | except socket.error: | |
222 | # no good; attempt to clean it out, but don't fail: | |
223 | try: | |
224 | os.unlink(fn) | |
225 | except IOError: | |
226 | pass | |
227 | else: | |
228 | return s | |
229 | ||
230 | def _remote(self, action): | |
231 | s = self._find_grail_rc() | |
232 | if not s: | |
233 | return 0 | |
234 | s.send(action) | |
235 | s.close() | |
236 | return 1 | |
237 | ||
238 | def open(self, url, new=0, autoraise=1): | |
239 | if new: | |
240 | self._remote("LOADNEW " + url) | |
241 | else: | |
242 | self._remote("LOAD " + url) | |
243 | ||
244 | def open_new(self, url): | |
245 | self.open(url, 1) | |
246 | ||
247 | ||
248 | class WindowsDefault: | |
249 | def open(self, url, new=0, autoraise=1): | |
250 | os.startfile(url) | |
251 | ||
252 | def open_new(self, url): | |
253 | self.open(url) | |
254 | ||
255 | # | |
256 | # Platform support for Unix | |
257 | # | |
258 | ||
259 | # This is the right test because all these Unix browsers require either | |
260 | # a console terminal of an X display to run. Note that we cannot split | |
261 | # the TERM and DISPLAY cases, because we might be running Python from inside | |
262 | # an xterm. | |
263 | if os.environ.get("TERM") or os.environ.get("DISPLAY"): | |
264 | _tryorder = ["links", "lynx", "w3m"] | |
265 | ||
266 | # Easy cases first -- register console browsers if we have them. | |
267 | if os.environ.get("TERM"): | |
268 | # The Links browser <http://artax.karlin.mff.cuni.cz/~mikulas/links/> | |
269 | if _iscommand("links"): | |
270 | register("links", None, GenericBrowser("links '%s'")) | |
271 | # The Lynx browser <http://lynx.browser.org/> | |
272 | if _iscommand("lynx"): | |
273 | register("lynx", None, GenericBrowser("lynx '%s'")) | |
274 | # The w3m browser <http://ei5nazha.yz.yamagata-u.ac.jp/~aito/w3m/eng/> | |
275 | if _iscommand("w3m"): | |
276 | register("w3m", None, GenericBrowser("w3m '%s'")) | |
277 | ||
278 | # X browsers have more in the way of options | |
279 | if os.environ.get("DISPLAY"): | |
280 | _tryorder = ["galeon", "skipstone", | |
281 | "mozilla-firefox", "mozilla-firebird", "mozilla", "netscape", | |
282 | "kfm", "grail"] + _tryorder | |
283 | ||
284 | # First, the Netscape series | |
285 | for browser in ("mozilla-firefox", "mozilla-firebird", | |
286 | "mozilla", "netscape"): | |
287 | if _iscommand(browser): | |
288 | register(browser, None, Netscape(browser)) | |
289 | ||
290 | # Next, Mosaic -- old but still in use. | |
291 | if _iscommand("mosaic"): | |
292 | register("mosaic", None, GenericBrowser( | |
293 | "mosaic '%s' >/dev/null &")) | |
294 | ||
295 | # Gnome's Galeon | |
296 | if _iscommand("galeon"): | |
297 | register("galeon", None, Galeon("galeon")) | |
298 | ||
299 | # Skipstone, another Gtk/Mozilla based browser | |
300 | if _iscommand("skipstone"): | |
301 | register("skipstone", None, GenericBrowser( | |
302 | "skipstone '%s' >/dev/null &")) | |
303 | ||
304 | # Konqueror/kfm, the KDE browser. | |
305 | if _iscommand("kfm") or _iscommand("konqueror"): | |
306 | register("kfm", Konqueror, Konqueror()) | |
307 | ||
308 | # Grail, the Python browser. | |
309 | if _iscommand("grail"): | |
310 | register("grail", Grail, None) | |
311 | ||
312 | ||
313 | class InternetConfig: | |
314 | def open(self, url, new=0, autoraise=1): | |
315 | ic.launchurl(url) | |
316 | ||
317 | def open_new(self, url): | |
318 | self.open(url) | |
319 | ||
320 | ||
321 | # | |
322 | # Platform support for Windows | |
323 | # | |
324 | ||
325 | if sys.platform[:3] == "win": | |
326 | _tryorder = ["netscape", "windows-default"] | |
327 | register("windows-default", WindowsDefault) | |
328 | ||
329 | # | |
330 | # Platform support for MacOS | |
331 | # | |
332 | ||
333 | try: | |
334 | import ic | |
335 | except ImportError: | |
336 | pass | |
337 | else: | |
338 | # internet-config is the only supported controller on MacOS, | |
339 | # so don't mess with the default! | |
340 | _tryorder = ["internet-config"] | |
341 | register("internet-config", InternetConfig) | |
342 | ||
343 | # | |
344 | # Platform support for OS/2 | |
345 | # | |
346 | ||
347 | if sys.platform[:3] == "os2" and _iscommand("netscape.exe"): | |
348 | _tryorder = ["os2netscape"] | |
349 | register("os2netscape", None, | |
350 | GenericBrowser("start netscape.exe %s")) | |
351 | ||
352 | # OK, now that we know what the default preference orders for each | |
353 | # platform are, allow user to override them with the BROWSER variable. | |
354 | # | |
355 | if "BROWSER" in os.environ: | |
356 | # It's the user's responsibility to register handlers for any unknown | |
357 | # browser referenced by this value, before calling open(). | |
358 | _tryorder[0:0] = os.environ["BROWSER"].split(os.pathsep) | |
359 | ||
360 | for cmd in _tryorder: | |
361 | if not cmd.lower() in _browsers: | |
362 | if _iscommand(cmd.lower()): | |
363 | register(cmd.lower(), None, GenericBrowser( | |
364 | "%s '%%s'" % cmd.lower())) | |
365 | cmd = None # to make del work if _tryorder was empty | |
366 | del cmd | |
367 | ||
368 | _tryorder = filter(lambda x: x.lower() in _browsers | |
369 | or x.find("%s") > -1, _tryorder) | |
370 | # what to do if _tryorder is now empty? |