webbrowser.py 11 KB

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