Bug 769808 - Update mozbase on m-c to include fix for bug 769789. r=jhammel
authorSiddharth Agarwal <sid.bugzilla@gmail.com>
Tue, 03 Jul 2012 01:52:54 +0530
changeset 102928 a9fda5347abd578680813882ebb09980de955223
parent 102927 4a5f794edd2d03d83a9836f749665ac874b7320c
child 102929 72893005d192f5a447f8a1d52b8daf7fdab6deb0
push idunknown
push userunknown
push dateunknown
reviewersjhammel
bugs769808, 769789
milestone16.0a1
Bug 769808 - Update mozbase on m-c to include fix for bug 769789. r=jhammel
testing/mozbase/manifestdestiny/README.md
testing/mozbase/manifestdestiny/manifestparser/__init__.py
testing/mozbase/manifestdestiny/manifestparser/manifestparser.py
testing/mozbase/manifestdestiny/setup.py
testing/mozbase/manifestdestiny/tests/test.py
testing/mozbase/manifestdestiny/tests/test_expressionparser.txt
testing/mozbase/mozhttpd/README.md
testing/mozbase/mozhttpd/mozhttpd/__init__.py
testing/mozbase/mozhttpd/mozhttpd/handlers.py
testing/mozbase/mozhttpd/mozhttpd/iface.py
testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py
testing/mozbase/mozhttpd/setup.py
testing/mozbase/mozhttpd/tests/api.py
testing/mozbase/mozhttpd/tests/filelisting.py
testing/mozbase/mozinfo/README.md
testing/mozbase/mozinfo/mozinfo/__init__.py
testing/mozbase/mozinfo/mozinfo/mozinfo.py
testing/mozbase/mozinfo/setup.py
testing/mozbase/mozinstall/README.md
testing/mozbase/mozinstall/mozinstall/__init__.py
testing/mozbase/mozinstall/mozinstall/mozinstall.py
testing/mozbase/mozinstall/setup.py
testing/mozbase/mozlog/README.md
testing/mozbase/mozlog/mozlog/__init__.py
testing/mozbase/mozlog/mozlog/logger.py
testing/mozbase/mozlog/setup.py
testing/mozbase/mozprocess/README.md
testing/mozbase/mozprocess/mozprocess/__init__.py
testing/mozbase/mozprocess/mozprocess/pid.py
testing/mozbase/mozprocess/mozprocess/processhandler.py
testing/mozbase/mozprocess/mozprocess/qijo.py
testing/mozbase/mozprocess/mozprocess/wpk.py
testing/mozbase/mozprocess/setup.py
testing/mozbase/mozprocess/tests/mozprocess1.py
testing/mozbase/mozprocess/tests/mozprocess2.py
testing/mozbase/mozprocess/tests/proclaunch.c
testing/mozbase/mozprofile/README.md
testing/mozbase/mozprofile/mozprofile/__init__.py
testing/mozbase/mozprofile/mozprofile/addons.py
testing/mozbase/mozprofile/mozprofile/cli.py
testing/mozbase/mozprofile/mozprofile/permissions.py
testing/mozbase/mozprofile/mozprofile/prefs.py
testing/mozbase/mozprofile/mozprofile/profile.py
testing/mozbase/mozprofile/setup.py
testing/mozbase/mozprofile/tests/bug758250.py
testing/mozbase/mozprofile/tests/empty/install.rdf
testing/mozbase/mozprofile/tests/manifest.ini
testing/mozbase/mozprofile/tests/test_preferences.py
testing/mozbase/mozrunner/README.md
testing/mozbase/mozrunner/mozrunner/__init__.py
testing/mozbase/mozrunner/mozrunner/runner.py
testing/mozbase/mozrunner/mozrunner/utils.py
testing/mozbase/mozrunner/setup.py
testing/mozbase/setup_development.py
testing/mozbase/test-manifest.ini
testing/mozbase/test.py
--- a/testing/mozbase/manifestdestiny/README.md
+++ b/testing/mozbase/manifestdestiny/README.md
@@ -1,12 +1,8 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
 Universal manifests for Mozilla test harnesses
 
 # What is ManifestDestiny?
 
 What ManifestDestiny gives you:
 
 * manifests are ordered lists of tests
 * tests may have an arbitrary number of key, value pairs
--- a/testing/mozbase/manifestdestiny/manifestparser/__init__.py
+++ b/testing/mozbase/manifestdestiny/manifestparser/__init__.py
@@ -1,5 +1,5 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from manifestparser import *
--- a/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py
+++ b/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py
@@ -1,12 +1,13 @@
 #!/usr/bin/env python
+
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 """
 Mozilla universal manifest parser
 """
 
 # this file lives at
 # http://hg.mozilla.org/automation/ManifestDestiny/raw-file/tip/manifestparser.py
 
@@ -690,18 +691,18 @@ class TestManifest(ManifestParser):
     """
     apply logic to manifests;  this is your integration layer :)
     specific harnesses may subclass from this if they need more logic
     """
 
     def filter(self, values, tests):
         """
         filter on a specific list tag, e.g.:
-        run-if = os == 'win' || os == 'linux'
-        skip-if = os == 'mac'
+        run-if.os = win linux
+        skip-if.os = mac
         """
 
         # tags:
         run_tag = 'run-if'
         skip_tag = 'skip-if'
         fail_tag = 'fail-if'
 
         # loop over test
--- a/testing/mozbase/manifestdestiny/setup.py
+++ b/testing/mozbase/manifestdestiny/setup.py
@@ -1,12 +1,11 @@
-#!/usr/bin/env python
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # The real details are in manifestparser.py; this is just a front-end
 # BUT use this file when you want to distribute to python!
 # otherwise setuptools will complain that it can't find setup.py
 # and result in a useless package
 
 from setuptools import setup, find_packages
 import sys
--- a/testing/mozbase/manifestdestiny/tests/test.py
+++ b/testing/mozbase/manifestdestiny/tests/test.py
@@ -1,13 +1,11 @@
-#!/usr/bin/env python
-
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 """tests for ManifestDestiny"""
 
 import doctest
 import os
 import sys
 from optparse import OptionParser
 
--- a/testing/mozbase/manifestdestiny/tests/test_expressionparser.txt
+++ b/testing/mozbase/manifestdestiny/tests/test_expressionparser.txt
@@ -31,18 +31,16 @@ Test basic values::
     'xyz'
 
 Test equality::
 
     >>> parse("true == true")
     True
     >>> parse("false == false")
     True
-    >>> parse("false == false")
-    True
     >>> parse("1 == 1")
     True
     >>> parse("100 == 100")
     True
     >>> parse('"some text" == "some text"')
     True
     >>> parse("true != false")
     True
--- a/testing/mozbase/mozhttpd/README.md
+++ b/testing/mozbase/mozhttpd/README.md
@@ -1,5 +1,1 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
 basic python webserver, tested with talos
--- a/testing/mozbase/mozhttpd/mozhttpd/__init__.py
+++ b/testing/mozbase/mozhttpd/mozhttpd/__init__.py
@@ -1,7 +1,7 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from mozhttpd import MozHttpd, Request, RequestHandler, main
 from handlers import json_response
 import iface
--- a/testing/mozbase/mozhttpd/mozhttpd/handlers.py
+++ b/testing/mozbase/mozhttpd/mozhttpd/handlers.py
@@ -1,11 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 try:
     import json
 except ImportError:
     import simplejson as json
 
 def json_response(func):
     """ Translates results of 'func' into a JSON response. """
--- a/testing/mozbase/mozhttpd/mozhttpd/iface.py
+++ b/testing/mozbase/mozhttpd/mozhttpd/iface.py
@@ -1,11 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 import socket
 if os.name != 'nt':
     import fcntl
     import struct
 
 def _get_interface_ip(ifname):
--- a/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py
+++ b/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py
@@ -1,26 +1,27 @@
 #!/usr/bin/env python
 
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import BaseHTTPServer
 import SimpleHTTPServer
 import errno
 import logging
 import threading
 import posixpath
 import socket
 import sys
 import os
 import urllib
 import urlparse
 import re
+import iface
 from SocketServer import ThreadingMixIn
 
 class EasyServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
     allow_reuse_address = True
     acceptable_errors = (errno.EPIPE, errno.ECONNABORTED)
 
     def handle_error(self, request, client_address):
         error = sys.exc_value
@@ -219,35 +220,43 @@ class MozHttpd(object):
             except AttributeError:
                 pass
         self.httpd = None
 
     __del__ = stop
 
 
 def main(args=sys.argv[1:]):
-    
+
     # parse command line options
     from optparse import OptionParser
     parser = OptionParser()
-    parser.add_option('-p', '--port', dest='port', 
+    parser.add_option('-p', '--port', dest='port',
                       type="int", default=8888,
                       help="port to run the server on [DEFAULT: %default]")
     parser.add_option('-H', '--host', dest='host',
                       default='127.0.0.1',
                       help="host [DEFAULT: %default]")
+    parser.add_option('-i', '--external-ip', action="store_true",
+                      dest='external_ip', default=False,
+                      help="find and use external ip for host")
     parser.add_option('-d', '--docroot', dest='docroot',
                       default=os.getcwd(),
                       help="directory to serve files from [DEFAULT: %default]")
     options, args = parser.parse_args(args)
     if args:
+        parser.error("mozhttpd does not take any arguments")
         parser.print_help()
         parser.exit()
 
+    if options.external_ip:
+        host = iface.get_lan_ip()
+    else:
+        host = options.host
+
     # create the server
-    kwargs = options.__dict__.copy()
-    server = MozHttpd(**kwargs)
+    server = MozHttpd(host=host, port=options.port, docroot=options.docroot)
 
     print "Serving '%s' at %s:%s" % (server.docroot, server.host, server.port)
     server.start(block=True)
 
 if __name__ == '__main__':
     main()
--- a/testing/mozbase/mozhttpd/setup.py
+++ b/testing/mozbase/mozhttpd/setup.py
@@ -1,22 +1,22 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 from setuptools import setup, find_packages
 
 try:
     here = os.path.dirname(os.path.abspath(__file__))
     description = file(os.path.join(here, 'README.md')).read()
 except IOError:
     description = None
 
-version = '0.2'
+version = '0.3'
 
 deps = []
 
 setup(name='mozhttpd',
       version=version,
       description="basic python webserver, tested with talos",
       long_description=description,
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
--- a/testing/mozbase/mozhttpd/tests/api.py
+++ b/testing/mozbase/mozhttpd/tests/api.py
@@ -1,13 +1,11 @@
-#!/usr/bin/env python
-
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import mozhttpd
 import urllib2
 import os
 import unittest
 import re
 try:
     import json
--- a/testing/mozbase/mozhttpd/tests/filelisting.py
+++ b/testing/mozbase/mozhttpd/tests/filelisting.py
@@ -1,13 +1,11 @@
-#!/usr/bin/env python
-
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import mozhttpd
 import urllib2
 import os
 import unittest
 import re
 
 here = os.path.dirname(os.path.abspath(__file__))
--- a/testing/mozbase/mozinfo/README.md
+++ b/testing/mozbase/mozinfo/README.md
@@ -1,12 +1,8 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
 Throughout [mozmill](https://developer.mozilla.org/en/Mozmill)
 and other Mozilla python code, checking the underlying
 platform is done in many different ways.  The various checks needed
 lead to a lot of copy+pasting, leaving the reader to wonder....is this
 specific check necessary for (e.g.) an operating system?  Because
 information is not consolidated, checks are not done consistently, nor
 is it defined what we are checking for.
 
--- a/testing/mozbase/mozinfo/mozinfo/__init__.py
+++ b/testing/mozbase/mozinfo/mozinfo/__init__.py
@@ -1,5 +1,5 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from mozinfo import *
--- a/testing/mozbase/mozinfo/mozinfo/mozinfo.py
+++ b/testing/mozbase/mozinfo/mozinfo/mozinfo.py
@@ -1,13 +1,13 @@
 #!/usr/bin/env python
 
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 """
 file for interface to transform introspected system information to a format
 pallatable to Mozilla
 
 Information:
 - os : what operating system ['win', 'mac', 'linux', ...]
 - bits : 32 or 64
--- a/testing/mozbase/mozinfo/setup.py
+++ b/testing/mozbase/mozinfo/setup.py
@@ -1,11 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 
 import os
 from setuptools import setup, find_packages
 
 version = '0.3.3'
 
 # get documentation from the README
--- a/testing/mozbase/mozinstall/README.md
+++ b/testing/mozbase/mozinstall/README.md
@@ -1,39 +1,60 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-[Mozinstall](https://github.com/mozilla/mozbase/tree/master/mozinstall)
-is a python package for installing Mozilla applications on various platforms.
+[Mozinstall](https://github.com/mozilla/mozbase/tree/master/mozinstall) is a
+python package for installing and uninstalling Mozilla applications on
+various platforms.
 
 For example, depending on the platform, Firefox can be distributed as a 
-zip, tar.bz2, exe or dmg file or cloned from a repository. Mozinstall takes the 
-hassle out of extracting and/or running these files and for convenience returns
-the full path to the application's binary in the install directory. In the case 
-that mozinstall is invoked from the command line, the binary path will be 
-printed to stdout.
+zip, tar.bz2, exe, or dmg file or cloned from a repository. Mozinstall takes
+the hassle out of extracting and/or running these files and for convenience
+returns the full path to the install directory. In the case that mozinstall
+is invoked from the command line, the binary path will be printed to stdout.
+
+To remove an installed application the uninstaller can be used. It requires
+the installation path of the application and will remove all the installed
+files. On Windows the uninstaller will be tried first.
 
 # Usage
+Mozinstall can be used as API or via the CLI commands.
 
-For command line options run mozinstall --help
-
-Mozinstall's main function is the install method
+## API
+An application can be installed by running the commands below. The install
+method will return the installation path of the application.
 
     import mozinstall
-    mozinstall.install('path_to_install_file', dest='path_to_install_folder')
+    path = mozinstall.install(%installer%, %install_folder%)
+
+To retrieve the real binary call get_binary with the path and
+the application name as arguments:
+
+    mozinstall.get_binary(path, 'firefox')
+
+If the application is not needed anymore the uninstaller will remove all
+traces from the system:
+
+    mozinstall.uninstall(path)
 
-The dest parameter defaults to the directory in which the install file is located.
-The install method accepts a third parameter called apps which tells mozinstall which 
-binary to search for. By default it will search for 'firefox', 'thunderbird' and 'fennec'
-so unless you are installing a different application, this parameter is unnecessary.
+## CLI
+The installer can also be used as a command line tool:
+
+    $ mozinstall -d firefox %installer%
+
+Whereby the directory option is optional and will default to the current
+working directory. If the installation was successful the path to the
+binary will be printed to stdout.
+
+Also the uninstaller can be called via the command line:
+
+    $ mozuninstall %install_path%
 
 # Error Handling
 
-Mozinstall throws two different types of exceptions:
+Mozinstall throws different types of exceptions:
 
-- mozinstall.InvalidSource is thrown when the source is not a recognized file type (zip, exe, tar.bz2, tar.gz, dmg)
 - mozinstall.InstallError is thrown when the installation fails for any reason. A traceback is provided.
+- mozinstall.InvalidBinary is thrown when the binary cannot be found.
+- mozinstall.InvalidSource is thrown when the source is not a recognized file type (zip, exe, tar.bz2, tar.gz, dmg).
+
 
 # Dependencies
 
 Mozinstall depends on the [mozinfo](https://github.com/mozilla/mozbase/tree/master/mozinfo) 
 package which is also found in the mozbase repository.
--- a/testing/mozbase/mozinstall/mozinstall/__init__.py
+++ b/testing/mozbase/mozinstall/mozinstall/__init__.py
@@ -1,4 +1,5 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
 from mozinstall import *
--- a/testing/mozbase/mozinstall/mozinstall/mozinstall.py
+++ b/testing/mozbase/mozinstall/mozinstall/mozinstall.py
@@ -1,201 +1,370 @@
-#!/usr/bin/env python
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
+"""Module to handle the installation and uninstallation of Gecko based
+applications across platforms.
+
+"""
+import mozinfo
 from optparse import OptionParser
-import mozinfo
-import subprocess
-import zipfile
-import tarfile
-import sys
 import os
+import shutil
+import subprocess
+import sys
+import tarfile
+import time
+import zipfile
 
-_default_apps = ["firefox",
-                 "thunderbird",
-                 "fennec"]
+if mozinfo.isMac:
+    from plistlib import readPlist
+
+
+TIMEOUT_UNINSTALL = 60
+
+
+class InstallError(Exception):
+    """Thrown when installation fails. Includes traceback if available."""
+
+
+class InvalidBinary(Exception):
+    """Thrown when the binary cannot be found after the installation."""
+
+
+class InvalidSource(Exception):
+    """Thrown when the specified source is not a recognized file type.
+
+    Supported types:
+    Linux:   tar.gz, tar.bz2
+    Mac:     dmg
+    Windows: zip, exe
+
+    """
+
+
+class UninstallError(Exception):
+    """Thrown when uninstallation fails. Includes traceback if available."""
+
+
+def get_binary(path, app_name):
+    """Find the binary in the specified path, and return its path. If binary is
+    not found throw an InvalidBinary exception.
 
-def install(src, dest=None, apps=_default_apps):
+    Arguments:
+    path -- the path within to search for the binary
+    app_name -- application binary without file extension to look for
+
     """
-    Installs a zip, exe, tar.gz, tar.bz2 or dmg file
-    src - the path to the install file
-    dest - the path to install to [default is os.path.dirname(src)]
-    returns - the full path to the binary in the installed folder
-              or None if the binary cannot be found
+    binary = None
+
+    # On OS X we can get the real binary from the app bundle
+    if mozinfo.isMac:
+        plist = '%s/Contents/Info.plist' % path
+        assert os.path.isfile(plist), '"%s" has not been found.' % plist
+
+        binary = os.path.join(path, 'Contents/MacOS/',
+                              readPlist(plist)['CFBundleExecutable'])
+
+    else:
+        app_name = app_name.lower()
+
+        if mozinfo.isWin:
+            app_name = app_name + '.exe'
+
+        for root, dirs, files in os.walk(path):
+            for filename in files:
+                # os.access evaluates to False for some reason, so not using it
+                if filename.lower() == app_name:
+                    binary = os.path.realpath(os.path.join(root, filename))
+                    break
+
+    if not binary:
+        # The expected binary has not been found. Make sure we clean the
+        # install folder to remove any traces
+        shutil.rmtree(path)
+
+        raise InvalidBinary('"%s" does not contain a valid binary.' % path)
+
+    return binary
+
+
+def install(src, dest):
+    """Install a zip, exe, tar.gz, tar.bz2 or dmg file, and return the path of
+    the installation folder.
+
+    Arguments:
+    src  -- the path to the install file
+    dest -- the path to install to (to ensure we do not overwrite any existent
+                                    files the folder should not exist yet)
+
     """
     src = os.path.realpath(src)
-    assert(os.path.isfile(src))
-    if not dest:
-        dest = os.path.dirname(src)
+    dest = os.path.realpath(dest)
+
+    if not is_installer(src):
+        raise InvalidSource(src + ' is not a recognized file type ' +
+                                  '(zip, exe, tar.gz, tar.bz2 or dmg)')
+
+    if not os.path.exists(dest):
+        os.makedirs(dest)
 
     trbk = None
     try:
         install_dir = None
         if zipfile.is_zipfile(src) or tarfile.is_tarfile(src):
             install_dir = _extract(src, dest)[0]
-        elif mozinfo.isMac and src.lower().endswith(".dmg"):
+        elif src.lower().endswith('.dmg'):
             install_dir = _install_dmg(src, dest)
-        elif mozinfo.isWin and os.access(src, os.X_OK):
+        elif src.lower().endswith('.exe'):
             install_dir = _install_exe(src, dest)
-        else:
-            raise InvalidSource(src + " is not a recognized file type " +
-                                      "(zip, exe, tar.gz, tar.bz2 or dmg)")
-    except InvalidSource, e:
-        raise
+
+        return install_dir
+
     except Exception, e:
         cls, exc, trbk = sys.exc_info()
-        install_error = InstallError("Failed to install %s" % src)
-        raise install_error.__class__, install_error, trbk
+        error = InstallError('Failed to install "%s"' % src)
+        raise InstallError, error, trbk
+
     finally:
         # trbk won't get GC'ed due to circular reference
         # http://docs.python.org/library/sys.html#sys.exc_info
         del trbk
 
-    if install_dir:
-        return get_binary(install_dir, apps=apps)
+
+def is_installer(src):
+    """Tests if the given file is a valid installer package.
 
-def get_binary(path, apps=_default_apps):
+    Supported types:
+    Linux:   tar.gz, tar.bz2
+    Mac:     dmg
+    Windows: zip, exe
+
+    Arguments:
+    src -- the path to the install file
+
     """
-    Finds the binary in the specified path
-    path - the path within which to search for the binary
-    returns - the full path to the binary in the folder
-              or None if the binary cannot be found
+    src = os.path.realpath(src)
+
+    if not os.path.isfile(src):
+        return False
+
+    if mozinfo.isLinux:
+        return tarfile.is_tarfile(src)
+    elif mozinfo.isMac:
+        return src.lower().endswith('.dmg')
+    elif mozinfo.isWin:
+        return src.lower().endswith('.exe') or zipfile.is_zipfile(src)
+
+
+def uninstall(install_folder):
+    """Uninstalls the application in the specified path. If it has been
+    installed via an installer on Windows, use the uninstaller first.
+
+    Arguments:
+    install_folder -- the path of the installation folder
+
     """
+    install_folder = os.path.realpath(install_folder)
+    assert os.path.isdir(install_folder), \
+        'installation folder "%s" exists.' % install_folder
+
+    # On Windows we have to use the uninstaller. If it's not available fallback
+    # to the directory removal code
     if mozinfo.isWin:
-        apps = [app + ".exe" for app in apps]
-    for root, dirs, files in os.walk(path):
-        for filename in files:
-            # os.access evaluates to False for some reason, so not using it
-            if filename in apps:
-                return os.path.realpath(os.path.join(root, filename))
+        uninstall_folder = '%s\uninstall' % install_folder
+        log_file = '%s\uninstall.log' % uninstall_folder
 
-def _extract(path, extdir=None, delete=False):
-    """
-    Takes in a tar or zip file and extracts it to extdir
-    If extdir is not specified, extracts to os.path.dirname(path)
-    If delete is set to True, deletes the bundle at path
-    Returns the list of top level files that were extracted
+        if os.path.isfile(log_file):
+            trbk = None
+            try:
+                cmdArgs = ['%s\uninstall\helper.exe' % install_folder, '/S']
+                result = subprocess.call(cmdArgs)
+                if not result is 0:
+                    raise Exception('Execution of uninstaller failed.')
+
+                # The uninstaller spawns another process so the subprocess call
+                # returns immediately. We have to wait until the uninstall
+                # folder has been removed or until we run into a timeout.
+                end_time = time.time() + TIMEOUT_UNINSTALL
+                while os.path.exists(uninstall_folder):
+                    time.sleep(1)
+
+                    if time.time() > end_time:
+                        raise Exception('Failure removing uninstall folder.')
+
+            except Exception, e:
+                cls, exc, trbk = sys.exc_info()
+                error = UninstallError('Failed to uninstall %s' % install_folder)
+                raise UninstallError, error, trbk
+
+            finally:
+                # trbk won't get GC'ed due to circular reference
+                # http://docs.python.org/library/sys.html#sys.exc_info
+                del trbk
+
+    # Ensure that we remove any trace of the installation. Even the uninstaller
+    # on Windows leaves files behind we have to explicitely remove.
+    shutil.rmtree(install_folder)
+
+
+def _extract(src, dest):
+    """Extract a tar or zip file into the destination folder and return the
+    application folder.
+
+    Arguments:
+    src -- archive which has to be extracted
+    dest -- the path to extract to
+
     """
-    assert not os.path.isfile(extdir), "extdir cannot be a file"
-    if extdir is None:
-        extdir = os.path.dirname(path)
-    elif not os.path.isdir(extdir):
-        os.makedirs(extdir)
-    if zipfile.is_zipfile(path):
-        bundle = zipfile.ZipFile(path)
+    if zipfile.is_zipfile(src):
+        bundle = zipfile.ZipFile(src)
         namelist = bundle.namelist()
+
         if hasattr(bundle, 'extractall'):
-            bundle.extractall(path=extdir)
-        # zipfile.extractall doesn't exist in Python 2.5
+            # zipfile.extractall doesn't exist in Python 2.5
+            bundle.extractall(path=dest)
         else:
             for name in namelist:
-                filename = os.path.realpath(os.path.join(extdir, name))
-                if name.endswith("/"):
+                filename = os.path.realpath(os.path.join(dest, name))
+                if name.endswith('/'):
                     os.makedirs(filename)
                 else:
                     path = os.path.dirname(filename)
                     if not os.path.isdir(path):
                         os.makedirs(path)
-                    dest = open(filename, "wb")
+                    dest = open(filename, 'wb')
                     dest.write(bundle.read(name))
                     dest.close()
-    elif tarfile.is_tarfile(path):
-        bundle = tarfile.open(path)
+
+    elif tarfile.is_tarfile(src):
+        bundle = tarfile.open(src)
         namelist = bundle.getnames()
+
         if hasattr(bundle, 'extractall'):
-            bundle.extractall(path=extdir)
-        # tarfile.extractall doesn't exist in Python 2.4
+            # tarfile.extractall doesn't exist in Python 2.4
+            bundle.extractall(path=dest)
         else:
             for name in namelist:
-                bundle.extract(name, path=extdir)
+                bundle.extract(name, path=dest)
     else:
         return
+
     bundle.close()
-    if delete:
-        os.remove(path)
+
     # namelist returns paths with forward slashes even in windows
-    top_level_files = [os.path.join(extdir, name) for name in namelist
+    top_level_files = [os.path.join(dest, name) for name in namelist
                              if len(name.rstrip('/').split('/')) == 1]
+
     # namelist doesn't include folders, append these to the list
     for name in namelist:
-        root = os.path.join(extdir, name[:name.find('/')])
+        root = os.path.join(dest, name[:name.find('/')])
         if root not in top_level_files:
             top_level_files.append(root)
+
     return top_level_files
 
+
 def _install_dmg(src, dest):
-    proc = subprocess.Popen("hdiutil attach " + src,
-                            shell=True,
-                            stdout=subprocess.PIPE)
+    """Extract a dmg file into the destination folder and return the
+    application folder.
+
+    Arguments:
+    src -- DMG image which has to be extracted
+    dest -- the path to extract to
+
+    """
     try:
+        proc = subprocess.Popen('hdiutil attach %s' % src,
+                                shell=True,
+                                stdout=subprocess.PIPE)
+
         for data in proc.communicate()[0].split():
-            if data.find("/Volumes/") != -1:
+            if data.find('/Volumes/') != -1:
                 appDir = data
                 break
+
         for appFile in os.listdir(appDir):
-            if appFile.endswith(".app"):
-                 appName = appFile
-                 break
+            if appFile.endswith('.app'):
+                appName = appFile
+                break
+
+        mounted_path = os.path.join(appDir, appName)
 
         dest = os.path.join(dest, appName)
-        assert not os.path.isfile(dest)
-        if not os.path.isdir(dest):
-            os.makedirs(dest)
-        subprocess.call("cp -r " +
-                        os.path.join(appDir,appName, "*") + " " + dest,
+
+        # copytree() would fail if dest already exists.
+        if os.path.exists(dest):
+            raise InstallError('App bundle "%s" already exists.' % dest)
+
+        shutil.copytree(mounted_path, dest, False)
+
+    finally:
+        subprocess.call('hdiutil detach %s -quiet' % appDir,
                         shell=True)
-    finally:
-        subprocess.call("hdiutil detach " + appDir + " -quiet",
-                        shell=True)
+
     return dest
 
+
 def _install_exe(src, dest):
+    """Run the MSI installer to silently install the application into the
+    destination folder. Return the folder path.
+
+    Arguments:
+    src -- MSI installer to be executed
+    dest -- the path to install to
+
+    """
+    # The installer doesn't automatically create a sub folder. Lets guess the
+    # best name from the src file name
+    filename = os.path.basename(src)
+    dest = os.path.join(dest, filename.split('.')[0])
+
     # possibly gets around UAC in vista (still need to run as administrator)
-    os.environ['__compat_layer'] = "RunAsInvoker"
-    cmd = [src, "/S", "/D=" + os.path.realpath(dest)]
-    subprocess.call(cmd)
+    os.environ['__compat_layer'] = 'RunAsInvoker'
+    cmd = [src, '/S', '/D=%s' % os.path.realpath(dest)]
+
+    # As long as we support Python 2.4 check_call will not be available.
+    result = subprocess.call(cmd)
+    if not result is 0:
+        raise Exception('Execution of installer failed.')
+
     return dest
 
-def cli(argv=sys.argv[1:]):
-    parser = OptionParser()
-    parser.add_option("-s", "--source",
-                      dest="src",
-                      help="Path to installation file. "
-                           "Accepts: zip, exe, tar.bz2, tar.gz, and dmg")
-    parser.add_option("-d", "--destination",
-                      dest="dest",
-                      default=None,
-                      help="[optional] Directory to install application into")
-    parser.add_option("--app", dest="app",
-                      action="append",
-                      default=_default_apps,
-                      help="[optional] Application being installed. "
-                           "Should be lowercase, e.g: "
-                           "firefox, fennec, thunderbird, etc.")
+
+def install_cli(argv=sys.argv[1:]):
+    parser = OptionParser(usage="usage: %prog [options] installer")
+    parser.add_option('-d', '--destination',
+                      dest='dest',
+                      default=os.getcwd(),
+                      help='Directory to install application into. '
+                           '[default: "%default"]')
+    parser.add_option('--app', dest='app',
+                      default='firefox',
+                      help='Application being installed. [default: %default]')
 
     (options, args) = parser.parse_args(argv)
-    if not options.src or not os.path.exists(options.src):
-        print "Error: must specify valid source"
-        return 2
+    if not len(args) == 1:
+        parser.error('An installer file has to be specified.')
+
+    src = args[0]
 
     # Run it
-    if os.path.isdir(options.src):
-        binary = get_binary(options.src, apps=options.app)
+    if os.path.isdir(src):
+        binary = get_binary(src, app_name=options.app)
     else:
-        binary = install(options.src, dest=options.dest, apps=options.app)
+        install_path = install(src, options.dest)
+        binary = get_binary(install_path, app_name=options.app)
+
     print binary
 
-class InvalidSource(Exception):
-    """
-    Thrown when the specified source is not a recognized
-    file type (zip, exe, tar.gz, tar.bz2 or dmg)
-    """
+
+def uninstall_cli(argv=sys.argv[1:]):
+    parser = OptionParser(usage="usage: %prog install_path")
 
-class InstallError(Exception):
-    """
-    Thrown when the installation fails. Includes traceback
-    if available.
-    """
+    (options, args) = parser.parse_args(argv)
+    if not len(args) == 1:
+        parser.error('An installation path has to be specified.')
 
-if __name__ == "__main__":
-    sys.exit(cli())
+    # Run it
+    uninstall(argv[0])
+
--- a/testing/mozbase/mozinstall/setup.py
+++ b/testing/mozbase/mozinstall/setup.py
@@ -1,44 +1,47 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 from setuptools import setup, find_packages
 
 try:
     here = os.path.dirname(os.path.abspath(__file__))
     description = file(os.path.join(here, 'README.md')).read()
 except IOError:
     description = None
 
-version = '0.3'
+version = '1.1'
 
-deps = ['mozinfo']
+deps = ['mozinfo==0.3.3']
 
 setup(name='mozInstall',
       version=version,
-      description="This is a utility package for installing Mozilla applications on various platforms.",
+      description="This is a utility package for installing and uninstalling "
+                  "Mozilla applications on various platforms.",
       long_description=description,
+      # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       classifiers=['Environment :: Console',
                    'Intended Audience :: Developers',
-                   'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)',
+                   'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
                    'Natural Language :: English',
                    'Operating System :: OS Independent',
                    'Programming Language :: Python',
                    'Topic :: Software Development :: Libraries :: Python Modules',
-                  ], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+                  ],
       keywords='mozilla',
-      author='mdas',
-      author_email='mdas@mozilla.com',
+      author='Mozilla Automation and Tools team',
+      author_email='tools@lists.mozilla.org',
       url='https://github.com/mozilla/mozbase',
-      license='MPL',
+      license='MPL 2.0',
       packages=find_packages(exclude=['legacy']),
       include_package_data=True,
       zip_safe=False,
       install_requires=deps,
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
-      mozinstall = mozinstall:cli
+      mozinstall = mozinstall:install_cli
+      mozuninstall = mozinstall:uninstall_cli
       """,
       )
--- a/testing/mozbase/mozlog/README.md
+++ b/testing/mozbase/mozlog/README.md
@@ -1,12 +1,8 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
 [Mozlog](https://github.com/mozilla/mozbase/tree/master/mozlog)
 is a python package intended to simplify and standardize logs in the Mozilla universe. 
 It wraps around python's [logging](http://docs.python.org/library/logging.html) 
 module and adds some additional functionality.
 
 # Usage
 
 Import mozlog instead of [logging](http://docs.python.org/library/logging.html) 
--- a/testing/mozbase/mozlog/mozlog/__init__.py
+++ b/testing/mozbase/mozlog/mozlog/__init__.py
@@ -1,4 +1,5 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
 from logger import *
--- a/testing/mozbase/mozlog/mozlog/logger.py
+++ b/testing/mozbase/mozlog/mozlog/logger.py
@@ -1,17 +1,18 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
 from logging import getLogger as getSysLogger
 from logging import *
 # Some of the build slave environments don't see the following when doing
 # 'from logging import *'
 # see https://bugzilla.mozilla.org/show_bug.cgi?id=700415#c35
-from logging import getLoggerClass, addLevelName, setLoggerClass
+from logging import getLoggerClass, addLevelName, setLoggerClass, shutdown
 
 _default_level = INFO
 _LoggerClass = getLoggerClass()
 
 # Define mozlog specific log levels
 START      = _default_level + 1
 END        = _default_level + 2
 PASS       = _default_level + 3
--- a/testing/mozbase/mozlog/setup.py
+++ b/testing/mozbase/mozlog/setup.py
@@ -1,18 +1,18 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 import sys
 from setuptools import setup, find_packages
 
 PACKAGE_NAME = "mozlog"
-PACKAGE_VERSION = "1.0"
+PACKAGE_VERSION = "1.1"
 
 desc = """Robust log handling specialized for logging in the Mozilla universe"""
 # take description from README
 here = os.path.dirname(os.path.abspath(__file__))
 try:
     description = file(os.path.join(here, 'README.md')).read()
 except IOError, OSError:
     description = ''
--- a/testing/mozbase/mozprocess/README.md
+++ b/testing/mozbase/mozprocess/README.md
@@ -1,12 +1,8 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
 [mozprocess](https://github.com/mozilla/mozbase/tree/master/mozprocess)
 provides python process management via an operating system
 and platform transparent interface to Mozilla platforms of interest.
 Mozprocess aims to provide the ability
 to robustly terminate a process (by timeout or otherwise), along with
 any child processes, on Windows, OS X, and Linux. Mozprocess utilizes
 and extends `subprocess.Popen` to these ends.
 
--- a/testing/mozbase/mozprocess/mozprocess/__init__.py
+++ b/testing/mozbase/mozprocess/mozprocess/__init__.py
@@ -1,5 +1,5 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from processhandler import *
--- a/testing/mozbase/mozprocess/mozprocess/pid.py
+++ b/testing/mozbase/mozprocess/mozprocess/pid.py
@@ -1,13 +1,13 @@
 #!/usr/bin/env python
 
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 import mozinfo
 import shlex
 import subprocess
 import sys
 
 # determine the platform-specific invocation of `ps`
--- a/testing/mozbase/mozprocess/mozprocess/processhandler.py
+++ b/testing/mozbase/mozprocess/mozprocess/processhandler.py
@@ -1,11 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import logging
 import mozinfo
 import os
 import select
 import signal
 import subprocess
 import sys
@@ -106,24 +106,25 @@ class ProcessHandlerMixin(object):
                     pass
             else:
                 if not self._ignore_children:
                     try:
                         os.killpg(self.pid, signal.SIGKILL)
                     except BaseException, e:
                         if getattr(e, "errno", None) != 3:
                             # Error 3 is "no such process", which is ok
-                            print >> sys.stderr, "Could not kill process, could not find pid: %s" % self.pid
+                            print >> sys.stdout, "Could not kill process, could not find pid: %s, assuming it's already dead" % self.pid
                 else:
                     os.kill(self.pid, signal.SIGKILL)
                 if self.returncode is None:
                     self.returncode = subprocess.Popen._internal_poll(self)
 
             self._cleanup()
-            return self.returncode
+
+        return self.returncode
 
         def wait(self):
             """ Popen.wait
                 Called to wait for a running process to shut down and return
                 its exit code
                 Returns the main process's exit code
             """
             # This call will be different for each OS
@@ -360,17 +361,18 @@ falling back to not using job objects fo
                         # child processes to shutdown before killing them with extreme prejudice.
                         item = self._process_events.get(timeout=self.MAX_IOCOMPLETION_PORT_NOTIFICATION_DELAY +
                                                                 self.MAX_PROCESS_KILL_DELAY)
                         if item[self.pid] == 'FINISHED':
                             self._process_events.task_done()
                     except:
                         err = "IO Completion Port failed to signal process shutdown"
                     # Either way, let's try to get this code
-                    self.returncode = winprocess.GetExitCodeProcess(self._handle)
+                    if self._handle:
+                        self.returncode = winprocess.GetExitCodeProcess(self._handle)
                     self._cleanup()
 
                     if err is not None:
                         raise OSError(err)
 
 
                 else:
                     # Not managing with job objects, so all we can reasonably do
@@ -477,17 +479,17 @@ falling back to not using job objects fo
 
             def _cleanup(self):
                 pass
 
     def __init__(self,
                  cmd,
                  args=None,
                  cwd=None,
-                 env=os.environ.copy(),
+                 env=None,
                  ignore_children = False,
                  processOutputLine=(),
                  onTimeout=(),
                  onFinish=(),
                  **kwargs):
         """
         cmd = Command to run
         args = array of arguments (defaults to None)
@@ -502,21 +504,24 @@ falling back to not using job objects fo
         NOTE: Child processes will be tracked by default.  If for any reason
         we are unable to track child processes and ignore_children is set to False,
         then we will fall back to only tracking the root process.  The fallback
         will be logged.
         """
         self.cmd = cmd
         self.args = args
         self.cwd = cwd
-        self.env = env
         self.didTimeout = False
         self._ignore_children = ignore_children
         self.keywordargs = kwargs
 
+        if env is None:
+            env = os.environ.copy()
+        self.env = env
+
         # handlers
         self.processOutputLineHandlers = list(processOutputLine)
         self.onTimeoutHandlers = list(onTimeout)
         self.onFinishHandlers = list(onFinish)
 
         # It is common for people to pass in the entire array with the cmd and
         # the args together since this is how Popen uses it.  Allow for that.
         if not isinstance(self.cmd, list):
@@ -618,17 +623,16 @@ falling back to not using job objects fo
 
         (line, self.didTimeout) = self.readWithTimeout(logsource, lineReadTimeout)
         while line != "" and not self.didTimeout:
             self.processOutputLine(line.rstrip())
             if timeout:
                 lineReadTimeout = timeout - (datetime.now() - self.startTime).seconds
             (line, self.didTimeout) = self.readWithTimeout(logsource, lineReadTimeout)
 
-
         if self.didTimeout:
             self.proc.kill()
             self.onTimeout()
         else:
             self.onFinish()
 
         status = self.proc.wait()
         return status
@@ -663,17 +667,17 @@ falling back to not using job objects fo
             return ('', True)
 
     else:
         # Generic
         def _readWithTimeout(self, f, timeout):
             try:
                 (r, w, e) = select.select([f], [], [], timeout)
             except:
-                # TODO: return a blank line?
+                # return a blank line
                 return ('', True)
 
             if len(r) == 0:
                 return ('', True)
             return (f.readline(), False)
 
 
 ### default output handlers
@@ -703,30 +707,35 @@ class LogOutput(object):
             self.file = file(self.filename, 'a')
         self.file.write(line + '\n')
         self.file.flush()
 
     def __del__(self):
         if self.file is not None:
             self.file.close()
 
+
 ### front end class with the default handlers
 
 class ProcessHandler(ProcessHandlerMixin):
 
     def __init__(self, cmd, logfile=None, storeOutput=True, **kwargs):
         """
         If storeOutput=True, the output produced by the process will be saved
         as self.output.
 
         If logfile is not None, the output produced by the process will be
         appended to the given file.
         """
 
-        kwargs.setdefault('processOutputLine', []).append(print_output)
+        kwargs.setdefault('processOutputLine', [])
+
+        # Print to standard output only if no outputline provided
+        if not kwargs['processOutputLine']:
+            kwargs['processOutputLine'].append(print_output)
 
         if logfile:
             logoutput = LogOutput(logfile)
             kwargs['processOutputLine'].append(logoutput)
 
         self.output = None
         if storeOutput:
             storeoutput = StoreOutput()
--- a/testing/mozbase/mozprocess/mozprocess/qijo.py
+++ b/testing/mozbase/mozprocess/mozprocess/qijo.py
@@ -1,11 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE, addressof, c_size_t, c_ulong
 from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LARGE_INTEGER
 
 LPVOID = c_void_p
 LPDWORD = POINTER(DWORD)
 SIZE_T = c_size_t
 ULONG_PTR = POINTER(c_ulong)
--- a/testing/mozbase/mozprocess/mozprocess/wpk.py
+++ b/testing/mozbase/mozprocess/mozprocess/wpk.py
@@ -1,12 +1,11 @@
-#!/usr/bin/env python
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from ctypes import sizeof, windll, addressof, c_wchar, create_unicode_buffer
 from ctypes.wintypes import DWORD, HANDLE
 
 PROCESS_TERMINATE = 0x0001
 PROCESS_QUERY_INFORMATION = 0x0400
 PROCESS_VM_READ = 0x0010
 
--- a/testing/mozbase/mozprocess/setup.py
+++ b/testing/mozbase/mozprocess/setup.py
@@ -1,34 +1,41 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 from setuptools import setup, find_packages
 
-version = '0.1b2'
+PACKAGE_VERSION = '0.3'
 
 # take description from README
 here = os.path.dirname(os.path.abspath(__file__))
 try:
     description = file(os.path.join(here, 'README.md')).read()
 except (OSError, IOError):
     description = ''
 
 setup(name='mozprocess',
-      version=version,
+      version=PACKAGE_VERSION,
       description="Mozilla-authored process handling",
       long_description=description,
-      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
-      keywords='',
-      author='Mozilla Automation and Testing Team',
+      classifiers=['Environment :: Console',
+                   'Intended Audience :: Developers',
+                   'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
+                   'Natural Language :: English',
+                   'Operating System :: OS Independent',
+                   'Programming Language :: Python',
+                   'Topic :: Software Development :: Libraries :: Python Modules',
+                   ],
+      keywords='mozilla',
+      author='Mozilla Automation and Tools team',
       author_email='tools@lists.mozilla.com',
       url='https://github.com/mozilla/mozbase/tree/master/mozprocess',
-      license='MPL',
+      license='MPL 2.0',
       packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
       include_package_data=True,
       zip_safe=False,
       install_requires=['mozinfo'],
       entry_points="""
       # -*- Entry points: -*-
       """,
       )
--- a/testing/mozbase/mozprocess/tests/mozprocess1.py
+++ b/testing/mozbase/mozprocess/tests/mozprocess1.py
@@ -1,13 +1,11 @@
-#!/usr/bin/env python
-
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 import subprocess
 import sys
 import unittest
 from time import sleep
 
 from mozprocess import processhandler
--- a/testing/mozbase/mozprocess/tests/mozprocess2.py
+++ b/testing/mozbase/mozprocess/tests/mozprocess2.py
@@ -1,13 +1,11 @@
-#!/usr/bin/env python
-
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 import subprocess
 import sys
 import unittest
 from time import sleep
 
 from mozprocess import processhandler
--- a/testing/mozbase/mozprocess/tests/proclaunch.c
+++ b/testing/mozbase/mozprocess/tests/proclaunch.c
@@ -1,11 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include <stdio.h>
 #include <stdlib.h>
 #include "iniparser.h"
 
 #ifdef _WIN32
 #include <windows.h>
 #include <tchar.h>
--- a/testing/mozbase/mozprofile/README.md
+++ b/testing/mozbase/mozprofile/README.md
@@ -1,12 +1,8 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
 [Mozprofile](https://github.com/mozilla/mozbase/tree/master/mozprofile)
 is a python tool for creating and managing profiles for Mozilla's
 applications (Firefox, Thunderbird, etc.). In addition to creating profiles,
 mozprofile can install [addons](https://developer.mozilla.org/en/addons)
 and set
 [preferences](https://developer.mozilla.org/En/A_Brief_Guide_to_Mozilla_Preferences).
 Mozprofile can be utilized from the command line or as an API.
 
--- a/testing/mozbase/mozprofile/mozprofile/__init__.py
+++ b/testing/mozbase/mozprofile/mozprofile/__init__.py
@@ -1,6 +1,7 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
 from profile import *
 from addons import *
 from cli import *
--- a/testing/mozbase/mozprofile/mozprofile/addons.py
+++ b/testing/mozbase/mozprofile/mozprofile/addons.py
@@ -1,15 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 import shutil
-import sys
 import tempfile
 import urllib2
 import zipfile
 from distutils import dir_util
 from manifestparser import ManifestParser
 from xml.dom import minidom
 
 # Needed for the AMO's rest API - https://developer.mozilla.org/en/addons.mozilla.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API
@@ -20,48 +19,51 @@ class AddonManager(object):
     Handles all operations regarding addons including: installing and cleaning addons
     """
 
     def __init__(self, profile):
         """
         profile - the path to the profile for which we install addons
         """
         self.profile = profile
+
+        # information needed for profile reset:
+        # https://github.com/mozilla/mozbase/blob/270a857328b130860d1b1b512e23899557a3c8f7/mozprofile/mozprofile/profile.py#L93
         self.installed_addons = []
-        # keeps track of addons and manifests that were passed to install_addons
-        self.addons = []
-        self.manifests = []
+        self.installed_manifests = []
 
+        # addons that we've installed; needed for cleanup
+        self._addon_dirs = []
 
     def install_addons(self, addons=None, manifests=None):
         """
         Installs all types of addons
         addons - a list of addon paths to install
         manifest - a list of addon manifests to install
         """
         # install addon paths
         if addons:
             if isinstance(addons, basestring):
                 addons = [addons]
+            self.installed_addons.extend(addons)
             for addon in addons:
                 self.install_from_path(addon)
         # install addon manifests
         if manifests:
             if isinstance(manifests, basestring):
                 manifests = [manifests]
             for manifest in manifests:
                 self.install_from_manifest(manifest)
-
+            self.installed_manifests.extended(manifests)
 
     def install_from_manifest(self, filepath):
         """
         Installs addons from a manifest
         filepath - path to the manifest of addons to install
         """
-        self.manifests.append(filepath)
         manifest = ManifestParser()
         manifest.read(filepath)
         addons = manifest.get()
 
         for addon in addons:
             if '://' in addon['path'] or os.path.exists(addon['path']):
                 self.install_from_path(addon['path'])
                 continue
@@ -150,17 +152,16 @@ class AddonManager(object):
 
     def install_from_path(self, path, unpack=False):
         """
         Installs addon from a filepath, url
         or directory of addons in the profile.
         - path: url, path to .xpi, or directory of addons
         - unpack: whether to unpack unless specified otherwise in the install.rdf
         """
-        self.addons.append(path)
 
         # if the addon is a url, download it
         # note that this won't work with protocols urllib2 doesn't support
         if '://' in path:
             response = urllib2.urlopen(path)
             fd, path = tempfile.mkstemp(suffix='.xpi')
             os.write(fd, response.read())
             os.close(fd)
@@ -203,23 +204,23 @@ class AddonManager(object):
             extensions_path = os.path.join(self.profile, 'extensions')
             addon_path = os.path.join(extensions_path, addon_id)
             if not unpack and not addon_details['unpack'] and xpifile:
                 if not os.path.exists(extensions_path):
                     os.makedirs(extensions_path)
                 shutil.copy(xpifile, addon_path + '.xpi')
             else:
                 dir_util.copy_tree(addon, addon_path, preserve_symlinks=1)
-                self.installed_addons.append(addon_path)
+                self._addon_dirs.append(addon_path)
 
             # remove the temporary directory, if any
             if tmpdir:
                 dir_util.remove_tree(tmpdir)
 
         # remove temporary file, if any
         if tmpfile:
             os.remove(tmpfile)
 
     def clean_addons(self):
         """Cleans up addons in the profile."""
-        for addon in self.installed_addons:
+        for addon in self._addon_dirs:
             if os.path.isdir(addon):
                 dir_util.remove_tree(addon)
--- a/testing/mozbase/mozprofile/mozprofile/cli.py
+++ b/testing/mozbase/mozprofile/mozprofile/cli.py
@@ -1,11 +1,13 @@
+#!/usr/bin/env python
+
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 """
 Creates and/or modifies a Firefox profile.
 The profile can be modified by passing in addons to install or preferences to set.
 If no profile is specified, a new profile is created and the path of the resulting profile is printed.
 """
 
 import sys
--- a/testing/mozbase/mozprofile/mozprofile/permissions.py
+++ b/testing/mozbase/mozprofile/mozprofile/permissions.py
@@ -1,11 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 
 """
 add permissions to the profile
 """
 
 __all__ = ['MissingPrimaryLocationError', 'MultiplePrimaryLocationsError',
            'DuplicateLocationError', 'BadPortLocationError',
@@ -344,18 +344,22 @@ function FindProxyForURL(url, host)
         prefs.append(("network.proxy.type", 2))
         prefs.append(("network.proxy.autoconfig_url", pacURL))
 
         return prefs
 
     def clean_db(self):
         """Removed permissions added by mozprofile."""
 
+        sqlite_file = os.path.join(self._profileDir, "permissions.sqlite")
+        if not os.path.exists(sqlite_file):
+            return
+
         # Open database and create table
-        permDB = sqlite3.connect(os.path.join(self._profileDir, "permissions.sqlite"))
+        permDB = sqlite3.connect(sqlite_file)
         cursor = permDB.cursor();
 
         # TODO: only delete values that we add, this would require sending in the full permissions object
         cursor.execute("DROP TABLE IF EXISTS moz_hosts");
 
         # Commit and close
         permDB.commit()
         cursor.close()
--- a/testing/mozbase/mozprofile/mozprofile/prefs.py
+++ b/testing/mozbase/mozprofile/mozprofile/prefs.py
@@ -1,12 +1,11 @@
-#!/usr/bin/env python
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 """
 user preferences
 """
 
 import os
 import re
 from ConfigParser import SafeConfigParser as ConfigParser
--- a/testing/mozbase/mozprofile/mozprofile/profile.py
+++ b/testing/mozbase/mozprofile/mozprofile/profile.py
@@ -1,11 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 __all__ = ['Profile', 'FirefoxProfile', 'ThunderbirdProfile']
 
 import os
 import tempfile
 from addons import AddonManager
 from permissions import Permissions
 from shutil import rmtree
@@ -86,18 +86,18 @@ class Profile(object):
         reset the profile to the beginning state
         """
         self.cleanup()
         if self.create_new:
             profile = None
         else:
             profile = self.profile
         self.__init__(profile=profile,
-                      addons=self.addon_manager.addons,
-                      addon_manifests=self.addon_manager.manifests,
+                      addons=self.addon_manager.installed_addons,
+                      addon_manifests=self.addon_manager.installed_manifests,
                       preferences=self._preferences,
                       locations=self._locations,
                       proxy = self._proxy)
 
     def create_new_profile(self):
         """Create a new clean profile in tmp which is a simple empty folder"""
         profile = tempfile.mkdtemp(suffix='.mozrunner')
         return profile
@@ -165,27 +165,31 @@ class Profile(object):
         f = file(os.path.join(self.profile, 'user.js'), 'w')
         f.write(cleaned_prefs)
         f.close()
         return True
 
     def clean_preferences(self):
         """Removed preferences added by mozrunner."""
         for filename in self.written_prefs:
+            if not os.path.exists(os.path.join(self.profile, filename)):
+                # file has been deleted
+                break
             while True:
                 if not self.pop_preferences(filename):
                     break
 
     ### cleanup
 
     def _cleanup_error(self, function, path, excinfo):
         """ Specifically for windows we need to handle the case where the windows
             process has not yet relinquished handles on files, so we do a wait/try
             construct and timeout if we can't get a clear road to deletion
         """
+
         try:
             from exceptions import WindowsError
             from time import sleep
             def is_file_locked():
                 return excinfo[0] is WindowsError and excinfo[1].winerror == 32
 
             if excinfo[0] is WindowsError and excinfo[1].winerror == 32:
                 # Then we're on windows, wait to see if the file gets unlocked
@@ -197,17 +201,16 @@ class Profile(object):
                         function(path)
                         break
                     except:
                         count += 1
         except ImportError:
             # We can't re-raise an error, so we'll hope the stuff above us will throw
             pass
 
-
     def cleanup(self):
         """Cleanup operations for the profile."""
         if self.restore:
             if self.create_new:
                 if os.path.exists(self.profile):
                     rmtree(self.profile, onerror=self._cleanup_error)
             else:
                 self.clean_preferences()
--- a/testing/mozbase/mozprofile/setup.py
+++ b/testing/mozbase/mozprofile/setup.py
@@ -1,17 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 import sys
 from setuptools import setup, find_packages
 
-version = '0.1'
+version = '0.4'
 
 # we only support python 2 right now
 assert sys.version_info[0] == 2
 
 deps = ["ManifestDestiny >= 0.5.4"]
 # version-dependent dependencies
 try:
     import json
@@ -27,26 +27,33 @@ except ImportError:
 here = os.path.dirname(os.path.abspath(__file__))
 try:
     description = file(os.path.join(here, 'README.md')).read()
 except (OSError, IOError):
     description = ''
 
 setup(name='mozprofile',
       version=version,
-      description="handling of Mozilla XUL app profiles",
+      description="Handling of Mozilla Gecko based application profiles",
       long_description=description,
-      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
-      keywords='',
-      author='Mozilla Automation + Testing Team',
-      author_email='mozmill-dev@googlegroups.com',
-      url='http://github.com/mozautomation/mozmill',
-      license='MPL',
+      classifiers=['Environment :: Console',
+                   'Intended Audience :: Developers',
+                   'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
+                   'Natural Language :: English',
+                   'Operating System :: OS Independent',
+                   'Programming Language :: Python',
+                   'Topic :: Software Development :: Libraries :: Python Modules',
+                   ],
+      keywords='mozilla',
+      author='Mozilla Automation and Tools team',
+      author_email='tools@lists.mozilla.com',
+      url='https://github.com/mozilla/mozbase/tree/master/mozprofile',
+      license='MPL 2.0',
       packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
       include_package_data=True,
       zip_safe=False,
       install_requires=deps,
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
       mozprofile = mozprofile:cli
       """,
-      )
+    )
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/bug758250.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+
+import mozprofile
+import os
+import shutil
+import tempfile
+import unittest
+
+here = os.path.dirname(os.path.abspath(__file__))
+
+class Bug758250(unittest.TestCase):
+    """
+    use of --profile in mozrunner just blows away addon sources:
+    https://bugzilla.mozilla.org/show_bug.cgi?id=758250
+    """
+
+    def test_profile_addon_cleanup(self):
+
+        # sanity check: the empty addon should be here
+        empty = os.path.join(here, 'empty')
+        self.assertTrue(os.path.exists(empty))
+        self.assertTrue(os.path.isdir(empty))
+        self.assertTrue(os.path.exists(os.path.join(empty, 'install.rdf')))
+
+        # because we are testing data loss, let's make sure we make a copy
+        tmpdir = tempfile.mktemp()
+        shutil.copytree(empty, tmpdir)
+        self.assertTrue(os.path.exists(os.path.join(tmpdir, 'install.rdf')))
+
+        # make a starter profile
+        profile = mozprofile.FirefoxProfile()
+        path = profile.profile
+
+        # make a new profile based on the old
+        newprofile = mozprofile.FirefoxProfile(profile=path, addons=[tmpdir])
+        newprofile.cleanup()
+
+        # the source addon *should* still exist
+        self.assertTrue(os.path.exists(tmpdir))
+        self.assertTrue(os.path.exists(os.path.join(tmpdir, 'install.rdf')))
+
+        # remove vestiges
+        shutil.rmtree(tmpdir)
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/empty/install.rdf
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+	<Description about="urn:mozilla:install-manifest">
+		<em:id>test-empty@quality.mozilla.org</em:id>
+		<em:version>0.1</em:version>
+		<em:name>Test Extension (empty)</em:name>
+		<em:creator>Mozilla QA</em:creator>
+		<em:homepageURL>http://quality.mozilla.org</em:homepageURL>
+		<em:type>2</em:type>
+
+		<!-- Firefox -->
+		<em:targetApplication>
+			<Description>
+				<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+				<em:minVersion>3.5.*</em:minVersion>
+				<em:maxVersion>*</em:maxVersion>
+			</Description>
+		</em:targetApplication>
+	</Description>  
+</RDF>
--- a/testing/mozbase/mozprofile/tests/manifest.ini
+++ b/testing/mozbase/mozprofile/tests/manifest.ini
@@ -1,4 +1,5 @@
 [addonid.py]
 [server_locations.py]
 [test_preferences.py]
 [permissions.py]
+[bug758250.py]
--- a/testing/mozbase/mozprofile/tests/test_preferences.py
+++ b/testing/mozbase/mozprofile/tests/test_preferences.py
@@ -80,16 +80,41 @@ browser.startup.homepage = http://github
 
         # test a specific section
         _prefs = {'browser.startup.homepage': 'http://github.com/'}
         commandline[-1] = commandline[-1] + ':foo'
         self.compare_generated(_prefs, commandline)
 
         # cleanup
         os.remove(name)
+    
+    def test_reset_should_remove_added_prefs(self):
+        """Check that when we call reset the items we expect are updated"""
+
+        profile = Profile()
+        prefs_file = os.path.join(profile.profile, 'user.js')
+
+        # we shouldn't have any initial preferences
+        initial_prefs = Preferences.read_prefs(prefs_file)
+        self.assertFalse(initial_prefs)
+        initial_prefs = file(prefs_file).read().strip()
+        self.assertFalse(initial_prefs)
+
+        # add some preferences
+        prefs1 = [("mr.t.quotes", "i aint getting on no plane!")]
+        profile.set_preferences(prefs1)
+        self.assertEqual(prefs1, Preferences.read_prefs(prefs_file))
+        lines = file(prefs_file).read().strip().splitlines()
+        self.assertTrue('#MozRunner Prefs Start' in lines)
+        self.assertTrue('#MozRunner Prefs End' in lines)
+
+        profile.reset()
+        self.assertNotEqual(prefs1, \
+                    Preferences.read_prefs(os.path.join(profile.profile, 'user.js')),\
+                            "I pity the fool who left my pref")
 
     def test_magic_markers(self):
         """ensure our magic markers are working"""
 
         profile = Profile()
         prefs_file = os.path.join(profile.profile, 'user.js')
 
         # we shouldn't have any initial preferences
--- a/testing/mozbase/mozrunner/README.md
+++ b/testing/mozbase/mozrunner/README.md
@@ -1,29 +1,25 @@
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
 [mozrunner](https://github.com/mozilla/mozbase/tree/master/mozrunner)
 is a [python package](http://pypi.python.org/pypi/mozrunner)
 which handles running of Mozilla applications.
-mozrunner utilizes [mozprofile](/en/Mozprofile)
+mozrunner utilizes [mozprofile](https://github.com/mozilla/mozbase/tree/master/mozprofile)
 for managing application profiles
-and [mozprocess](/en/Mozprocess) for robust process control.
+and [mozprocess](https://github.com/mozilla/mozbase/tree/master/mozprocess) for robust process control.
 
 mozrunner may be used from the command line or programmatically as an API.
 
 
 # Command Line Usage
 
 The `mozrunner` command will launch the application (specified by
 `--app`) from a binary specified with `-b` or as located on the `PATH`.
 
 mozrunner takes the command line options from 
-[mozprofile](/en/Mozprofile) for constructing the profile to be used by 
+[mozprofile](https://github.com/mozilla/mozbase/tree/master/mozprofile) for constructing the profile to be used by 
 the application.
 
 Run `mozrunner --help` for detailed information on the command line
 program.
 
 
 # API Usage
 
--- a/testing/mozbase/mozrunner/mozrunner/__init__.py
+++ b/testing/mozbase/mozrunner/mozrunner/__init__.py
@@ -1,5 +1,5 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from runner import *
--- a/testing/mozbase/mozrunner/mozrunner/runner.py
+++ b/testing/mozbase/mozrunner/mozrunner/runner.py
@@ -1,36 +1,78 @@
+#!/usr/bin/env python
+
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 __all__ = ['Runner', 'ThunderbirdRunner', 'FirefoxRunner', 'runners', 'CLI', 'cli', 'package_metadata']
 
 import mozinfo
 import optparse
 import os
 import platform
+import subprocess
 import sys
 import ConfigParser
 
+from threading import Thread
 from utils import get_metadata_from_egg
 from utils import findInPath
 from mozprofile import *
 from mozprocess.processhandler import ProcessHandler
 
+if mozinfo.isMac:
+    from plistlib import readPlist
+
 package_metadata = get_metadata_from_egg('mozrunner')
 
+# Map of debugging programs to information about them
+# from http://mxr.mozilla.org/mozilla-central/source/build/automationutils.py#59
+debuggers = {'gdb': {'interactive': True,
+                     'args': ['-q', '--args'],},
+             'valgrind': {'interactive': False,
+                          'args': ['--leak-check=full']}
+             }
+
+def debugger_arguments(debugger, arguments=None, interactive=None):
+    """
+    finds debugger arguments from debugger given and defaults
+    * debugger : debugger name or path to debugger
+    * arguments : arguments to the debugger, or None to use defaults
+    * interactive : whether the debugger should be run in interactive mode, or None to use default
+    """
+
+    # find debugger executable if not a file
+    executable = debugger
+    if not os.path.exists(executable):
+        executable = findInPath(debugger)
+    if executable is None:
+        raise Exception("Path to '%s' not found" % debugger)
+
+    # if debugger not in dictionary of knowns return defaults
+    dirname, debugger = os.path.split(debugger)
+    if debugger not in debuggers:
+        return ([executable] + (arguments or []), bool(interactive))
+
+    # otherwise use the dictionary values for arguments unless specified
+    if arguments is None:
+        arguments = debuggers[debugger].get('args', [])
+    if interactive is None:
+        interactive = debuggers[debugger].get('interactive', False)
+    return ([executable] + arguments, interactive)
+
 class Runner(object):
     """Handles all running operations. Finds bins, runs and kills the process."""
 
     profile_class = Profile # profile class to use by default
 
     @classmethod
-    def create(cls, binary, cmdargs=None, env=None, kp_kwargs=None, profile_args=None,
-                                               clean_profile=True, process_class=ProcessHandler):
+    def create(cls, binary=None, cmdargs=None, env=None, kp_kwargs=None, profile_args=None,
+               clean_profile=True, process_class=ProcessHandler):
         profile = cls.profile_class(**(profile_args or {}))
         return cls(profile, binary=binary, cmdargs=cmdargs, env=env, kp_kwargs=kp_kwargs,
                                            clean_profile=clean_profile, process_class=process_class)
 
     def __init__(self, profile, binary, cmdargs=None, env=None,
                  kp_kwargs=None, clean_profile=True, process_class=ProcessHandler):
         self.process_handler = None
         self.process_class = process_class
@@ -39,16 +81,23 @@ class Runner(object):
 
         # find the binary
         self.binary = binary
         if not self.binary:
             raise Exception("Binary not specified")
         if not os.path.exists(self.binary):
             raise OSError("Binary path does not exist: %s" % self.binary)
 
+        # allow Mac binaries to be specified as an app bundle
+        plist = '%s/Contents/Info.plist' % self.binary
+        if mozinfo.isMac and os.path.exists(plist):
+            info = readPlist(plist)
+            self.binary = os.path.join(self.binary, "Contents/MacOS/",
+                                       info['CFBundleExecutable'])
+
         self.cmdargs = cmdargs or []
         _cmdargs = [i for i in self.cmdargs
                     if i != '-foreground']
         if len(_cmdargs) != len(self.cmdargs):
             # foreground should be last; see
             # - https://bugzilla.mozilla.org/show_bug.cgi?id=625614
             # - https://bugzilla.mozilla.org/show_bug.cgi?id=626826
             self.cmdargs = _cmdargs
@@ -97,36 +146,57 @@ class Runner(object):
                 except:
                     repository['%s_%s' % (file, id)] = None
 
         return repository
 
     def is_running(self):
         return self.process_handler is not None
 
-    def start(self):
-        """Run self.command in the proper environment."""
+    def start(self, debug_args=None, interactive=False):
+        """
+        Run self.command in the proper environment.
+        - debug_args: arguments for the debugger
+        """
 
         # ensure you are stopped
         self.stop()
 
         # ensure the profile exists
         if not self.profile.exists():
             self.profile.reset()
-        
+            assert self.profile.exists(), "%s : failure to reset profile" % self.__class__.__name__
+
         cmd = self._wrap_command(self.command+self.cmdargs)
-        # this run uses the managed processhandler
-        self.process_handler = self.process_class(cmd, env=self.env, **self.kp_kwargs)
-        self.process_handler.run()
+
+        # attach a debugger, if specified
+        if debug_args:
+            cmd = list(debug_args) + cmd
+
+        #
+        if interactive:
+            self.process_handler = subprocess.Popen(cmd, env=self.env)
+            # TODO: other arguments
+        else:
+            # this run uses the managed processhandler
+            self.process_handler = self.process_class(cmd, env=self.env, **self.kp_kwargs)
+            self.process_handler.run()
+
+            # Spin a thread to handle reading the output
+            self.outThread = OutputThread(self.process_handler)
+            self.outThread.start()
 
     def wait(self, timeout=None, outputTimeout=None):
         """Wait for the app to exit."""
         if self.process_handler is None:
             return
-        self.process_handler.waitForFinish(timeout=timeout, outputTimeout=outputTimeout)
+        if isinstance(self.process_handler, subprocess.Popen):
+            self.process_handler.wait()
+        else:
+            self.process_handler.waitForFinish(timeout=timeout, outputTimeout=outputTimeout)
         self.process_handler = None
 
     def stop(self):
         """Kill the app"""
         if self.process_handler is None:
             return
         self.process_handler.kill()
         self.process_handler = None
@@ -147,17 +217,17 @@ class Runner(object):
         """
         If running on OS X 10.5 or older, wrap |cmd| so that it will
         be executed as an i386 binary, in case it's a 32-bit/64-bit universal
         binary.
         """
         if mozinfo.isMac and hasattr(platform, 'mac_ver') and \
                                platform.mac_ver()[0][:4] < '10.6':
             return ["arch", "-arch", "i386"] + cmd
-        return cmd 
+        return cmd
 
     __del__ = cleanup
 
 
 class FirefoxRunner(Runner):
     """Specialized Runner subclass for running Firefox."""
 
     profile_class = FirefoxProfile
@@ -165,38 +235,30 @@ class FirefoxRunner(Runner):
     def __init__(self, profile, binary=None, **kwargs):
 
         # take the binary from BROWSER_PATH environment variable
         if (not binary) and 'BROWSER_PATH' in os.environ:
             binary = os.environ['BROWSER_PATH']
 
         Runner.__init__(self, profile, binary, **kwargs)
 
-        # Find application version number
-        appdir = os.path.dirname(os.path.realpath(self.binary))
-        appini = ConfigParser.RawConfigParser()
-        appini.read(os.path.join(appdir, 'application.ini'))
-        # Version needs to be of the form 3.6 or 4.0b and not the whole string
-        version = appini.get('App', 'Version').rstrip('0123456789pre').rstrip('.')
-
-        # Disable compatibility check. See:
-        # - http://kb.mozillazine.org/Extensions.checkCompatibility
-        # - https://bugzilla.mozilla.org/show_bug.cgi?id=659048
-        preference = {'extensions.checkCompatibility.' + version: False,
-                      'extensions.checkCompatibility.nightly': False}
-        self.profile.set_preferences(preference)
-
-
 class ThunderbirdRunner(Runner):
     """Specialized Runner subclass for running Thunderbird"""
     profile_class = ThunderbirdProfile
 
 runners = {'firefox': FirefoxRunner,
            'thunderbird': ThunderbirdRunner}
 
+class OutputThread(Thread):
+    def __init__(self, prochandler):
+        Thread.__init__(self)
+        self.ph = prochandler
+    def run(self):
+        self.ph.waitForFinish()
+
 class CLI(MozProfileCLI):
     """Command line interface."""
 
     module = "mozrunner"
 
     def __init__(self, args=sys.argv[1:]):
         """
         Setup command line parser and parse arguments
@@ -234,16 +296,24 @@ class CLI(MozProfileCLI):
         parser.add_option('-b', "--binary",
                           dest="binary", help="Binary path.",
                           metavar=None, default=None)
         parser.add_option('--app', dest='app', default='firefox',
                           help="Application to use [DEFAULT: %default]")
         parser.add_option('--app-arg', dest='appArgs',
                           default=[], action='append',
                           help="provides an argument to the test application")
+        parser.add_option('--debugger', dest='debugger',
+                          help="run under a debugger, e.g. gdb or valgrind")
+        parser.add_option('--debugger-args', dest='debugger_args',
+                          action='append', default=None,
+                          help="arguments to the debugger")
+        parser.add_option('--interactive', dest='interactive',
+                          action='store_true',
+                          help="run the program interactively")
         if self.metadata:
             parser.add_option("--info", dest="info", default=False,
                               action="store_true",
                               help="Print module information")
 
     ### methods for introspecting data
 
     def get_metadata_from_egg(self):
@@ -279,20 +349,34 @@ class CLI(MozProfileCLI):
     def create_runner(self):
         return self.runner_class.create(**self.runner_args())
 
     def run(self):
         runner = self.create_runner()
         self.start(runner)
         runner.cleanup()
 
+    def debugger_arguments(self):
+        """
+        returns a 2-tuple of debugger arguments:
+        (debugger_arguments, interactive)
+        """
+        debug_args = self.options.debugger_args
+        interactive = self.options.interactive
+        if self.options.debugger:
+            debug_args, interactive = debugger_arguments(self.options.debugger)
+        return debug_args, interactive
+
     def start(self, runner):
         """Starts the runner and waits for Firefox to exit or Keyboard Interrupt.
         Shoule be overwritten to provide custom running of the runner instance."""
-        runner.start()
+
+        # attach a debugger if specified
+        debug_args, interactive = self.debugger_arguments()
+        runner.start(debug_args=debug_args, interactive=interactive)
         print 'Starting:', ' '.join(runner.command)
         try:
             runner.wait()
         except KeyboardInterrupt:
             runner.stop()
 
 
 def cli(args=sys.argv[1:]):
--- a/testing/mozbase/mozrunner/mozrunner/utils.py
+++ b/testing/mozbase/mozrunner/mozrunner/utils.py
@@ -1,12 +1,11 @@
-#!/usr/bin/env python
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 """
 utility functions for mozrunner
 """
 
 __all__ = ['findInPath', 'get_metadata_from_egg']
 
 import mozinfo
--- a/testing/mozbase/mozrunner/setup.py
+++ b/testing/mozbase/mozrunner/setup.py
@@ -1,51 +1,53 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 import sys
 from setuptools import setup, find_packages
 
 PACKAGE_NAME = "mozrunner"
-PACKAGE_VERSION = "5.1"
+PACKAGE_VERSION = '5.7'
 
 desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
 # take description from README
 here = os.path.dirname(os.path.abspath(__file__))
 try:
     description = file(os.path.join(here, 'README.md')).read()
 except (OSError, IOError):
     description = ''
 
-deps = ['mozinfo',
-        'mozprocess',
-        'mozprofile >= 0.1',
+deps = ['mozinfo == 0.3.3',
+        'mozprocess == 0.3',
+        'mozprofile == 0.4',
        ]
 
 # we only support python 2 right now
 assert sys.version_info[0] == 2
 
 setup(name=PACKAGE_NAME,
       version=PACKAGE_VERSION,
       description=desc,
       long_description=description,
-      author='Mikeal Rogers, Mozilla',
-      author_email='mikeal.rogers@gmail.com',
-      url='http://github.com/mozautomation/mozmill',
-      license='MPL 1.1/GPL 2.0/LGPL 2.1',
+      classifiers=['Environment :: Console',
+                   'Intended Audience :: Developers',
+                   'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
+                   'Natural Language :: English',
+                   'Operating System :: OS Independent',
+                   'Programming Language :: Python',
+                   'Topic :: Software Development :: Libraries :: Python Modules',
+                   ],
+      keywords='mozilla',
+      author='Mozilla Automation and Tools team',
+      author_email='tools@lists.mozilla.com',
+      url='https://github.com/mozilla/mozbase/tree/master/mozrunner',
+      license='MPL 2.0',
       packages=find_packages(exclude=['legacy']),
       zip_safe=False,
+      install_requires = deps,
       entry_points="""
-          [console_scripts]
-          mozrunner = mozrunner:cli
-        """,
-      platforms =['Any'],
-      install_requires = deps,
-      classifiers=['Development Status :: 4 - Beta',
-                   'Environment :: Console',
-                   'Intended Audience :: Developers',
-                   'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)',
-                   'Operating System :: OS Independent',
-                   'Topic :: Software Development :: Libraries :: Python Modules',
-                  ]
-     )
+      # -*- Entry points: -*-
+      [console_scripts]
+      mozrunner = mozrunner:cli
+      """,
+    )
--- a/testing/mozbase/setup_development.py
+++ b/testing/mozbase/setup_development.py
@@ -1,24 +1,23 @@
 #!/usr/bin/env python
+
 # This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
 
 """
 Setup mozbase packages for development.
 
 Packages may be specified as command line arguments.
 If no arguments are given, install all packages.
 
 See https://wiki.mozilla.org/Auto-tools/Projects/MozBase
 """
 
-# XXX note that currently directory names must equal package names
-
 import pkg_resources
 import os
 import sys
 from optparse import OptionParser
 
 from subprocess import PIPE
 try:
     from subprocess import check_call as call
@@ -144,32 +143,42 @@ def main(args=sys.argv[1:]):
 
     if options.list_dependencies:
         # list the package dependencies
         for package in packages:
             print '%s: %s' % dependencies(os.path.join(here, package))
         parser.exit()
 
     # gather dependencies
+    # TODO: version conflict checking
     deps = {}
+    alldeps = {}
     mapping = {} # mapping from subdir name to package name
     # core dependencies
     for package in packages:
         key, value = dependencies(os.path.join(here, package))
         deps[key] = [sanitize_dependency(dep) for dep in value]
         mapping[package] = key
+
+        # keep track of all dependencies for non-mozbase packages
+        for dep in value:
+            alldeps[sanitize_dependency(dep)] = ''.join(dep.split())
+
     # indirect dependencies
     flag = True
     while flag:
         flag = False
         for value in deps.values():
             for dep in value:
                 if dep in all_packages and dep not in deps:
                     key, value = dependencies(os.path.join(here, dep))
                     deps[key] = [sanitize_dependency(dep) for dep in value]
+
+                    for dep in value:
+                        alldeps[sanitize_dependency(dep)] = ''.join(dep.split())
                     mapping[package] = key
                     flag = True
                     break
             if flag:
                 break
 
     # get the remaining names for the mapping
     for package in all_packages:
@@ -188,15 +197,26 @@ def main(args=sys.argv[1:]):
     unrolled = [package for package in unrolled if package in reverse_mapping]
 
     if options.list:
         # list what will be installed
         for package in unrolled:
             print package
         parser.exit()
 
+    # install non-mozbase dependencies
+    # (currently none on modern python)
+    # these need to be installed separately and the --no-deps flag
+    # subsequently used due to a bug in setuptools; see
+    # https://bugzilla.mozilla.org/show_bug.cgi?id=759836
+    pypi_deps = dict([(i, j) for i,j in alldeps.items()
+                      if i not in unrolled])
+    for package, version in pypi_deps.items():
+        # easy_install should be available since we rely on setuptools
+        call(['easy_install', version])
+
     # set up the packages for development
     for package in unrolled:
-        call([sys.executable, 'setup.py', 'develop'],
+        call([sys.executable, 'setup.py', 'develop', '--no-deps'],
              cwd=os.path.join(here, reverse_mapping[package]))
 
 if __name__ == '__main__':
     main()
--- a/testing/mozbase/test-manifest.ini
+++ b/testing/mozbase/test-manifest.ini
@@ -1,13 +1,10 @@
-; This Source Code Form is subject to the terms of the Mozilla Public
-; License, v. 2.0. If a copy of the MPL was not distributed with this
-; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
 # mozbase test manifest, in the format of
 # https://github.com/mozilla/mozbase/blob/master/manifestdestiny/README.txt
 
 # run with
 # https://github.com/mozilla/mozbase/blob/master/test.py
 
 [include:mozprocess/tests/manifest.ini]
 [include:mozprofile/tests/manifest.ini]
 [include:mozhttpd/tests/manifest.ini]
+[include:mozdevice/tests/manifest.ini]
--- a/testing/mozbase/test.py
+++ b/testing/mozbase/test.py
@@ -1,13 +1,9 @@
 #!/usr/bin/env python
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
 
 """
 run mozbase tests from a manifest,
 by default https://github.com/mozilla/mozbase/blob/master/test-manifest.ini
 """
 
 import imp
 import manifestparser