__init__.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. # This Source Code Form is subject to the terms of the Mozilla Public
  2. # License, v. 2.0. If a copy of the MPL was not distributed with this
  3. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  4. import os
  5. import sys
  6. import copy
  7. import tempfile
  8. import signal
  9. import commands
  10. import zipfile
  11. import optparse
  12. import killableprocess
  13. import subprocess
  14. import platform
  15. import shutil
  16. from StringIO import StringIO
  17. from xml.dom import minidom
  18. from distutils import dir_util
  19. from time import sleep
  20. # conditional (version-dependent) imports
  21. try:
  22. import simplejson
  23. except ImportError:
  24. import json as simplejson
  25. import logging
  26. logger = logging.getLogger(__name__)
  27. # Use dir_util for copy/rm operations because shutil is all kinds of broken
  28. copytree = dir_util.copy_tree
  29. rmtree = dir_util.remove_tree
  30. def findInPath(fileName, path=os.environ['PATH']):
  31. dirs = path.split(os.pathsep)
  32. for dir in dirs:
  33. if os.path.isfile(os.path.join(dir, fileName)):
  34. return os.path.join(dir, fileName)
  35. if os.name == 'nt' or sys.platform == 'cygwin':
  36. if os.path.isfile(os.path.join(dir, fileName + ".exe")):
  37. return os.path.join(dir, fileName + ".exe")
  38. return None
  39. stdout = sys.stdout
  40. stderr = sys.stderr
  41. stdin = sys.stdin
  42. def run_command(cmd, env=None, **kwargs):
  43. """Run the given command in killable process."""
  44. killable_kwargs = {'stdout':stdout ,'stderr':stderr, 'stdin':stdin}
  45. killable_kwargs.update(kwargs)
  46. if sys.platform != "win32":
  47. return killableprocess.Popen(cmd, preexec_fn=lambda : os.setpgid(0, 0),
  48. env=env, **killable_kwargs)
  49. else:
  50. return killableprocess.Popen(cmd, env=env, **killable_kwargs)
  51. def getoutput(l):
  52. tmp = tempfile.mktemp()
  53. x = open(tmp, 'w')
  54. subprocess.call(l, stdout=x, stderr=x)
  55. x.close(); x = open(tmp, 'r')
  56. r = x.read() ; x.close()
  57. os.remove(tmp)
  58. return r
  59. def get_pids(name, minimun_pid=0):
  60. """Get all the pids matching name, exclude any pids below minimum_pid."""
  61. if os.name == 'nt' or sys.platform == 'cygwin':
  62. import wpk
  63. pids = wpk.get_pids(name)
  64. else:
  65. data = getoutput(['ps', 'ax']).splitlines()
  66. pids = [int(line.split()[0]) for line in data if line.find(name) is not -1]
  67. matching_pids = [m for m in pids if m > minimun_pid]
  68. return matching_pids
  69. def makedirs(name):
  70. head, tail = os.path.split(name)
  71. if not tail:
  72. head, tail = os.path.split(head)
  73. if head and tail and not os.path.exists(head):
  74. try:
  75. makedirs(head)
  76. except OSError, e:
  77. pass
  78. if tail == os.curdir: # xxx/newdir/. exists if xxx/newdir exists
  79. return
  80. try:
  81. os.mkdir(name)
  82. except:
  83. pass
  84. # addon_details() copied from mozprofile
  85. def addon_details(install_rdf_fh):
  86. """
  87. returns a dictionary of details about the addon
  88. - addon_path : path to the addon directory
  89. Returns:
  90. {'id': u'rainbow@colors.org', # id of the addon
  91. 'version': u'1.4', # version of the addon
  92. 'name': u'Rainbow', # name of the addon
  93. 'unpack': # whether to unpack the addon
  94. """
  95. details = {
  96. 'id': None,
  97. 'unpack': False,
  98. 'name': None,
  99. 'version': None
  100. }
  101. def get_namespace_id(doc, url):
  102. attributes = doc.documentElement.attributes
  103. namespace = ""
  104. for i in range(attributes.length):
  105. if attributes.item(i).value == url:
  106. if ":" in attributes.item(i).name:
  107. # If the namespace is not the default one remove 'xlmns:'
  108. namespace = attributes.item(i).name.split(':')[1] + ":"
  109. break
  110. return namespace
  111. def get_text(element):
  112. """Retrieve the text value of a given node"""
  113. rc = []
  114. for node in element.childNodes:
  115. if node.nodeType == node.TEXT_NODE:
  116. rc.append(node.data)
  117. return ''.join(rc).strip()
  118. doc = minidom.parse(install_rdf_fh)
  119. # Get the namespaces abbreviations
  120. em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#")
  121. rdf = get_namespace_id(doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
  122. description = doc.getElementsByTagName(rdf + "Description").item(0)
  123. for node in description.childNodes:
  124. # Remove the namespace prefix from the tag for comparison
  125. entry = node.nodeName.replace(em, "")
  126. if entry in details.keys():
  127. details.update({ entry: get_text(node) })
  128. # turn unpack into a true/false value
  129. if isinstance(details['unpack'], basestring):
  130. details['unpack'] = details['unpack'].lower() == 'true'
  131. return details
  132. class Profile(object):
  133. """Handles all operations regarding profile. Created new profiles, installs extensions,
  134. sets preferences and handles cleanup."""
  135. def __init__(self, binary=None, profile=None, addons=None,
  136. preferences=None):
  137. self.binary = binary
  138. self.create_new = not(bool(profile))
  139. if profile:
  140. self.profile = profile
  141. else:
  142. self.profile = self.create_new_profile(self.binary)
  143. self.addons_installed = []
  144. self.addons = addons or []
  145. ### set preferences from class preferences
  146. preferences = preferences or {}
  147. if hasattr(self.__class__, 'preferences'):
  148. self.preferences = self.__class__.preferences.copy()
  149. else:
  150. self.preferences = {}
  151. self.preferences.update(preferences)
  152. for addon in self.addons:
  153. self.install_addon(addon)
  154. self.set_preferences(self.preferences)
  155. def create_new_profile(self, binary):
  156. """Create a new clean profile in tmp which is a simple empty folder"""
  157. profile = tempfile.mkdtemp(suffix='.mozrunner')
  158. return profile
  159. def unpack_addon(self, xpi_zipfile, addon_path):
  160. for name in xpi_zipfile.namelist():
  161. if name.endswith('/'):
  162. makedirs(os.path.join(addon_path, name))
  163. else:
  164. if not os.path.isdir(os.path.dirname(os.path.join(addon_path, name))):
  165. makedirs(os.path.dirname(os.path.join(addon_path, name)))
  166. data = xpi_zipfile.read(name)
  167. f = open(os.path.join(addon_path, name), 'wb')
  168. f.write(data) ; f.close()
  169. zi = xpi_zipfile.getinfo(name)
  170. os.chmod(os.path.join(addon_path,name), (zi.external_attr>>16))
  171. def install_addon(self, path):
  172. """Installs the given addon or directory of addons in the profile."""
  173. extensions_path = os.path.join(self.profile, 'extensions')
  174. if not os.path.exists(extensions_path):
  175. os.makedirs(extensions_path)
  176. addons = [path]
  177. if not path.endswith('.xpi') and not os.path.exists(os.path.join(path, 'install.rdf')):
  178. addons = [os.path.join(path, x) for x in os.listdir(path)]
  179. for addon in addons:
  180. if addon.endswith('.xpi'):
  181. xpi_zipfile = zipfile.ZipFile(addon, "r")
  182. details = addon_details(StringIO(xpi_zipfile.read('install.rdf')))
  183. addon_path = os.path.join(extensions_path, details["id"])
  184. if details.get("unpack", True):
  185. self.unpack_addon(xpi_zipfile, addon_path)
  186. self.addons_installed.append(addon_path)
  187. else:
  188. shutil.copy(addon, addon_path + '.xpi')
  189. else:
  190. # it's already unpacked, but we need to extract the id so we
  191. # can copy it
  192. details = addon_details(open(os.path.join(addon, "install.rdf"), "rb"))
  193. addon_path = os.path.join(extensions_path, details["id"])
  194. shutil.copytree(addon, addon_path, symlinks=True)
  195. def set_preferences(self, preferences):
  196. """Adds preferences dict to profile preferences"""
  197. prefs_file = os.path.join(self.profile, 'user.js')
  198. # Ensure that the file exists first otherwise create an empty file
  199. if os.path.isfile(prefs_file):
  200. f = open(prefs_file, 'a+')
  201. else:
  202. f = open(prefs_file, 'w')
  203. f.write('\n#MozRunner Prefs Start\n')
  204. pref_lines = ['user_pref(%s, %s);' %
  205. (simplejson.dumps(k), simplejson.dumps(v) ) for k, v in
  206. preferences.items()]
  207. for line in pref_lines:
  208. f.write(line+'\n')
  209. f.write('#MozRunner Prefs End\n')
  210. f.flush() ; f.close()
  211. def pop_preferences(self):
  212. """
  213. pop the last set of preferences added
  214. returns True if popped
  215. """
  216. # our magic markers
  217. delimeters = ('#MozRunner Prefs Start', '#MozRunner Prefs End')
  218. lines = file(os.path.join(self.profile, 'user.js')).read().splitlines()
  219. def last_index(_list, value):
  220. """
  221. returns the last index of an item;
  222. this should actually be part of python code but it isn't
  223. """
  224. for index in reversed(range(len(_list))):
  225. if _list[index] == value:
  226. return index
  227. s = last_index(lines, delimeters[0])
  228. e = last_index(lines, delimeters[1])
  229. # ensure both markers are found
  230. if s is None:
  231. assert e is None, '%s found without %s' % (delimeters[1], delimeters[0])
  232. return False # no preferences found
  233. elif e is None:
  234. assert e is None, '%s found without %s' % (delimeters[0], delimeters[1])
  235. # ensure the markers are in the proper order
  236. assert e > s, '%s found at %s, while %s found at %s' (delimeter[1], e, delimeter[0], s)
  237. # write the prefs
  238. cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:])
  239. f = file(os.path.join(self.profile, 'user.js'), 'w')
  240. f.write(cleaned_prefs)
  241. f.close()
  242. return True
  243. def clean_preferences(self):
  244. """Removed preferences added by mozrunner."""
  245. while True:
  246. if not self.pop_preferences():
  247. break
  248. def clean_addons(self):
  249. """Cleans up addons in the profile."""
  250. for addon in self.addons_installed:
  251. if os.path.isdir(addon):
  252. rmtree(addon)
  253. def cleanup(self):
  254. """Cleanup operations on the profile."""
  255. def oncleanup_error(function, path, excinfo):
  256. #TODO: How should we handle this?
  257. print "Error Cleaning up: " + str(excinfo[1])
  258. if self.create_new:
  259. shutil.rmtree(self.profile, False, oncleanup_error)
  260. else:
  261. self.clean_preferences()
  262. self.clean_addons()
  263. class FirefoxProfile(Profile):
  264. """Specialized Profile subclass for Firefox"""
  265. preferences = {# Don't automatically update the application
  266. 'app.update.enabled' : False,
  267. # Don't restore the last open set of tabs if the browser has crashed
  268. 'browser.sessionstore.resume_from_crash': False,
  269. # Don't check for the default web browser
  270. 'browser.shell.checkDefaultBrowser' : False,
  271. # Don't warn on exit when multiple tabs are open
  272. 'browser.tabs.warnOnClose' : False,
  273. # Don't warn when exiting the browser
  274. 'browser.warnOnQuit': False,
  275. # Only install add-ons from the profile and the app folder
  276. 'extensions.enabledScopes' : 5,
  277. # Don't automatically update add-ons
  278. 'extensions.update.enabled' : False,
  279. # Don't open a dialog to show available add-on updates
  280. 'extensions.update.notifyUser' : False,
  281. }
  282. # The possible names of application bundles on Mac OS X, in order of
  283. # preference from most to least preferred.
  284. # Note: Nightly is obsolete, as it has been renamed to FirefoxNightly,
  285. # but it will still be present if users update an older nightly build
  286. # via the app update service.
  287. bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly']
  288. # The possible names of binaries, in order of preference from most to least
  289. # preferred.
  290. @property
  291. def names(self):
  292. if sys.platform == 'darwin':
  293. return ['firefox', 'nightly', 'shiretoko']
  294. if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
  295. return ['firefox', 'mozilla-firefox', 'iceweasel']
  296. if os.name == 'nt' or sys.platform == 'cygwin':
  297. return ['firefox']
  298. class ThunderbirdProfile(Profile):
  299. preferences = {'extensions.update.enabled' : False,
  300. 'extensions.update.notifyUser' : False,
  301. 'browser.shell.checkDefaultBrowser' : False,
  302. 'browser.tabs.warnOnClose' : False,
  303. 'browser.warnOnQuit': False,
  304. 'browser.sessionstore.resume_from_crash': False,
  305. }
  306. # The possible names of application bundles on Mac OS X, in order of
  307. # preference from most to least preferred.
  308. bundle_names = ["Thunderbird", "Shredder"]
  309. # The possible names of binaries, in order of preference from most to least
  310. # preferred.
  311. names = ["thunderbird", "shredder"]
  312. class Runner(object):
  313. """Handles all running operations. Finds bins, runs and kills the process."""
  314. def __init__(self, binary=None, profile=None, cmdargs=[], env=None,
  315. kp_kwargs={}):
  316. if binary is None:
  317. self.binary = self.find_binary()
  318. elif sys.platform == 'darwin' and binary.find('Contents/MacOS/') == -1:
  319. self.binary = os.path.join(binary, 'Contents/MacOS/%s-bin' % self.names[0])
  320. else:
  321. self.binary = binary
  322. if not os.path.exists(self.binary):
  323. raise Exception("Binary path does not exist "+self.binary)
  324. if sys.platform == 'linux2' and self.binary.endswith('-bin'):
  325. dirname = os.path.dirname(self.binary)
  326. if os.environ.get('LD_LIBRARY_PATH', None):
  327. os.environ['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname)
  328. else:
  329. os.environ['LD_LIBRARY_PATH'] = dirname
  330. # Disable the crash reporter by default
  331. os.environ['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
  332. self.profile = profile
  333. self.cmdargs = cmdargs
  334. if env is None:
  335. self.env = copy.copy(os.environ)
  336. self.env.update({'MOZ_NO_REMOTE':"1",})
  337. else:
  338. self.env = env
  339. self.kp_kwargs = kp_kwargs or {}
  340. def find_binary(self):
  341. """Finds the binary for self.names if one was not provided."""
  342. binary = None
  343. if sys.platform in ('linux2', 'sunos5', 'solaris') \
  344. or sys.platform.startswith('freebsd'):
  345. for name in reversed(self.names):
  346. binary = findInPath(name)
  347. elif os.name == 'nt' or sys.platform == 'cygwin':
  348. # find the default executable from the windows registry
  349. try:
  350. import _winreg
  351. except ImportError:
  352. pass
  353. else:
  354. sam_flags = [0]
  355. # KEY_WOW64_32KEY etc only appeared in 2.6+, but that's OK as
  356. # only 2.6+ has functioning 64bit builds.
  357. if hasattr(_winreg, "KEY_WOW64_32KEY"):
  358. if "64 bit" in sys.version:
  359. # a 64bit Python should also look in the 32bit registry
  360. sam_flags.append(_winreg.KEY_WOW64_32KEY)
  361. else:
  362. # possibly a 32bit Python on 64bit Windows, so look in
  363. # the 64bit registry incase there is a 64bit app.
  364. sam_flags.append(_winreg.KEY_WOW64_64KEY)
  365. for sam_flag in sam_flags:
  366. try:
  367. # assumes self.app_name is defined, as it should be for
  368. # implementors
  369. keyname = r"Software\Mozilla\Mozilla %s" % self.app_name
  370. sam = _winreg.KEY_READ | sam_flag
  371. app_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, keyname, 0, sam)
  372. version, _type = _winreg.QueryValueEx(app_key, "CurrentVersion")
  373. version_key = _winreg.OpenKey(app_key, version + r"\Main")
  374. path, _ = _winreg.QueryValueEx(version_key, "PathToExe")
  375. return path
  376. except _winreg.error:
  377. pass
  378. # search for the binary in the path
  379. for name in reversed(self.names):
  380. binary = findInPath(name)
  381. if sys.platform == 'cygwin':
  382. program_files = os.environ['PROGRAMFILES']
  383. else:
  384. program_files = os.environ['ProgramFiles']
  385. if binary is None:
  386. for bin in [(program_files, 'Mozilla Firefox', 'firefox.exe'),
  387. (os.environ.get("ProgramFiles(x86)"),'Mozilla Firefox', 'firefox.exe'),
  388. (program_files, 'Nightly', 'firefox.exe'),
  389. (os.environ.get("ProgramFiles(x86)"),'Nightly', 'firefox.exe'),
  390. (program_files, 'Aurora', 'firefox.exe'),
  391. (os.environ.get("ProgramFiles(x86)"),'Aurora', 'firefox.exe')
  392. ]:
  393. path = os.path.join(*bin)
  394. if os.path.isfile(path):
  395. binary = path
  396. break
  397. elif sys.platform == 'darwin':
  398. for bundle_name in self.bundle_names:
  399. # Look for the application bundle in the user's home directory
  400. # or the system-wide /Applications directory. If we don't find
  401. # it in one of those locations, we move on to the next possible
  402. # bundle name.
  403. appdir = os.path.join("~/Applications/%s.app" % bundle_name)
  404. if not os.path.isdir(appdir):
  405. appdir = "/Applications/%s.app" % bundle_name
  406. if not os.path.isdir(appdir):
  407. continue
  408. # Look for a binary with any of the possible binary names
  409. # inside the application bundle.
  410. for binname in self.names:
  411. binpath = os.path.join(appdir,
  412. "Contents/MacOS/%s-bin" % binname)
  413. if (os.path.isfile(binpath)):
  414. binary = binpath
  415. break
  416. if binary:
  417. break
  418. if binary is None:
  419. raise Exception('Mozrunner could not locate your binary, you will need to set it.')
  420. return binary
  421. @property
  422. def command(self):
  423. """Returns the command list to run."""
  424. cmd = [self.binary, '-profile', self.profile.profile]
  425. # On i386 OS X machines, i386+x86_64 universal binaries need to be told
  426. # to run as i386 binaries. If we're not running a i386+x86_64 universal
  427. # binary, then this command modification is harmless.
  428. if sys.platform == 'darwin':
  429. if hasattr(platform, 'architecture') and platform.architecture()[0] == '32bit':
  430. cmd = ['arch', '-i386'] + cmd
  431. return cmd
  432. def get_repositoryInfo(self):
  433. """Read repository information from application.ini and platform.ini."""
  434. import ConfigParser
  435. config = ConfigParser.RawConfigParser()
  436. dirname = os.path.dirname(self.binary)
  437. repository = { }
  438. for entry in [['application', 'App'], ['platform', 'Build']]:
  439. (file, section) = entry
  440. config.read(os.path.join(dirname, '%s.ini' % file))
  441. for entry in [['SourceRepository', 'repository'], ['SourceStamp', 'changeset']]:
  442. (key, id) = entry
  443. try:
  444. repository['%s_%s' % (file, id)] = config.get(section, key);
  445. except:
  446. repository['%s_%s' % (file, id)] = None
  447. return repository
  448. def start(self):
  449. """Run self.command in the proper environment."""
  450. if self.profile is None:
  451. self.profile = self.profile_class()
  452. self.process_handler = run_command(self.command+self.cmdargs, self.env, **self.kp_kwargs)
  453. def wait(self, timeout=None):
  454. """Wait for the browser to exit."""
  455. self.process_handler.wait(timeout=timeout)
  456. if sys.platform != 'win32':
  457. for name in self.names:
  458. for pid in get_pids(name, self.process_handler.pid):
  459. self.process_handler.pid = pid
  460. self.process_handler.wait(timeout=timeout)
  461. def kill(self, kill_signal=signal.SIGTERM):
  462. """Kill the browser"""
  463. if sys.platform != 'win32':
  464. self.process_handler.kill()
  465. for name in self.names:
  466. for pid in get_pids(name, self.process_handler.pid):
  467. self.process_handler.pid = pid
  468. self.process_handler.kill()
  469. else:
  470. try:
  471. self.process_handler.kill(group=True)
  472. # On windows, it sometimes behooves one to wait for dust to settle
  473. # after killing processes. Let's try that.
  474. # TODO: Bug 640047 is invesitgating the correct way to handle this case
  475. self.process_handler.wait(timeout=10)
  476. except Exception, e:
  477. logger.error('Cannot kill process, '+type(e).__name__+' '+e.message)
  478. def stop(self):
  479. self.kill()
  480. class FirefoxRunner(Runner):
  481. """Specialized Runner subclass for running Firefox."""
  482. app_name = 'Firefox'
  483. profile_class = FirefoxProfile
  484. # The possible names of application bundles on Mac OS X, in order of
  485. # preference from most to least preferred.
  486. # Note: Nightly is obsolete, as it has been renamed to FirefoxNightly,
  487. # but it will still be present if users update an older nightly build
  488. # only via the app update service.
  489. bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly']
  490. @property
  491. def names(self):
  492. if sys.platform == 'darwin':
  493. return ['firefox', 'nightly', 'shiretoko']
  494. if sys.platform in ('linux2', 'sunos5', 'solaris') \
  495. or sys.platform.startswith('freebsd'):
  496. return ['firefox', 'mozilla-firefox', 'iceweasel']
  497. if os.name == 'nt' or sys.platform == 'cygwin':
  498. return ['firefox']
  499. class ThunderbirdRunner(Runner):
  500. """Specialized Runner subclass for running Thunderbird"""
  501. app_name = 'Thunderbird'
  502. profile_class = ThunderbirdProfile
  503. # The possible names of application bundles on Mac OS X, in order of
  504. # preference from most to least preferred.
  505. bundle_names = ["Thunderbird", "Shredder"]
  506. # The possible names of binaries, in order of preference from most to least
  507. # preferred.
  508. names = ["thunderbird", "shredder"]
  509. class CLI(object):
  510. """Command line interface."""
  511. runner_class = FirefoxRunner
  512. profile_class = FirefoxProfile
  513. module = "mozrunner"
  514. parser_options = {("-b", "--binary",): dict(dest="binary", help="Binary path.",
  515. metavar=None, default=None),
  516. ('-p', "--profile",): dict(dest="profile", help="Profile path.",
  517. metavar=None, default=None),
  518. ('-a', "--addons",): dict(dest="addons",
  519. help="Addons paths to install.",
  520. metavar=None, default=None),
  521. ("--info",): dict(dest="info", default=False,
  522. action="store_true",
  523. help="Print module information")
  524. }
  525. def __init__(self):
  526. """ Setup command line parser and parse arguments """
  527. self.metadata = self.get_metadata_from_egg()
  528. self.parser = optparse.OptionParser(version="%prog " + self.metadata["Version"])
  529. for names, opts in self.parser_options.items():
  530. self.parser.add_option(*names, **opts)
  531. (self.options, self.args) = self.parser.parse_args()
  532. if self.options.info:
  533. self.print_metadata()
  534. sys.exit(0)
  535. # XXX should use action='append' instead of rolling our own
  536. try:
  537. self.addons = self.options.addons.split(',')
  538. except:
  539. self.addons = []
  540. def get_metadata_from_egg(self):
  541. import pkg_resources
  542. ret = {}
  543. dist = pkg_resources.get_distribution(self.module)
  544. if dist.has_metadata("PKG-INFO"):
  545. for line in dist.get_metadata_lines("PKG-INFO"):
  546. key, value = line.split(':', 1)
  547. ret[key] = value
  548. if dist.has_metadata("requires.txt"):
  549. ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt")
  550. return ret
  551. def print_metadata(self, data=("Name", "Version", "Summary", "Home-page",
  552. "Author", "Author-email", "License", "Platform", "Dependencies")):
  553. for key in data:
  554. if key in self.metadata:
  555. print key + ": " + self.metadata[key]
  556. def create_runner(self):
  557. """ Get the runner object """
  558. runner = self.get_runner(binary=self.options.binary)
  559. profile = self.get_profile(binary=runner.binary,
  560. profile=self.options.profile,
  561. addons=self.addons)
  562. runner.profile = profile
  563. return runner
  564. def get_runner(self, binary=None, profile=None):
  565. """Returns the runner instance for the given command line binary argument
  566. the profile instance returned from self.get_profile()."""
  567. return self.runner_class(binary, profile)
  568. def get_profile(self, binary=None, profile=None, addons=None, preferences=None):
  569. """Returns the profile instance for the given command line arguments."""
  570. addons = addons or []
  571. preferences = preferences or {}
  572. return self.profile_class(binary, profile, addons, preferences)
  573. def run(self):
  574. runner = self.create_runner()
  575. self.start(runner)
  576. runner.profile.cleanup()
  577. def start(self, runner):
  578. """Starts the runner and waits for Firefox to exitor Keyboard Interrupt.
  579. Shoule be overwritten to provide custom running of the runner instance."""
  580. runner.start()
  581. print 'Started:', ' '.join(runner.command)
  582. try:
  583. runner.wait()
  584. except KeyboardInterrupt:
  585. runner.stop()
  586. def cli():
  587. CLI().run()