Exemple #1
0
 def __init__(self, options, browsers):
     from internal.browsers import Browsers
     from internal.webpagetest import WebPageTest
     from internal.traffic_shaping import TrafficShaper
     from internal.adb import Adb
     from internal.ios_device import iOSDevice
     self.must_exit = False
     self.options = options
     self.capture_display = None
     self.job = None
     self.task = None
     self.xvfb = None
     self.root_path = os.path.abspath(os.path.dirname(__file__))
     self.wpt = WebPageTest(options, os.path.join(self.root_path, "work"))
     self.persistent_work_dir = self.wpt.get_persistent_dir()
     self.adb = Adb(
         self.options,
         self.persistent_work_dir) if self.options.android else None
     self.ios = iOSDevice(self.options.device) if self.options.iOS else None
     self.browsers = Browsers(options, browsers, self.adb, self.ios)
     self.shaper = TrafficShaper(options)
     atexit.register(self.cleanup)
     signal.signal(signal.SIGTERM, self.signal_handler)
     signal.signal(signal.SIGINT, self.signal_handler)
     self.image_magick = {
         'convert': 'convert',
         'compare': 'compare',
         'mogrify': 'mogrify'
     }
     if platform.system() == "Windows":
         paths = [os.getenv('ProgramFiles'), os.getenv('ProgramFiles(x86)')]
         for path in paths:
             if path is not None and os.path.isdir(path):
                 dirs = sorted(os.listdir(path), reverse=True)
                 for subdir in dirs:
                     if subdir.lower().startswith('imagemagick'):
                         convert = os.path.join(path, subdir, 'convert.exe')
                         compare = os.path.join(path, subdir, 'compare.exe')
                         mogrify = os.path.join(path, subdir, 'mogrify.exe')
                         if os.path.isfile(convert) and \
                                 os.path.isfile(compare) and \
                                 os.path.isfile(mogrify):
                             if convert.find(' ') >= 0:
                                 convert = '"{0}"'.format(convert)
                             if compare.find(' ') >= 0:
                                 compare = '"{0}"'.format(compare)
                             if mogrify.find(' ') >= 0:
                                 mogrify = '"{0}"'.format(mogrify)
                             self.image_magick['convert'] = convert
                             self.image_magick['compare'] = compare
                             self.image_magick['mogrify'] = mogrify
                             break
Exemple #2
0
 def __init__(self, options, browsers):
     from internal.browsers import Browsers
     from internal.webpagetest import WebPageTest
     from internal.traffic_shaping import TrafficShaper
     self.must_exit = False
     self.options = options
     self.browsers = Browsers(options, browsers)
     self.root_path = os.path.abspath(os.path.dirname(__file__))
     self.wpt = WebPageTest(options, os.path.join(self.root_path, "work"))
     self.shaper = TrafficShaper(options.shaper)
     self.job = None
     self.task = None
     self.xvfb = None
     atexit.register(self.cleanup)
     signal.signal(signal.SIGINT, self.signal_handler)
Exemple #3
0
 def __init__(self, options, browsers):
     from internal.browsers import Browsers
     from internal.webpagetest import WebPageTest
     from internal.traffic_shaping import TrafficShaper
     from internal.adb import Adb
     self.must_exit = False
     self.options = options
     self.capture_display = None
     self.job = None
     self.task = None
     self.xvfb = None
     self.root_path = os.path.abspath(os.path.dirname(__file__))
     self.wpt = WebPageTest(options, os.path.join(self.root_path, "work"))
     self.persistent_work_dir = self.wpt.get_persistent_dir()
     self.adb = Adb(
         self.options,
         self.persistent_work_dir) if self.options.android else None
     self.browsers = Browsers(options, browsers, self.adb)
     self.shaper = TrafficShaper(options)
     atexit.register(self.cleanup)
     signal.signal(signal.SIGTERM, self.signal_handler)
     signal.signal(signal.SIGINT, self.signal_handler)
Exemple #4
0
class WPTAgent(object):
    """Main agent workflow"""
    def __init__(self, options, browsers):
        from internal.browsers import Browsers
        from internal.webpagetest import WebPageTest
        from internal.traffic_shaping import TrafficShaper
        from internal.adb import Adb
        from internal.ios_device import iOSDevice
        self.must_exit = False
        self.options = options
        self.capture_display = None
        self.job = None
        self.task = None
        self.xvfb = None
        self.root_path = os.path.abspath(os.path.dirname(__file__))
        self.wpt = WebPageTest(options, os.path.join(self.root_path, "work"))
        self.persistent_work_dir = self.wpt.get_persistent_dir()
        self.adb = Adb(self.options, self.persistent_work_dir) if self.options.android else None
        self.ios = iOSDevice(self.options.device) if self.options.iOS else None
        self.browsers = Browsers(options, browsers, self.adb, self.ios)
        self.shaper = TrafficShaper(options)
        atexit.register(self.cleanup)
        signal.signal(signal.SIGTERM, self.signal_handler)
        signal.signal(signal.SIGINT, self.signal_handler)
        self.image_magick = {'convert': 'convert', 'compare': 'compare', 'mogrify': 'mogrify'}
        if platform.system() == "Windows":
            paths = [os.getenv('ProgramFiles'), os.getenv('ProgramFiles(x86)')]
            for path in paths:
                if path is not None and os.path.isdir(path):
                    dirs = sorted(os.listdir(path), reverse=True)
                    for subdir in dirs:
                        if subdir.lower().startswith('imagemagick'):
                            convert = os.path.join(path, subdir, 'convert.exe')
                            compare = os.path.join(path, subdir, 'compare.exe')
                            mogrify = os.path.join(path, subdir, 'mogrify.exe')
                            if os.path.isfile(convert) and \
                                    os.path.isfile(compare) and \
                                    os.path.isfile(mogrify):
                                if convert.find(' ') >= 0:
                                    convert = '"{0}"'.format(convert)
                                if compare.find(' ') >= 0:
                                    compare = '"{0}"'.format(compare)
                                if mogrify.find(' ') >= 0:
                                    mogrify = '"{0}"'.format(mogrify)
                                self.image_magick['convert'] = convert
                                self.image_magick['compare'] = compare
                                self.image_magick['mogrify'] = mogrify
                                break
                            convert = os.path.join(path, subdir, 'magick.exe')
                            compare = os.path.join(path, subdir, 'magick.exe')
                            mogrify = os.path.join(path, subdir, 'magick.exe')
                            if os.path.isfile(convert) and \
                                    os.path.isfile(compare) and \
                                    os.path.isfile(mogrify):
                                if convert.find(' ') >= 0:
                                    convert = '"{0}" convert'.format(convert)
                                if compare.find(' ') >= 0:
                                    compare = '"{0}" compare'.format(compare)
                                if mogrify.find(' ') >= 0:
                                    mogrify = '"{0}" mogrify'.format(mogrify)
                                self.image_magick['convert'] = convert
                                self.image_magick['compare'] = compare
                                self.image_magick['mogrify'] = mogrify
                                break

    def run_testing(self):
        """Main testing flow"""
        import monotonic
        start_time = monotonic.monotonic()
        browser = None
        exit_file = os.path.join(self.root_path, 'exit')
        message_server = None
        if not self.options.android and not self.options.iOS:
            from internal.message_server import MessageServer
            message_server = MessageServer()
            message_server.start()
            if not message_server.is_ok():
                logging.error("Unable to start the local message server")
                return
        while not self.must_exit:
            try:
                self.alive()
                if os.path.isfile(exit_file):
                    try:
                        os.remove(exit_file)
                    except Exception:
                        pass
                    self.must_exit = True
                    break
                if message_server is not None and self.options.exit > 0 and \
                        not message_server.is_ok():
                    logging.error("Message server not responding, exiting")
                    break
                if self.browsers.is_ready():
                    self.job = self.wpt.get_test(self.browsers.browsers)
                    if self.job is not None:
                        self.job['image_magick'] = self.image_magick
                        self.job['message_server'] = message_server
                        self.job['capture_display'] = self.capture_display
                        self.job['shaper'] = self.shaper
                        self.task = self.wpt.get_task(self.job)
                        while self.task is not None:
                            start = monotonic.monotonic()
                            try:
                                self.task['running_lighthouse'] = False
                                if self.job['type'] != 'lighthouse':
                                    self.run_single_test()
                                    self.wpt.get_bodies(self.task)
                                if self.task['run'] == 1 and not self.task['cached'] and \
                                        self.job['warmup'] <= 0 and \
                                        self.task['error'] is None and \
                                        'lighthouse' in self.job and self.job['lighthouse']:
                                    if 'page_result' not in self.task or \
                                            self.task['page_result'] is None or \
                                            self.task['page_result'] == 0 or \
                                            self.task['page_result'] == 99999:
                                        self.task['running_lighthouse'] = True
                                        self.wpt.running_another_test(self.task)
                                        self.run_single_test()
                                elapsed = monotonic.monotonic() - start
                                logging.debug('Test run time: %0.3f sec', elapsed)
                            except Exception as err:
                                msg = ''
                                if err is not None and err.__str__() is not None:
                                    msg = err.__str__()
                                self.task['error'] = 'Unhandled exception running test: '\
                                    '{0}'.format(msg)
                                logging.exception("Unhandled exception running test: %s", msg)
                                traceback.print_exc(file=sys.stdout)
                            self.wpt.upload_task_result(self.task)
                            # Set up for the next run
                            self.task = self.wpt.get_task(self.job)
                if self.job is not None:
                    self.job = None
                else:
                    self.sleep(self.options.polling)
            except Exception as err:
                msg = ''
                if err is not None and err.__str__() is not None:
                    msg = err.__str__()
                if self.task is not None:
                    self.task['error'] = 'Unhandled exception preparing test: '\
                        '{0}'.format(msg)
                logging.exception("Unhandled exception: %s", msg)
                traceback.print_exc(file=sys.stdout)
                if browser is not None:
                    browser.on_stop_capture(None)
                    browser.on_stop_recording(None)
                    browser = None
            if self.options.exit > 0:
                run_time = (monotonic.monotonic() - start_time) / 60.0
                if run_time > self.options.exit:
                    break
            # Exit if adb is having issues (will cause a reboot after several tries)
            if self.adb is not None and self.adb.needs_exit:
                break
        self.cleanup()

    def run_single_test(self):
        """Run a single test run"""
        self.alive()
        browser = self.browsers.get_browser(self.job['browser'], self.job)
        if browser is not None:
            browser.prepare(self.job, self.task)
            browser.launch(self.job, self.task)
            try:
                if self.task['running_lighthouse']:
                    self.task['lighthouse_log'] = \
                        'Lighthouse testing is not supported with this browser.'
                    try:
                        browser.run_lighthouse_test(self.task)
                    except Exception:
                        pass
                    if self.task['lighthouse_log']:
                        log_file = os.path.join(self.task['dir'], 'lighthouse.log.gz')
                        with gzip.open(log_file, 'wb', 7) as f_out:
                            f_out.write(self.task['lighthouse_log'])
                else:
                    browser.run_task(self.task)
            except Exception as err:
                msg = ''
                if err is not None and err.__str__() is not None:
                    msg = err.__str__()
                self.task['error'] = 'Unhandled exception in test run: '\
                    '{0}'.format(msg)
                logging.exception("Unhandled exception in test run: %s", msg)
                traceback.print_exc(file=sys.stdout)
            browser.stop(self.job, self.task)
            # Delete the browser profile if needed
            if self.task['cached'] or self.job['fvonly']:
                browser.clear_profile(self.task)
        else:
            err = "Invalid browser - {0}".format(self.job['browser'])
            logging.critical(err)
            self.task['error'] = err
        browser = None

    def signal_handler(self, *_):
        """Ctrl+C handler"""
        if self.must_exit:
            exit(1)
        if self.job is None:
            print "Exiting..."
        else:
            print "Will exit after test completes.  Hit Ctrl+C again to exit immediately"
        self.must_exit = True

    def cleanup(self):
        """Do any cleanup that needs to be run regardless of how we exit"""
        logging.debug('Cleaning up')
        self.shaper.remove()
        if self.xvfb is not None:
            self.xvfb.stop()
        if self.adb is not None:
            self.adb.stop()
        if self.ios is not None:
            self.ios.disconnect()

    def sleep(self, seconds):
        """Sleep wrapped in an exception handler to properly deal with Ctrl+C"""
        try:
            time.sleep(seconds)
        except IOError:
            pass

    def wait_for_idle(self, timeout=30):
        """Wait for the system to go idle for at least 2 seconds"""
        import monotonic
        import psutil
        logging.debug("Waiting for Idle...")
        cpu_count = psutil.cpu_count()
        if cpu_count > 0:
            target_pct = 50. / float(cpu_count)
            idle_start = None
            end_time = monotonic.monotonic() + timeout
            idle = False
            while not idle and monotonic.monotonic() < end_time:
                self.alive()
                check_start = monotonic.monotonic()
                pct = psutil.cpu_percent(interval=0.5)
                if pct <= target_pct:
                    if idle_start is None:
                        idle_start = check_start
                    if monotonic.monotonic() - idle_start > 2:
                        idle = True
                else:
                    idle_start = None

    def alive(self):
        """Touch a watchdog file indicating we are still alive"""
        if self.options.alive:
            with open(self.options.alive, 'a'):
                os.utime(self.options.alive, None)

    def requires(self, module, module_name=None):
        """Try importing a module and installing it if it isn't available"""
        ret = False
        if module_name is None:
            module_name = module
        try:
            __import__(module)
            ret = True
        except ImportError:
            pass
        if not ret:
            from internal.os_util import run_elevated
            logging.debug('Trying to install %s...', module_name)
            subprocess.call([sys.executable, '-m', 'pip', 'uninstall', '-y', module_name])
            run_elevated(sys.executable, '-m pip uninstall -y {0}'.format(module_name))
            subprocess.call([sys.executable, '-m', 'pip', 'install', module_name])
            run_elevated(sys.executable, '-m pip install {0}'.format(module_name))
            try:
                __import__(module)
                ret = True
            except ImportError:
                pass
        if not ret:
            print "Missing {0} module. Please run 'pip install {1}'".format(module, module_name)
        return ret

    def startup(self):
        """Validate that all of the external dependencies are installed"""
        ret = True

        # default /tmp/wptagent as an alive file on Linux
        if self.options.alive is None:
            if platform.system() == "Linux":
                self.options.alive = '/tmp/wptagent'
            else:
                self.options.alive = os.path.join(os.path.dirname(__file__), 'wptagent.alive')
        self.alive()

        ret = self.requires('dns', 'dnspython') and ret
        ret = self.requires('monotonic') and ret
        ret = self.requires('PIL', 'pillow') and ret
        ret = self.requires('psutil') and ret
        ret = self.requires('requests') and ret
        if not self.options.android and not self.options.iOS:
            ret = self.requires('tornado') and ret
        # Windows-specific imports
        if platform.system() == "Windows":
            ret = self.requires('win32api', 'pywin32') and ret

        # Optional imports
        self.requires('brotli')
        self.requires('fontTools', 'fonttools')

        # Try patching ws4py with a faster lib
        try:
            self.requires('wsaccel')
            import wsaccel
            wsaccel.patch_ws4py()
        except Exception:
            pass

        try:
            subprocess.check_output(['python', '--version'])
        except Exception:
            print "Make sure python 2.7 is available in the path."
            ret = False

        try:
            subprocess.check_output('{0} -version'.format(self.image_magick['convert']), shell=True)
        except Exception:
            print "Missing convert utility. Please install ImageMagick " \
                  "and make sure it is in the path."
            ret = False

        try:
            subprocess.check_output('{0} -version'.format(self.image_magick['mogrify']), shell=True)
        except Exception:
            print "Missing mogrify utility. Please install ImageMagick " \
                  "and make sure it is in the path."
            ret = False

        if platform.system() == "Linux":
            try:
                subprocess.check_output(['traceroute', '--version'])
            except Exception:
                logging.debug("Traceroute is missing, installing...")
                subprocess.call(['sudo', 'apt-get', '-yq', 'install', 'traceroute'])

        # If we are on Linux and there is no display, enable xvfb by default
        if platform.system() == "Linux" and not self.options.android and \
                not self.options.iOS and 'DISPLAY' not in os.environ:
            self.options.xvfb = True

        if self.options.xvfb:
            ret = self.requires('xvfbwrapper') and ret
            if ret:
                from xvfbwrapper import Xvfb
                self.xvfb = Xvfb(width=1920, height=1200, colordepth=24)
                self.xvfb.start()

        # Figure out which display to capture from
        if platform.system() == "Linux" and 'DISPLAY' in os.environ:
            logging.debug('Display: %s', os.environ['DISPLAY'])
            self.capture_display = os.environ['DISPLAY']
        elif platform.system() == "Darwin":
            proc = subprocess.Popen('ffmpeg -f avfoundation -list_devices true -i ""',
                                    stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
            _, err = proc.communicate()
            for line in err.splitlines():
                matches = re.search(r'\[(\d+)\] Capture screen', line)
                if matches:
                    self.capture_display = matches.group(1)
                    break
        elif platform.system() == "Windows":
            self.capture_display = 'desktop'

        if self.options.throttle:
            try:
                subprocess.check_output('sudo cgset -h', shell=True)
            except Exception:
                print "Missing cgroups, make sure cgroup-tools is installed."
                ret = False

        # Fix Lighthouse install permissions
        if platform.system() != "Windows":
            from internal.os_util import run_elevated
            run_elevated('chmod', '-R 777 ~/.config/configstore/')
            try:
                import getpass
                run_elevated('chown', '-R {0}:{0} ~/.config'.format(getpass.getuser()))
            except Exception:
                pass

        # Check for Node 10+
        if self.get_node_version() < 10.0:
            if platform.system() == "Linux":
                # This only works on debian-based systems
                logging.debug('Updating Node.js to 10.x')
                subprocess.call('curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -',
                                shell=True)
                subprocess.call(['sudo', 'apt-get', 'install', '-y', 'nodejs'])
            if self.get_node_version() < 10.0:
                logging.warning("Node.js 10 or newer is required for Lighthouse testing")

        # Check the iOS install
        if self.ios is not None:
            ret = self.ios.check_install()

        if not self.options.android and not self.options.iOS and not self.options.noidle:
            self.wait_for_idle(300)
        if self.adb is not None:
            if not self.adb.start():
                print "Error configuring adb. Make sure it is installed and in the path."
                ret = False
        self.shaper.remove()
        if not self.shaper.install():
            if platform.system() == "Windows":
                print "Error configuring traffic shaping, make sure secure boot is disabled."
            else:
                print "Error configuring traffic shaping, make sure it is installed."
            ret = False

        # Update the Windows root certs
        if platform.system() == "Windows":
            self.update_windows_certificates()

        return ret

    def get_node_version(self):
        """Get the installed version of Node.js"""
        version = 0
        try:
            stdout = subprocess.check_output(['node', '--version'])
            matches = re.match(r'^v(\d+\.\d+)', stdout)
            if matches:
                version = float(matches.group(1))
        except Exception:
            pass
        return version

    def update_windows_certificates(self):
        """ Update the root Windows certificates"""
        try:
            cert_file = os.path.join(self.persistent_work_dir, 'root_certs.sst')
            if not os.path.isdir(self.persistent_work_dir):
                os.makedirs(self.persistent_work_dir)
            needs_update = True
            if os.path.isfile(cert_file):
                days = (time.time() - os.path.getmtime(cert_file)) / 86400
                if days < 5:
                    needs_update = False
            if needs_update:
                logging.debug("Updating Windows root certificates...")
                if os.path.isfile(cert_file):
                    os.unlink(cert_file)
                from internal.os_util import run_elevated
                run_elevated('certutil.exe', '-generateSSTFromWU "{0}"'.format(cert_file))
                if os.path.isfile(cert_file):
                    run_elevated('certutil.exe', '-addstore -f Root "{0}"'.format(cert_file))
        except Exception:
            pass
Exemple #5
0
class WPTAgent(object):
    """Main agent workflow"""
    def __init__(self, options, browsers):
        from internal.browsers import Browsers
        from internal.webpagetest import WebPageTest
        from internal.traffic_shaping import TrafficShaper
        from internal.adb import Adb
        self.must_exit = False
        self.options = options
        self.adb = Adb(self.options) if self.options.android else None
        self.browsers = Browsers(options, browsers, self.adb)
        self.root_path = os.path.abspath(os.path.dirname(__file__))
        self.wpt = WebPageTest(options, os.path.join(self.root_path, "work"))
        self.shaper = TrafficShaper(options)
        self.job = None
        self.task = None
        self.xvfb = None
        atexit.register(self.cleanup)
        signal.signal(signal.SIGINT, self.signal_handler)

    def run_testing(self):
        """Main testing flow"""
        import monotonic
        start_time = monotonic.monotonic()
        browser = None
        while not self.must_exit:
            try:
                if self.browsers.is_ready():
                    self.job = self.wpt.get_test()
                    if self.job is not None:
                        self.task = self.wpt.get_task(self.job)
                        while self.task is not None:
                            start = monotonic.monotonic()
                            try:
                                self.task['running_lighthouse'] = False
                                if self.job['type'] != 'lighthouse':
                                    self.run_single_test()
                                if self.task['run'] == 1 and not self.task['cached'] and \
                                        'lighthouse' in self.job and self.job['lighthouse']:
                                    self.task['running_lighthouse'] = True
                                    self.run_single_test()
                                elapsed = monotonic.monotonic() - start
                                logging.debug('Test run time: %0.3f sec',
                                              elapsed)
                            except Exception as err:
                                msg = ''
                                if err is not None and err.__str__(
                                ) is not None:
                                    msg = err.__str__()
                                self.task['error'] = 'Unhandled exception running test: '\
                                    '{0}'.format(msg)
                                logging.critical(
                                    "Unhandled exception running test: %s",
                                    msg)
                                traceback.print_exc(file=sys.stdout)
                            self.wpt.upload_task_result(self.task)
                            # Set up for the next run
                            self.task = self.wpt.get_task(self.job)
                if self.job is not None:
                    self.job = None
                else:
                    self.sleep(5)
            except Exception as err:
                msg = ''
                if err is not None and err.__str__() is not None:
                    msg = err.__str__()
                if self.task is not None:
                    self.task['error'] = 'Unhandled exception preparing test: '\
                        '{0}'.format(msg)
                logging.critical("Unhandled exception: %s", msg)
                traceback.print_exc(file=sys.stdout)
                if browser is not None:
                    browser.on_stop_recording(None)
                    browser = None
            if self.options.exit > 0:
                run_time = (monotonic.monotonic() - start_time) / 60.0
                if run_time > self.options.exit:
                    break

    def run_single_test(self):
        """Run a single test run"""
        browser = self.browsers.get_browser(self.job['browser'], self.job)
        if browser is not None:
            browser.prepare(self.job, self.task)
            browser.launch(self.job, self.task)
            if self.shaper.configure(self.job):
                try:
                    if self.task['running_lighthouse']:
                        browser.run_lighthouse_test(self.task)
                    else:
                        browser.run_task(self.task)
                except Exception as err:
                    msg = ''
                    if err is not None and err.__str__() is not None:
                        msg = err.__str__()
                    self.task['error'] = 'Unhandled exception in test run: '\
                        '{0}'.format(msg)
                    logging.critical("Unhandled exception in test run: %s",
                                     msg)
                    traceback.print_exc(file=sys.stdout)
            else:
                self.task['error'] = "Error configuring traffic-shaping"
            self.shaper.reset()
            browser.stop(self.job, self.task)
        else:
            err = "Invalid browser - {0}".format(self.job['browser'])
            logging.critical(err)
            self.task['error'] = err
        # Delete the browser profile if needed
        if self.task['cached'] or self.job['fvonly']:
            browser.clear_profile(self.task)
        browser = None

    def signal_handler(self, *_):
        """Ctrl+C handler"""
        if self.must_exit:
            exit(1)
        if self.job is None:
            print "Exiting..."
        else:
            print "Will exit after test completes.  Hit Ctrl+C again to exit immediately"
        self.must_exit = True

    def cleanup(self):
        """Do any cleanup that needs to be run regardless of how we exit."""
        logging.debug('Cleaning up')
        self.shaper.remove()
        if self.xvfb is not None:
            self.xvfb.stop()

    def sleep(self, seconds):
        """Sleep wrapped in an exception handler to properly deal with Ctrl+C"""
        try:
            time.sleep(seconds)
        except IOError:
            pass

    def wait_for_idle(self, timeout=30):
        """Wait for the system to go idle"""
        import monotonic
        import psutil
        logging.debug("Waiting for Idle...")
        cpu_count = psutil.cpu_count()
        if cpu_count > 0:
            target_pct = 20. / float(cpu_count)
            idle_start = None
            end_time = monotonic.monotonic() + timeout
            idle = False
            while not idle and monotonic.monotonic() < end_time:
                check_start = monotonic.monotonic()
                pct = psutil.cpu_percent(interval=1)
                if pct <= target_pct:
                    if idle_start is None:
                        idle_start = check_start
                    if monotonic.monotonic() - idle_start > 2:
                        idle = True
                else:
                    idle_start = None

    def startup(self):
        """Validate that all of the external dependencies are installed"""
        ret = True

        try:
            import dns.resolver as _
        except ImportError:
            print "Missing dns module. Please run 'pip install dnspython'"
            ret = False

        try:
            import monotonic as _
        except ImportError:
            print "Missing monotonic module. Please run 'pip install monotonic'"
            ret = False

        try:
            from PIL import Image as _
        except ImportError:
            print "Missing PIL module. Please run 'pip install pillow'"
            ret = False

        try:
            import psutil as _
        except ImportError:
            print "Missing psutil module. Please run 'pip install psutil'"
            ret = False

        try:
            import requests as _
        except ImportError:
            print "Missing requests module. Please run 'pip install requests'"
            ret = False

        try:
            import ujson as _
        except ImportError:
            print "Missing ujson parser. Please run 'pip install ujson'"
            ret = False

        try:
            subprocess.check_output(['python', '--version'])
        except Exception:
            print "Make sure python 2.7 is available in the path."
            ret = False

        try:
            subprocess.check_output('convert -version', shell=True)
        except Exception:
            print "Missing convert utility. Please install ImageMagick " \
                  "and make sure it is in the path."
            ret = False

        try:
            subprocess.check_output('mogrify -version', shell=True)
        except Exception:
            print "Missing mogrify utility. Please install ImageMagick " \
                  "and make sure it is in the path."
            ret = False

        if self.options.xvfb:
            try:
                from xvfbwrapper import Xvfb
                self.xvfb = Xvfb(width=1920, height=1200, colordepth=24)
                self.xvfb.start()
            except ImportError:
                print "Missing xvfbwrapper module. Please run 'pip install xvfbwrapper'"
                ret = False

        # Windows-specific imports
        if platform.system() == "Windows":
            try:
                import win32api as _
                import win32process as _
            except ImportError:
                print "Missing pywin32 module. Please run 'python -m pip install pypiwin32'"
                ret = False

        if not self.options.android:
            self.wait_for_idle(300)
        self.shaper.remove()
        if not self.shaper.install():
            print "Error configuring traffic shaping, make sure it is installed."
            ret = False

        if self.adb is not None:
            if not self.adb.start():
                print "Error configuring adb. Make sure it is installed and in the path."
                ret = False
        return ret
Exemple #6
0
class WPTAgent(object):
    """Main agent workflow"""
    def __init__(self, options, browsers):
        from internal.browsers import Browsers
        from internal.webpagetest import WebPageTest
        from internal.traffic_shaping import TrafficShaper
        from internal.adb import Adb
        from internal.ios_device import iOSDevice
        self.must_exit = False
        self.options = options
        self.capture_display = None
        self.job = None
        self.task = None
        self.xvfb = None
        self.root_path = os.path.abspath(os.path.dirname(__file__))
        self.wpt = WebPageTest(options, os.path.join(self.root_path, "work"))
        self.persistent_work_dir = self.wpt.get_persistent_dir()
        self.adb = Adb(
            self.options,
            self.persistent_work_dir) if self.options.android else None
        self.ios = iOSDevice(self.options.device) if self.options.iOS else None
        self.browsers = Browsers(options, browsers, self.adb, self.ios)
        self.shaper = TrafficShaper(options)
        atexit.register(self.cleanup)
        signal.signal(signal.SIGTERM, self.signal_handler)
        signal.signal(signal.SIGINT, self.signal_handler)

    def run_testing(self):
        """Main testing flow"""
        import monotonic
        start_time = monotonic.monotonic()
        browser = None
        exit_file = os.path.join(self.root_path, 'exit')
        message_server = None
        if not self.options.android and not self.options.iOS:
            message_server = MessageServer()
            message_server.start()
        while not self.must_exit:
            try:
                if os.path.isfile(exit_file):
                    try:
                        os.remove(exit_file)
                    except Exception:
                        pass
                    self.must_exit = True
                    break
                if message_server is not None and self.options.exit > 0 and \
                        not message_server.is_ok():
                    logging.error("Message server not responding, exiting")
                    break
                if self.browsers.is_ready():
                    self.job = self.wpt.get_test()
                    if self.job is not None:
                        self.job['message_server'] = message_server
                        self.job['capture_display'] = self.capture_display
                        self.task = self.wpt.get_task(self.job)
                        while self.task is not None:
                            start = monotonic.monotonic()
                            try:
                                self.task['running_lighthouse'] = False
                                if self.job['type'] != 'lighthouse':
                                    self.run_single_test()
                                if self.task['run'] == 1 and not self.task['cached'] and \
                                        self.task['error'] is None and \
                                        'lighthouse' in self.job and self.job['lighthouse']:
                                    if 'page_result' not in self.task or \
                                            self.task['page_result'] is None or \
                                            self.task['page_result'] == 0 or \
                                            self.task['page_result'] == 99999:
                                        self.task['running_lighthouse'] = True
                                        self.wpt.running_another_test(
                                            self.task)
                                        self.run_single_test()
                                elapsed = monotonic.monotonic() - start
                                logging.debug('Test run time: %0.3f sec',
                                              elapsed)
                            except Exception as err:
                                msg = ''
                                if err is not None and err.__str__(
                                ) is not None:
                                    msg = err.__str__()
                                self.task['error'] = 'Unhandled exception running test: '\
                                    '{0}'.format(msg)
                                logging.exception(
                                    "Unhandled exception running test: %s",
                                    msg)
                                traceback.print_exc(file=sys.stdout)
                            self.wpt.upload_task_result(self.task)
                            # Set up for the next run
                            self.task = self.wpt.get_task(self.job)
                if self.job is not None:
                    self.job = None
                else:
                    self.sleep(self.options.polling)
            except Exception as err:
                msg = ''
                if err is not None and err.__str__() is not None:
                    msg = err.__str__()
                if self.task is not None:
                    self.task['error'] = 'Unhandled exception preparing test: '\
                        '{0}'.format(msg)
                logging.exception("Unhandled exception: %s", msg)
                traceback.print_exc(file=sys.stdout)
                if browser is not None:
                    browser.on_stop_recording(None)
                    browser = None
            if self.options.exit > 0:
                run_time = (monotonic.monotonic() - start_time) / 60.0
                if run_time > self.options.exit:
                    break

    def run_single_test(self):
        """Run a single test run"""
        self.alive()
        browser = self.browsers.get_browser(self.job['browser'], self.job)
        if browser is not None:
            browser.prepare(self.job, self.task)
            browser.launch(self.job, self.task)
            if self.shaper.configure(self.job):
                try:
                    if self.task['running_lighthouse']:
                        browser.run_lighthouse_test(self.task)
                    else:
                        browser.run_task(self.task)
                except Exception as err:
                    msg = ''
                    if err is not None and err.__str__() is not None:
                        msg = err.__str__()
                    self.task['error'] = 'Unhandled exception in test run: '\
                        '{0}'.format(msg)
                    logging.exception("Unhandled exception in test run: %s",
                                      msg)
                    traceback.print_exc(file=sys.stdout)
            else:
                self.task['error'] = "Error configuring traffic-shaping"
            self.shaper.reset()
            browser.stop(self.job, self.task)
            # Delete the browser profile if needed
            if self.task['cached'] or self.job['fvonly']:
                browser.clear_profile(self.task)
        else:
            err = "Invalid browser - {0}".format(self.job['browser'])
            logging.critical(err)
            self.task['error'] = err
        browser = None

    def signal_handler(self, *_):
        """Ctrl+C handler"""
        if self.must_exit:
            exit(1)
        if self.job is None:
            print "Exiting..."
        else:
            print "Will exit after test completes.  Hit Ctrl+C again to exit immediately"
        self.must_exit = True

    def cleanup(self):
        """Do any cleanup that needs to be run regardless of how we exit."""
        logging.debug('Cleaning up')
        self.shaper.remove()
        if self.xvfb is not None:
            self.xvfb.stop()
        if self.adb is not None:
            self.adb.stop()
        if self.ios is not None:
            self.ios.disconnect()

    def sleep(self, seconds):
        """Sleep wrapped in an exception handler to properly deal with Ctrl+C"""
        try:
            time.sleep(seconds)
        except IOError:
            pass

    def wait_for_idle(self, timeout=30):
        """Wait for the system to go idle for at least 2 seconds"""
        import monotonic
        import psutil
        logging.debug("Waiting for Idle...")
        cpu_count = psutil.cpu_count()
        if cpu_count > 0:
            target_pct = 50. / float(cpu_count)
            idle_start = None
            end_time = monotonic.monotonic() + timeout
            idle = False
            while not idle and monotonic.monotonic() < end_time:
                self.alive()
                check_start = monotonic.monotonic()
                pct = psutil.cpu_percent(interval=0.5)
                if pct <= target_pct:
                    if idle_start is None:
                        idle_start = check_start
                    if monotonic.monotonic() - idle_start > 2:
                        idle = True
                else:
                    idle_start = None

    def alive(self):
        """Touch a watchdog file indicating we are still alive"""
        if self.options.alive:
            with open(self.options.alive, 'a'):
                os.utime(self.options.alive, None)

    def startup(self):
        """Validate that all of the external dependencies are installed"""
        ret = True
        self.alive()

        try:
            import dns.resolver as _
        except ImportError:
            print "Missing dns module. Please run 'pip install dnspython'"
            ret = False

        try:
            import monotonic as _
        except ImportError:
            print "Missing monotonic module. Please run 'pip install monotonic'"
            ret = False

        try:
            from PIL import Image as _
        except ImportError:
            print "Missing PIL module. Please run 'pip install pillow'"
            ret = False

        try:
            import psutil as _
        except ImportError:
            print "Missing psutil module. Please run 'pip install psutil'"
            ret = False

        try:
            import requests as _
        except ImportError:
            print "Missing requests module. Please run 'pip install requests'"
            ret = False

        try:
            subprocess.check_output(['python', '--version'])
        except Exception:
            print "Make sure python 2.7 is available in the path."
            ret = False

        try:
            subprocess.check_output('convert -version', shell=True)
        except Exception:
            print "Missing convert utility. Please install ImageMagick " \
                  "and make sure it is in the path."
            ret = False

        try:
            subprocess.check_output('mogrify -version', shell=True)
        except Exception:
            print "Missing mogrify utility. Please install ImageMagick " \
                  "and make sure it is in the path."
            ret = False

        if platform.system() == "Linux":
            try:
                subprocess.check_output(['traceroute', '--version'])
            except Exception:
                logging.debug("Traceroute is missing, installing...")
                subprocess.call(
                    ['sudo', 'apt-get', '-yq', 'install', 'traceroute'])

        # if we are on Linux and there is no display, enable xvfb by default
        if platform.system() == "Linux" and not self.options.android and \
                not self.options.iOS and 'DISPLAY' not in os.environ:
            self.options.xvfb = True

        if self.options.xvfb:
            try:
                from xvfbwrapper import Xvfb
                self.xvfb = Xvfb(width=1920, height=1200, colordepth=24)
                self.xvfb.start()
            except ImportError:
                print "Missing xvfbwrapper module. Please run 'pip install xvfbwrapper'"
                ret = False

        # Figure out which display to capture from
        if platform.system() == "Linux" and 'DISPLAY' in os.environ:
            logging.debug('Display: %s', os.environ['DISPLAY'])
            self.capture_display = os.environ['DISPLAY']
        elif platform.system() == "Darwin":
            proc = subprocess.Popen(
                'ffmpeg -f avfoundation -list_devices true -i ""',
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                shell=True)
            _, err = proc.communicate()
            for line in err.splitlines():
                matches = re.search(r'\[(\d+)\] Capture screen', line)
                if matches:
                    self.capture_display = matches.group(1)
                    break
        elif platform.system() == "Windows":
            self.capture_display = 'desktop'

        if self.options.throttle:
            try:
                subprocess.check_output('sudo cgset -h', shell=True)
            except Exception:
                print "Missing cgroups, make sure cgroup-tools is installed."
                ret = False

        # Windows-specific imports
        if platform.system() == "Windows":
            try:
                import win32api as _
                import win32process as _
            except ImportError:
                print "Missing pywin32 module. Please run 'python -m pip install pypiwin32'"
                ret = False

        # Check the iOS install
        if self.ios is not None:
            ret = self.ios.check_install()

        if not self.options.android and not self.options.iOS:
            self.wait_for_idle(300)
        self.shaper.remove()
        if not self.shaper.install():
            if platform.system() == "Windows":
                print "Error configuring traffic shaping, make sure secure boot is disabled."
            else:
                print "Error configuring traffic shaping, make sure it is installed."
            ret = False

        if self.adb is not None:
            if not self.adb.start():
                print "Error configuring adb. Make sure it is installed and in the path."
                ret = False
        return ret