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 |