OLD | NEW |
(Empty) | |
| 1 # ***** BEGIN LICENSE BLOCK ***** |
| 2 # Version: MPL 1.1/GPL 2.0/LGPL 2.1 |
| 3 # |
| 4 # The contents of this file are subject to the Mozilla Public License Version |
| 5 # 1.1 (the "License"); you may not use this file except in compliance with |
| 6 # the License. You may obtain a copy of the License at |
| 7 # http://www.mozilla.org/MPL/ |
| 8 # |
| 9 # Software distributed under the License is distributed on an "AS IS" basis, |
| 10 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License |
| 11 # for the specific language governing rights and limitations under the |
| 12 # License. |
| 13 # |
| 14 # The Original Code is Mozilla Corporation Code. |
| 15 # |
| 16 # The Initial Developer of the Original Code is |
| 17 # Mikeal Rogers. |
| 18 # Portions created by the Initial Developer are Copyright (C) 2008-2009 |
| 19 # the Initial Developer. All Rights Reserved. |
| 20 # |
| 21 # Contributor(s): |
| 22 # Mikeal Rogers <mikeal.rogers@gmail.com> |
| 23 # Clint Talbert <ctalbert@mozilla.com> |
| 24 # Henrik Skupin <hskupin@mozilla.com> |
| 25 # |
| 26 # Alternatively, the contents of this file may be used under the terms of |
| 27 # either the GNU General Public License Version 2 or later (the "GPL"), or |
| 28 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), |
| 29 # in which case the provisions of the GPL or the LGPL are applicable instead |
| 30 # of those above. If you wish to allow use of your version of this file only |
| 31 # under the terms of either the GPL or the LGPL, and not to allow others to |
| 32 # use your version of this file under the terms of the MPL, indicate your |
| 33 # decision by deleting the provisions above and replace them with the notice |
| 34 # and other provisions required by the GPL or the LGPL. If you do not delete |
| 35 # the provisions above, a recipient may use your version of this file under |
| 36 # the terms of any one of the MPL, the GPL or the LGPL. |
| 37 # |
| 38 # ***** END LICENSE BLOCK ***** |
| 39 |
| 40 import os |
| 41 import sys |
| 42 import copy |
| 43 import tempfile |
| 44 import signal |
| 45 import commands |
| 46 import zipfile |
| 47 import optparse |
| 48 import killableprocess |
| 49 import subprocess |
| 50 import platform |
| 51 |
| 52 from distutils import dir_util |
| 53 from time import sleep |
| 54 from xml.dom import minidom |
| 55 |
| 56 # conditional (version-dependent) imports |
| 57 try: |
| 58 import simplejson |
| 59 except ImportError: |
| 60 import json as simplejson |
| 61 |
| 62 import logging |
| 63 logger = logging.getLogger(__name__) |
| 64 |
| 65 # Use dir_util for copy/rm operations because shutil is all kinds of broken |
| 66 copytree = dir_util.copy_tree |
| 67 rmtree = dir_util.remove_tree |
| 68 |
| 69 def findInPath(fileName, path=os.environ['PATH']): |
| 70 dirs = path.split(os.pathsep) |
| 71 for dir in dirs: |
| 72 if os.path.isfile(os.path.join(dir, fileName)): |
| 73 return os.path.join(dir, fileName) |
| 74 if os.name == 'nt' or sys.platform == 'cygwin': |
| 75 if os.path.isfile(os.path.join(dir, fileName + ".exe")): |
| 76 return os.path.join(dir, fileName + ".exe") |
| 77 return None |
| 78 |
| 79 stdout = sys.stdout |
| 80 stderr = sys.stderr |
| 81 stdin = sys.stdin |
| 82 |
| 83 def run_command(cmd, env=None, **kwargs): |
| 84 """Run the given command in killable process.""" |
| 85 killable_kwargs = {'stdout':stdout ,'stderr':stderr, 'stdin':stdin} |
| 86 killable_kwargs.update(kwargs) |
| 87 |
| 88 if sys.platform != "win32": |
| 89 return killableprocess.Popen(cmd, preexec_fn=lambda : os.setpgid(0, 0), |
| 90 env=env, **killable_kwargs) |
| 91 else: |
| 92 return killableprocess.Popen(cmd, env=env, **killable_kwargs) |
| 93 |
| 94 def getoutput(l): |
| 95 tmp = tempfile.mktemp() |
| 96 x = open(tmp, 'w') |
| 97 subprocess.call(l, stdout=x, stderr=x) |
| 98 x.close(); x = open(tmp, 'r') |
| 99 r = x.read() ; x.close() |
| 100 os.remove(tmp) |
| 101 return r |
| 102 |
| 103 def get_pids(name, minimun_pid=0): |
| 104 """Get all the pids matching name, exclude any pids below minimum_pid.""" |
| 105 if os.name == 'nt' or sys.platform == 'cygwin': |
| 106 import wpk |
| 107 |
| 108 pids = wpk.get_pids(name) |
| 109 |
| 110 else: |
| 111 # get_pids_cmd = ['ps', 'ax'] |
| 112 # h = killableprocess.runCommand(get_pids_cmd, stdout=subprocess.PIPE, u
niversal_newlines=True) |
| 113 # h.wait(group=False) |
| 114 # data = h.stdout.readlines() |
| 115 data = getoutput(['ps', 'ax']).splitlines() |
| 116 pids = [int(line.split()[0]) for line in data if line.find(name) is not
-1] |
| 117 |
| 118 matching_pids = [m for m in pids if m > minimun_pid] |
| 119 return matching_pids |
| 120 |
| 121 def kill_process_by_name(name): |
| 122 """Find and kill all processes containing a certain name""" |
| 123 |
| 124 pids = get_pids(name) |
| 125 |
| 126 if os.name == 'nt' or sys.platform == 'cygwin': |
| 127 for p in pids: |
| 128 import wpk |
| 129 |
| 130 wpk.kill_pid(p) |
| 131 |
| 132 else: |
| 133 for pid in pids: |
| 134 try: |
| 135 os.kill(pid, signal.SIGTERM) |
| 136 except OSError: pass |
| 137 sleep(.5) |
| 138 if len(get_pids(name)) is not 0: |
| 139 try: |
| 140 os.kill(pid, signal.SIGKILL) |
| 141 except OSError: pass |
| 142 sleep(.5) |
| 143 if len(get_pids(name)) is not 0: |
| 144 logger.error('Could not kill process') |
| 145 |
| 146 def makedirs(name): |
| 147 |
| 148 head, tail = os.path.split(name) |
| 149 if not tail: |
| 150 head, tail = os.path.split(head) |
| 151 if head and tail and not os.path.exists(head): |
| 152 try: |
| 153 makedirs(head) |
| 154 except OSError, e: |
| 155 pass |
| 156 if tail == os.curdir: # xxx/newdir/. exists if xxx/newdir exis
ts |
| 157 return |
| 158 try: |
| 159 os.mkdir(name) |
| 160 except: |
| 161 pass |
| 162 |
| 163 class Profile(object): |
| 164 """Handles all operations regarding profile. Created new profiles, installs
extensions, |
| 165 sets preferences and handles cleanup.""" |
| 166 |
| 167 def __init__(self, binary=None, profile=None, addons=None, |
| 168 preferences=None): |
| 169 |
| 170 self.binary = binary |
| 171 |
| 172 self.create_new = not(bool(profile)) |
| 173 if profile: |
| 174 self.profile = profile |
| 175 else: |
| 176 self.profile = self.create_new_profile(self.binary) |
| 177 |
| 178 self.addons_installed = [] |
| 179 self.addons = addons or [] |
| 180 |
| 181 ### set preferences from class preferences |
| 182 preferences = preferences or {} |
| 183 if hasattr(self.__class__, 'preferences'): |
| 184 self.preferences = self.__class__.preferences.copy() |
| 185 else: |
| 186 self.preferences = {} |
| 187 self.preferences.update(preferences) |
| 188 |
| 189 for addon in self.addons: |
| 190 self.install_addon(addon) |
| 191 |
| 192 self.set_preferences(self.preferences) |
| 193 |
| 194 def create_new_profile(self, binary): |
| 195 """Create a new clean profile in tmp which is a simple empty folder""" |
| 196 profile = tempfile.mkdtemp(suffix='.mozrunner') |
| 197 return profile |
| 198 |
| 199 ### methods related to addons |
| 200 |
| 201 @classmethod |
| 202 def addon_id(self, addon_path): |
| 203 """ |
| 204 return the id for a given addon, or None if not found |
| 205 - addon_path : path to the addon directory |
| 206 """ |
| 207 |
| 208 def find_id(desc): |
| 209 """finds the addon id give its description""" |
| 210 |
| 211 addon_id = None |
| 212 for elem in desc: |
| 213 apps = elem.getElementsByTagName('em:targetApplication') |
| 214 if apps: |
| 215 for app in apps: |
| 216 # remove targetApplication nodes, they contain id's we a
ren't interested in |
| 217 elem.removeChild(app) |
| 218 if elem.getElementsByTagName('em:id'): |
| 219 addon_id = str(elem.getElementsByTagName('em:id')[0].fir
stChild.data) |
| 220 elif elem.hasAttribute('em:id'): |
| 221 addon_id = str(elem.getAttribute('em:id')) |
| 222 return addon_id |
| 223 |
| 224 doc = minidom.parse(os.path.join(addon_path, 'install.rdf')) |
| 225 |
| 226 for tag in 'Description', 'RDF:Description': |
| 227 desc = doc.getElementsByTagName(tag) |
| 228 addon_id = find_id(desc) |
| 229 if addon_id: |
| 230 return addon_id |
| 231 |
| 232 |
| 233 def install_addon(self, path): |
| 234 """Installs the given addon or directory of addons in the profile.""" |
| 235 |
| 236 # if the addon is a directory, install all addons in it |
| 237 addons = [path] |
| 238 if not path.endswith('.xpi') and not os.path.exists(os.path.join(path, '
install.rdf')): |
| 239 addons = [os.path.join(path, x) for x in os.listdir(path)] |
| 240 |
| 241 # install each addon |
| 242 for addon in addons: |
| 243 |
| 244 # if the addon is an .xpi, uncompress it to a temporary directory |
| 245 if addon.endswith('.xpi'): |
| 246 tmpdir = tempfile.mkdtemp(suffix = "." + os.path.split(addon)[-1
]) |
| 247 compressed_file = zipfile.ZipFile(addon, "r") |
| 248 for name in compressed_file.namelist(): |
| 249 if name.endswith('/'): |
| 250 makedirs(os.path.join(tmpdir, name)) |
| 251 else: |
| 252 if not os.path.isdir(os.path.dirname(os.path.join(tmpdir
, name))): |
| 253 makedirs(os.path.dirname(os.path.join(tmpdir, name))
) |
| 254 data = compressed_file.read(name) |
| 255 f = open(os.path.join(tmpdir, name), 'wb') |
| 256 f.write(data) ; f.close() |
| 257 addon = tmpdir |
| 258 |
| 259 # determine the addon id |
| 260 addon_id = Profile.addon_id(addon) |
| 261 assert addon_id is not None, "The addon id could not be found: %s" %
addon |
| 262 |
| 263 # copy the addon to the profile |
| 264 addon_path = os.path.join(self.profile, 'extensions', addon_id) |
| 265 copytree(addon, addon_path, preserve_symlinks=1) |
| 266 self.addons_installed.append(addon_path) |
| 267 |
| 268 def clean_addons(self): |
| 269 """Cleans up addons in the profile.""" |
| 270 for addon in self.addons_installed: |
| 271 if os.path.isdir(addon): |
| 272 rmtree(addon) |
| 273 |
| 274 ### methods related to preferences |
| 275 |
| 276 def set_preferences(self, preferences): |
| 277 """Adds preferences dict to profile preferences""" |
| 278 |
| 279 prefs_file = os.path.join(self.profile, 'user.js') |
| 280 |
| 281 # Ensure that the file exists first otherwise create an empty file |
| 282 if os.path.isfile(prefs_file): |
| 283 f = open(prefs_file, 'a+') |
| 284 else: |
| 285 f = open(prefs_file, 'w') |
| 286 |
| 287 f.write('\n#MozRunner Prefs Start\n') |
| 288 |
| 289 pref_lines = ['user_pref(%s, %s);' % |
| 290 (simplejson.dumps(k), simplejson.dumps(v) ) for k, v in |
| 291 preferences.items()] |
| 292 for line in pref_lines: |
| 293 f.write(line+'\n') |
| 294 f.write('#MozRunner Prefs End\n') |
| 295 f.flush() ; f.close() |
| 296 |
| 297 def clean_preferences(self): |
| 298 """Removed preferences added by mozrunner.""" |
| 299 lines = open(os.path.join(self.profile, 'user.js'), 'r').read().splitlin
es() |
| 300 s = lines.index('#MozRunner Prefs Start') ; e = lines.index('#MozRunner
Prefs End') |
| 301 cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:]) |
| 302 f = open(os.path.join(self.profile, 'user.js'), 'w') |
| 303 f.write(cleaned_prefs) ; f.flush() ; f.close() |
| 304 |
| 305 ### cleanup |
| 306 |
| 307 def cleanup(self): |
| 308 """Cleanup operations on the profile.""" |
| 309 if self.create_new: |
| 310 rmtree(self.profile) |
| 311 else: |
| 312 self.clean_preferences() |
| 313 self.clean_addons() |
| 314 |
| 315 class FirefoxProfile(Profile): |
| 316 """Specialized Profile subclass for Firefox""" |
| 317 preferences = {# Don't automatically update the application |
| 318 'app.update.enabled' : False, |
| 319 # Don't restore the last open set of tabs if the browser has
crashed |
| 320 'browser.sessionstore.resume_from_crash': False, |
| 321 # Don't check for the default web browser |
| 322 'browser.shell.checkDefaultBrowser' : False, |
| 323 # Don't warn on exit when multiple tabs are open |
| 324 'browser.tabs.warnOnClose' : False, |
| 325 # Don't warn when exiting the browser |
| 326 'browser.warnOnQuit': False, |
| 327 # Only install add-ons from the profile and the app folder |
| 328 'extensions.enabledScopes' : 5, |
| 329 # Dont' run the add-on compatibility check during start-up |
| 330 'extensions.showMismatchUI' : False, |
| 331 # Don't automatically update add-ons |
| 332 'extensions.update.enabled' : False, |
| 333 # Don't open a dialog to show available add-on updates |
| 334 'extensions.update.notifyUser' : False, |
| 335 } |
| 336 |
| 337 @property |
| 338 def names(self): |
| 339 if sys.platform == 'darwin': |
| 340 return ['firefox', 'minefield', 'shiretoko'] |
| 341 if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris'))
: |
| 342 return ['firefox', 'mozilla-firefox', 'iceweasel'] |
| 343 if os.name == 'nt' or sys.platform == 'cygwin': |
| 344 return ['firefox'] |
| 345 |
| 346 class ThunderbirdProfile(Profile): |
| 347 preferences = {'extensions.update.enabled' : False, |
| 348 'extensions.update.notifyUser' : False, |
| 349 'browser.shell.checkDefaultBrowser' : False, |
| 350 'browser.tabs.warnOnClose' : False, |
| 351 'browser.warnOnQuit': False, |
| 352 'browser.sessionstore.resume_from_crash': False, |
| 353 } |
| 354 names = ["thunderbird", "shredder"] |
| 355 |
| 356 |
| 357 class Runner(object): |
| 358 """Handles all running operations. Finds bins, runs and kills the process.""
" |
| 359 |
| 360 def __init__(self, binary=None, profile=None, cmdargs=[], env=None, |
| 361 aggressively_kill=['crashreporter'], kp_kwargs={}): |
| 362 if binary is None: |
| 363 self.binary = self.find_binary() |
| 364 elif sys.platform == 'darwin' and binary.find('Contents/MacOS/') == -1: |
| 365 self.binary = os.path.join(binary, 'Contents/MacOS/%s-bin' % self.na
mes[0]) |
| 366 else: |
| 367 self.binary = binary |
| 368 |
| 369 if not os.path.exists(self.binary): |
| 370 raise Exception("Binary path does not exist "+self.binary) |
| 371 |
| 372 if sys.platform == 'linux2' and self.binary.endswith('-bin'): |
| 373 dirname = os.path.dirname(self.binary) |
| 374 if os.environ.get('LD_LIBRARY_PATH', None): |
| 375 os.environ['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRAR
Y_PATH'], dirname) |
| 376 else: |
| 377 os.environ['LD_LIBRARY_PATH'] = dirname |
| 378 |
| 379 self.profile = profile |
| 380 |
| 381 self.cmdargs = cmdargs |
| 382 if env is None: |
| 383 self.env = copy.copy(os.environ) |
| 384 self.env.update({'MOZ_NO_REMOTE':"1",}) |
| 385 else: |
| 386 self.env = env |
| 387 self.aggressively_kill = aggressively_kill |
| 388 self.kp_kwargs = kp_kwargs |
| 389 |
| 390 def find_binary(self): |
| 391 """Finds the binary for self.names if one was not provided.""" |
| 392 binary = None |
| 393 if sys.platform in ('linux2', 'sunos5', 'solaris'): |
| 394 for name in reversed(self.names): |
| 395 binary = findInPath(name) |
| 396 elif os.name == 'nt' or sys.platform == 'cygwin': |
| 397 |
| 398 # find the default executable from the windows registry |
| 399 try: |
| 400 # assumes self.app_name is defined, as it should be for |
| 401 # implementors |
| 402 import _winreg |
| 403 app_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, r"Software
\Mozilla\Mozilla %s" % self.app_name) |
| 404 version, _type = _winreg.QueryValueEx(app_key, "CurrentVersion") |
| 405 version_key = _winreg.OpenKey(app_key, version + r"\Main") |
| 406 path, _ = _winreg.QueryValueEx(version_key, "PathToExe") |
| 407 return path |
| 408 except: # XXX not sure what type of exception this should be |
| 409 pass |
| 410 |
| 411 # search for the binary in the path |
| 412 for name in reversed(self.names): |
| 413 binary = findInPath(name) |
| 414 if sys.platform == 'cygwin': |
| 415 program_files = os.environ['PROGRAMFILES'] |
| 416 else: |
| 417 program_files = os.environ['ProgramFiles'] |
| 418 |
| 419 if binary is None: |
| 420 for bin in [(program_files, 'Mozilla Firefox', 'firefox.exe'
), |
| 421 (os.environ.get("ProgramFiles(x86)"),'Mozilla Fi
refox', 'firefox.exe'), |
| 422 (program_files,'Minefield', 'firefox.exe'), |
| 423 (os.environ.get("ProgramFiles(x86)"),'Minefield'
, 'firefox.exe') |
| 424 ]: |
| 425 path = os.path.join(*bin) |
| 426 if os.path.isfile(path): |
| 427 binary = path |
| 428 break |
| 429 elif sys.platform == 'darwin': |
| 430 for name in reversed(self.names): |
| 431 appdir = os.path.join('Applications', name.capitalize()+'.app') |
| 432 if os.path.isdir(os.path.join(os.path.expanduser('~/'), appdir))
: |
| 433 binary = os.path.join(os.path.expanduser('~/'), appdir, |
| 434 'Contents/MacOS/'+name+'-bin') |
| 435 elif os.path.isdir('/'+appdir): |
| 436 binary = os.path.join("/"+appdir, 'Contents/MacOS/'+name+'-b
in') |
| 437 |
| 438 if binary is not None: |
| 439 if not os.path.isfile(binary): |
| 440 binary = binary.replace(name+'-bin', 'firefox-bin') |
| 441 if not os.path.isfile(binary): |
| 442 binary = None |
| 443 if binary is None: |
| 444 raise Exception('Mozrunner could not locate your binary, you will ne
ed to set it.') |
| 445 return binary |
| 446 |
| 447 @property |
| 448 def command(self): |
| 449 """Returns the command list to run.""" |
| 450 cmd = [self.binary, '-profile', self.profile.profile] |
| 451 # On i386 OS X machines, i386+x86_64 universal binaries need to be told |
| 452 # to run as i386 binaries. If we're not running a i386+x86_64 universal |
| 453 # binary, then this command modification is harmless. |
| 454 if sys.platform == 'darwin': |
| 455 if hasattr(platform, 'architecture') and platform.architecture()[0]
== '32bit': |
| 456 cmd = ['arch', '-i386'] + cmd |
| 457 return cmd |
| 458 |
| 459 def get_repositoryInfo(self): |
| 460 """Read repository information from application.ini and platform.ini.""" |
| 461 import ConfigParser |
| 462 |
| 463 config = ConfigParser.RawConfigParser() |
| 464 dirname = os.path.dirname(self.binary) |
| 465 repository = { } |
| 466 |
| 467 for entry in [['application', 'App'], ['platform', 'Build']]: |
| 468 (file, section) = entry |
| 469 config.read(os.path.join(dirname, '%s.ini' % file)) |
| 470 |
| 471 for entry in [['SourceRepository', 'repository'], ['SourceStamp', 'c
hangeset']]: |
| 472 (key, id) = entry |
| 473 |
| 474 try: |
| 475 repository['%s_%s' % (file, id)] = config.get(section, key); |
| 476 except: |
| 477 repository['%s_%s' % (file, id)] = None |
| 478 |
| 479 return repository |
| 480 |
| 481 def start(self): |
| 482 """Run self.command in the proper environment.""" |
| 483 if self.profile is None: |
| 484 self.profile = self.profile_class() |
| 485 self.process_handler = run_command(self.command+self.cmdargs, self.env,
**self.kp_kwargs) |
| 486 |
| 487 def wait(self, timeout=None): |
| 488 """Wait for the browser to exit.""" |
| 489 self.process_handler.wait(timeout=timeout) |
| 490 |
| 491 if sys.platform != 'win32': |
| 492 for name in self.names: |
| 493 for pid in get_pids(name, self.process_handler.pid): |
| 494 self.process_handler.pid = pid |
| 495 self.process_handler.wait(timeout=timeout) |
| 496 |
| 497 def kill(self, kill_signal=signal.SIGTERM): |
| 498 """Kill the browser""" |
| 499 if sys.platform != 'win32': |
| 500 self.process_handler.kill() |
| 501 for name in self.names: |
| 502 for pid in get_pids(name, self.process_handler.pid): |
| 503 self.process_handler.pid = pid |
| 504 self.process_handler.kill() |
| 505 else: |
| 506 try: |
| 507 self.process_handler.kill(group=True) |
| 508 except Exception, e: |
| 509 logger.error('Cannot kill process, '+type(e).__name__+' '+e.mess
age) |
| 510 |
| 511 for name in self.aggressively_kill: |
| 512 kill_process_by_name(name) |
| 513 |
| 514 def stop(self): |
| 515 self.kill() |
| 516 |
| 517 class FirefoxRunner(Runner): |
| 518 """Specialized Runner subclass for running Firefox.""" |
| 519 |
| 520 app_name = 'Firefox' |
| 521 profile_class = FirefoxProfile |
| 522 |
| 523 @property |
| 524 def names(self): |
| 525 if sys.platform == 'darwin': |
| 526 return ['firefox', 'minefield', 'shiretoko'] |
| 527 if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris'))
: |
| 528 return ['firefox', 'mozilla-firefox', 'iceweasel'] |
| 529 if os.name == 'nt' or sys.platform == 'cygwin': |
| 530 return ['firefox'] |
| 531 |
| 532 class ThunderbirdRunner(Runner): |
| 533 """Specialized Runner subclass for running Thunderbird""" |
| 534 |
| 535 app_name = 'Thunderbird' |
| 536 profile_class = ThunderbirdProfile |
| 537 |
| 538 names = ["thunderbird", "shredder"] |
| 539 |
| 540 class CLI(object): |
| 541 """Command line interface.""" |
| 542 |
| 543 runner_class = FirefoxRunner |
| 544 profile_class = FirefoxProfile |
| 545 module = "mozrunner" |
| 546 |
| 547 parser_options = {("-b", "--binary",): dict(dest="binary", help="Binary path
.", |
| 548 metavar=None, default=None), |
| 549 ('-p', "--profile",): dict(dest="profile", help="Profile p
ath.", |
| 550 metavar=None, default=None), |
| 551 ('-a', "--addons",): dict(dest="addons", |
| 552 help="Addons paths to install.", |
| 553 metavar=None, default=None), |
| 554 ("--info",): dict(dest="info", default=False, |
| 555 action="store_true", |
| 556 help="Print module information") |
| 557 } |
| 558 |
| 559 def __init__(self): |
| 560 """ Setup command line parser and parse arguments """ |
| 561 self.metadata = self.get_metadata_from_egg() |
| 562 self.parser = optparse.OptionParser(version="%prog " + self.metadata["Ve
rsion"]) |
| 563 for names, opts in self.parser_options.items(): |
| 564 self.parser.add_option(*names, **opts) |
| 565 (self.options, self.args) = self.parser.parse_args() |
| 566 |
| 567 if self.options.info: |
| 568 self.print_metadata() |
| 569 sys.exit(0) |
| 570 |
| 571 # XXX should use action='append' instead of rolling our own |
| 572 try: |
| 573 self.addons = self.options.addons.split(',') |
| 574 except: |
| 575 self.addons = [] |
| 576 |
| 577 def get_metadata_from_egg(self): |
| 578 import pkg_resources |
| 579 ret = {} |
| 580 dist = pkg_resources.get_distribution(self.module) |
| 581 if dist.has_metadata("PKG-INFO"): |
| 582 for line in dist.get_metadata_lines("PKG-INFO"): |
| 583 key, value = line.split(':', 1) |
| 584 ret[key] = value |
| 585 if dist.has_metadata("requires.txt"): |
| 586 ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt") |
| 587 return ret |
| 588 |
| 589 def print_metadata(self, data=("Name", "Version", "Summary", "Home-page", |
| 590 "Author", "Author-email", "License", "Platfor
m", "Dependencies")): |
| 591 for key in data: |
| 592 if key in self.metadata: |
| 593 print key + ": " + self.metadata[key] |
| 594 |
| 595 def create_runner(self): |
| 596 """ Get the runner object """ |
| 597 runner = self.get_runner(binary=self.options.binary) |
| 598 profile = self.get_profile(binary=runner.binary, |
| 599 profile=self.options.profile, |
| 600 addons=self.addons) |
| 601 runner.profile = profile |
| 602 return runner |
| 603 |
| 604 def get_runner(self, binary=None, profile=None): |
| 605 """Returns the runner instance for the given command line binary argumen
t |
| 606 the profile instance returned from self.get_profile().""" |
| 607 return self.runner_class(binary, profile) |
| 608 |
| 609 def get_profile(self, binary=None, profile=None, addons=None, preferences=No
ne): |
| 610 """Returns the profile instance for the given command line arguments.""" |
| 611 addons = addons or [] |
| 612 preferences = preferences or {} |
| 613 return self.profile_class(binary, profile, addons, preferences) |
| 614 |
| 615 def run(self): |
| 616 runner = self.create_runner() |
| 617 self.start(runner) |
| 618 runner.profile.cleanup() |
| 619 |
| 620 def start(self, runner): |
| 621 """Starts the runner and waits for Firefox to exitor Keyboard Interrupt. |
| 622 Shoule be overwritten to provide custom running of the runner instance."
"" |
| 623 runner.start() |
| 624 print 'Started:', ' '.join(runner.command) |
| 625 try: |
| 626 runner.wait() |
| 627 except KeyboardInterrupt: |
| 628 runner.stop() |
| 629 |
| 630 |
| 631 def cli(): |
| 632 CLI().run() |
| 633 |
| 634 def print_addon_ids(args=sys.argv[1:]): |
| 635 """print addon ids for testing""" |
| 636 for arg in args: |
| 637 print Profile.addon_id(arg) |
| 638 |
| 639 |
OLD | NEW |