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