• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2007 Google Inc. Released under the GPL v2
2#pylint: disable-msg=C0111
3
4import glob
5import logging
6import os
7import re
8import sys
9import tempfile
10import time
11import traceback
12
13import common
14from autotest_lib.client.bin.result_tools import runner as result_tools_runner
15from autotest_lib.client.common_lib import autotemp
16from autotest_lib.client.common_lib import base_job
17from autotest_lib.client.common_lib import error
18from autotest_lib.client.common_lib import global_config
19from autotest_lib.client.common_lib import packages
20from autotest_lib.client.common_lib.cros import dev_server
21from autotest_lib.client.common_lib import utils as client_utils
22from autotest_lib.server import autoserv_parser
23from autotest_lib.server import installable_object
24from autotest_lib.server import utils
25from autotest_lib.server import utils as server_utils
26from autotest_lib.server.cros.dynamic_suite import tools
27from autotest_lib.server.cros.dynamic_suite.constants import JOB_REPO_URL
28
29
30try:
31    from chromite.lib import metrics
32except ImportError:
33    metrics = client_utils.metrics_mock
34
35
36AUTOTEST_SVN = 'svn://test.kernel.org/autotest/trunk/client'
37AUTOTEST_HTTP = 'http://test.kernel.org/svn/autotest/trunk/client'
38
39_PARSER = autoserv_parser.autoserv_parser
40
41_CONFIG = global_config.global_config
42AUTOSERV_PREBUILD = _CONFIG.get_config_value(
43        'AUTOSERV', 'enable_server_prebuild', type=bool, default=False)
44
45# Match on a line like this:
46# FAIL test_name  test_name timestamp=1 localtime=Nov 15 12:43:10 <fail_msg>
47_FAIL_STATUS_RE = re.compile(
48    r'\s*FAIL.*localtime=.*\s*.*\s*[0-9]+:[0-9]+:[0-9]+\s*(?P<fail_msg>.*)')
49
50
51class AutodirNotFoundError(Exception):
52    """No Autotest installation could be found."""
53
54
55class AutotestFailure(Exception):
56    """Gereric exception class for failures during a test run."""
57
58
59class AutotestAbort(AutotestFailure):
60    """
61    AutotestAborts are thrown when the DUT seems fine,
62    and the test doesn't give us an explicit reason for
63    failure; In this case we have no choice but to abort.
64    """
65
66
67class AutotestDeviceError(AutotestFailure):
68    """
69    Exceptions that inherit from AutotestDeviceError
70    are thrown when we can determine the current
71    state of the DUT and conclude that it probably
72    lead to the test failing; these exceptions lead
73    to failures instead of aborts.
74    """
75
76
77class AutotestDeviceNotPingable(AutotestDeviceError):
78    """Error for when a DUT becomes unpingable."""
79
80
81class AutotestDeviceNotSSHable(AutotestDeviceError):
82    """Error for when a DUT is pingable but not SSHable."""
83
84
85class AutotestDeviceRebooted(AutotestDeviceError):
86    """Error for when a DUT rebooted unexpectedly."""
87
88
89class Autotest(installable_object.InstallableObject):
90    """
91    This class represents the Autotest program.
92
93    Autotest is used to run tests automatically and collect the results.
94    It also supports profilers.
95
96    Implementation details:
97    This is a leaf class in an abstract class hierarchy, it must
98    implement the unimplemented methods in parent classes.
99    """
100
101    def __init__(self, host=None):
102        self.host = host
103        self.got = False
104        self.installed = False
105        self.serverdir = utils.get_server_dir()
106        super(Autotest, self).__init__()
107
108
109    install_in_tmpdir = False
110    @classmethod
111    def set_install_in_tmpdir(cls, flag):
112        """ Sets a flag that controls whether or not Autotest should by
113        default be installed in a "standard" directory (e.g.
114        /home/autotest, /usr/local/autotest) or a temporary directory. """
115        cls.install_in_tmpdir = flag
116
117
118    @classmethod
119    def get_client_autodir_paths(cls, host):
120        return global_config.global_config.get_config_value(
121                'AUTOSERV', 'client_autodir_paths', type=list)
122
123
124    @classmethod
125    def get_installed_autodir(cls, host):
126        """
127        Find where the Autotest client is installed on the host.
128        @returns an absolute path to an installed Autotest client root.
129        @raises AutodirNotFoundError if no Autotest installation can be found.
130        """
131        autodir = host.get_autodir()
132        if autodir:
133            logging.debug('Using existing host autodir: %s', autodir)
134            return autodir
135
136        for path in Autotest.get_client_autodir_paths(host):
137            try:
138                autotest_binary = os.path.join(path, 'bin', 'autotest')
139                host.run('test -x %s' % utils.sh_escape(autotest_binary))
140                host.run('test -w %s' % utils.sh_escape(path))
141                logging.debug('Found existing autodir at %s', path)
142                return path
143            except error.GenericHostRunError:
144                logging.debug('%s does not exist on %s', autotest_binary,
145                              host.hostname)
146        raise AutodirNotFoundError
147
148
149    @classmethod
150    def get_install_dir(cls, host):
151        """
152        Determines the location where autotest should be installed on
153        host. If self.install_in_tmpdir is set, it will return a unique
154        temporary directory that autotest can be installed in. Otherwise, looks
155        for an existing installation to use; if none is found, looks for a
156        usable directory in the global config client_autodir_paths.
157        """
158        try:
159            install_dir = cls.get_installed_autodir(host)
160        except AutodirNotFoundError:
161            install_dir = cls._find_installable_dir(host)
162
163        if cls.install_in_tmpdir:
164            return host.get_tmp_dir(parent=install_dir)
165        return install_dir
166
167
168    @classmethod
169    def _find_installable_dir(cls, host):
170        client_autodir_paths = cls.get_client_autodir_paths(host)
171        for path in client_autodir_paths:
172            try:
173                host.run('mkdir -p %s' % utils.sh_escape(path))
174                host.run('test -w %s' % utils.sh_escape(path))
175                return path
176            except error.AutoservRunError:
177                logging.debug('Failed to create %s', path)
178        metrics.Counter(
179            'chromeos/autotest/errors/no_autotest_install_path').increment(
180                fields={'dut_host_name': host.hostname})
181        raise error.AutoservInstallError(
182                'Unable to find a place to install Autotest; tried %s' %
183                ', '.join(client_autodir_paths))
184
185
186    def get_fetch_location(self):
187        """Generate list of locations where autotest can look for packages.
188
189        Old n' busted: Autotest packages are always stored at a URL that can
190        be derived from the one passed via the voodoo magic --image argument.
191        New hotness: Hosts are tagged with an attribute containing the URL
192        from which to source packages when running a test on that host.
193
194        @returns the list of candidate locations to check for packages.
195        """
196        c = global_config.global_config
197        repos = c.get_config_value("PACKAGES", 'fetch_location', type=list,
198                                   default=[])
199        repos.reverse()
200
201        if _PARSER.options.image:
202            image_opt = _PARSER.options.image
203            if image_opt.startswith('http://'):
204                # A devserver HTTP url was specified, set that as the repo_url.
205                repos.append(image_opt.replace(
206                    'update', 'static').rstrip('/') + '/autotest')
207            else:
208                # An image_name like stumpy-release/R27-3437.0.0 was specified,
209                # set this as the repo_url for the host. If an AFE is not being
210                # run, this will ensure that the installed build uses the
211                # associated artifacts for the test specified when running
212                # autoserv with --image. However, any subsequent tests run on
213                # the host will no longer have the context of the image option
214                # and will revert back to utilizing test code/artifacts that are
215                # currently present in the users source checkout.
216                # devserver selected must be in the same subnet of self.host, if
217                # the host is in restricted subnet. Otherwise, host may not be
218                # able to reach the devserver and download packages from the
219                # repo_url.
220                hostname = self.host.hostname if self.host else None
221                devserver_url = dev_server.ImageServer.resolve(
222                        image_opt, hostname).url()
223                repo_url = tools.get_package_url(devserver_url, image_opt)
224                repos.append(repo_url)
225        elif not server_utils.is_inside_chroot():
226            # Only try to get fetch location from host attribute if the test
227            # is not running inside chroot.
228            # No --image option was specified, look for the repo url via
229            # the host attribute. If we are not running with a full AFE
230            # autoserv will fall back to serving packages itself from whatever
231            # source version it is sync'd to rather than using the proper
232            # artifacts for the build on the host.
233            found_repo = self._get_fetch_location_from_host_attribute()
234            if found_repo is not None:
235                # Add our new repo to the end, the package manager will
236                # later reverse the list of repositories resulting in ours
237                # being first
238                repos.append(found_repo)
239
240        return repos
241
242
243    def _get_fetch_location_from_host_attribute(self):
244        """Get repo to use for packages from host attribute, if possible.
245
246        Hosts are tagged with an attribute containing the URL
247        from which to source packages when running a test on that host.
248        If self.host is set, attempt to look this attribute in the host info.
249
250        @returns value of the 'job_repo_url' host attribute, if present.
251        """
252        if not self.host:
253            return None
254
255        try:
256            info = self.host.host_info_store.get()
257        except Exception as e:
258            # TODO(pprabhu): We really want to catch host_info.StoreError here,
259            # but we can't import host_info from this module.
260            #   - autotest_lib.hosts.host_info pulls in (naturally)
261            #   autotest_lib.hosts.__init__
262            #   - This pulls in all the host classes ever defined
263            #   - That includes abstract_ssh, which depends on autotest
264            logging.warning('Failed to obtain host info: %r', e)
265            logging.warning('Skipping autotest fetch location based on %s',
266                            JOB_REPO_URL)
267            return None
268
269        job_repo_url = info.attributes.get(JOB_REPO_URL, '')
270        if not job_repo_url:
271            logging.warning("No %s for %s", JOB_REPO_URL, self.host)
272            return None
273
274        logging.info('Got job repo url from host attributes: %s',
275                        job_repo_url)
276        return job_repo_url
277
278
279    def install(self, host=None, autodir=None, use_packaging=True):
280        """Install autotest.  If |host| is not None, stores it in |self.host|.
281
282        @param host A Host instance on which autotest will be installed
283        @param autodir Location on the remote host to install to
284        @param use_packaging Enable install modes that use the packaging system.
285
286        """
287        if host:
288            self.host = host
289        self._install(host=host, autodir=autodir, use_packaging=use_packaging)
290
291
292    def install_full_client(self, host=None, autodir=None):
293        self._install(host=host, autodir=autodir, use_autoserv=False,
294                      use_packaging=False)
295
296
297    def install_no_autoserv(self, host=None, autodir=None):
298        self._install(host=host, autodir=autodir, use_autoserv=False)
299
300
301    def _install_using_packaging(self, host, autodir):
302        repos = self.get_fetch_location()
303        if not repos:
304            raise error.PackageInstallError("No repos to install an "
305                                            "autotest client from")
306        pkgmgr = packages.PackageManager(autodir, hostname=host.hostname,
307                                         repo_urls=repos,
308                                         do_locking=False,
309                                         run_function=host.run,
310                                         run_function_dargs=dict(timeout=600))
311        # The packages dir is used to store all the packages that
312        # are fetched on that client. (for the tests,deps etc.
313        # too apart from the client)
314        pkg_dir = os.path.join(autodir, 'packages')
315        # clean up the autodir except for the packages and result_tools
316        # directory.
317        host.run('cd %s && ls | grep -v "^packages$" | grep -v "^result_tools$"'
318                 ' | xargs rm -rf && rm -rf .[!.]*' % autodir)
319        pkgmgr.install_pkg('autotest', 'client', pkg_dir, autodir,
320                           preserve_install_dir=True)
321        self.installed = True
322
323
324    def _install_using_send_file(self, host, autodir):
325        dirs_to_exclude = set(["tests", "site_tests", "deps", "profilers",
326                               "packages"])
327        light_files = [os.path.join(self.source_material, f)
328                       for f in os.listdir(self.source_material)
329                       if f not in dirs_to_exclude]
330        host.send_file(light_files, autodir, delete_dest=True)
331
332        # create empty dirs for all the stuff we excluded
333        commands = []
334        for path in dirs_to_exclude:
335            abs_path = os.path.join(autodir, path)
336            abs_path = utils.sh_escape(abs_path)
337            commands.append("mkdir -p '%s'" % abs_path)
338            commands.append("touch '%s'/__init__.py" % abs_path)
339        host.run(';'.join(commands))
340
341
342    def _install(self, host=None, autodir=None, use_autoserv=True,
343                 use_packaging=True):
344        """
345        Install autotest.  If get() was not called previously, an
346        attempt will be made to install from the autotest svn
347        repository.
348
349        @param host A Host instance on which autotest will be installed
350        @param autodir Location on the remote host to install to
351        @param use_autoserv Enable install modes that depend on the client
352            running with the autoserv harness
353        @param use_packaging Enable install modes that use the packaging system
354
355        @exception AutoservError if a tarball was not specified and
356            the target host does not have svn installed in its path
357        """
358        if not host:
359            host = self.host
360        if not self.got:
361            self.get()
362        host.wait_up(timeout=30)
363        host.setup()
364        logging.info("Installing autotest on %s", host.hostname)
365
366        # set up the autotest directory on the remote machine
367        if not autodir:
368            autodir = self.get_install_dir(host)
369        logging.info('Using installation dir %s', autodir)
370        host.set_autodir(autodir)
371        host.run('mkdir -p %s' % utils.sh_escape(autodir))
372
373        # make sure there are no files in $AUTODIR/results
374        results_path = os.path.join(autodir, 'results')
375        host.run('rm -rf %s/*' % utils.sh_escape(results_path),
376                 ignore_status=True)
377
378        # Fetch the autotest client from the nearest repository
379        if use_packaging:
380            try:
381                self._install_using_packaging(host, autodir)
382                logging.info("Installation of autotest completed using the "
383                             "packaging system.")
384                return
385            except (error.PackageInstallError, error.AutoservRunError,
386                    global_config.ConfigError), e:
387                logging.info("Could not install autotest using the packaging "
388                             "system: %s. Trying other methods", e)
389        else:
390            # Delete the package checksum file to force dut updating local
391            # packages.
392            command = ('rm -f "%s"' %
393                       (os.path.join(autodir, packages.CHECKSUM_FILE)))
394            host.run(command)
395
396        # try to install from file or directory
397        if self.source_material:
398            c = global_config.global_config
399            supports_autoserv_packaging = c.get_config_value(
400                "PACKAGES", "serve_packages_from_autoserv", type=bool)
401            # Copy autotest recursively
402            if supports_autoserv_packaging and use_autoserv:
403                self._install_using_send_file(host, autodir)
404            else:
405                host.send_file(self.source_material, autodir, delete_dest=True)
406            logging.info("Installation of autotest completed from %s",
407                         self.source_material)
408            self.installed = True
409        else:
410            # if that fails try to install using svn
411            if utils.run('which svn').exit_status:
412                raise error.AutoservError(
413                        'svn not found on target machine: %s' %
414                        host.hostname)
415            try:
416                host.run('svn checkout %s %s' % (AUTOTEST_SVN, autodir))
417            except error.AutoservRunError, e:
418                host.run('svn checkout %s %s' % (AUTOTEST_HTTP, autodir))
419            logging.info("Installation of autotest completed using SVN.")
420            self.installed = True
421
422        # TODO(milleral): http://crbug.com/258161
423        # Send over the most recent global_config.ini after installation if one
424        # is available.
425        # This code is a bit duplicated from
426        # _Run._create_client_config_file, but oh well.
427        if self.installed and self.source_material:
428            self._send_shadow_config()
429
430    def _send_shadow_config(self):
431        logging.info('Installing updated global_config.ini.')
432        destination = os.path.join(self.host.get_autodir(),
433                                   'global_config.ini')
434        with tempfile.NamedTemporaryFile() as client_config:
435            config = global_config.global_config
436            client_section = config.get_section_values('CLIENT')
437            client_section.write(client_config)
438            client_config.flush()
439            self.host.send_file(client_config.name, destination)
440
441
442    def uninstall(self, host=None):
443        """
444        Uninstall (i.e. delete) autotest. Removes the autotest client install
445        from the specified host.
446
447        @params host a Host instance from which the client will be removed
448        """
449        if not self.installed:
450            return
451        if not host:
452            host = self.host
453        autodir = host.get_autodir()
454        if not autodir:
455            return
456
457        # perform the actual uninstall
458        host.run("rm -rf %s" % utils.sh_escape(autodir), ignore_status=True)
459        host.set_autodir(None)
460        self.installed = False
461
462
463    def get(self, location=None):
464        if not location:
465            location = os.path.join(self.serverdir, '../client')
466            location = os.path.abspath(location)
467        installable_object.InstallableObject.get(self, location)
468        self.got = True
469
470
471    def run(self, control_file, results_dir='.', host=None, timeout=None,
472            tag=None, parallel_flag=False, background=False,
473            client_disconnect_timeout=None, use_packaging=True):
474        """
475        Run an autotest job on the remote machine.
476
477        @param control_file: An open file-like-obj of the control file.
478        @param results_dir: A str path where the results should be stored
479                on the local filesystem.
480        @param host: A Host instance on which the control file should
481                be run.
482        @param timeout: Maximum number of seconds to wait for the run or None.
483        @param tag: Tag name for the client side instance of autotest.
484        @param parallel_flag: Flag set when multiple jobs are run at the
485                same time.
486        @param background: Indicates that the client should be launched as
487                a background job; the code calling run will be responsible
488                for monitoring the client and collecting the results.
489        @param client_disconnect_timeout: Seconds to wait for the remote host
490                to come back after a reboot. Defaults to the host setting for
491                DEFAULT_REBOOT_TIMEOUT.
492
493        @raises AutotestRunError: If there is a problem executing
494                the control file.
495        """
496        host = self._get_host_and_setup(host, use_packaging=use_packaging)
497        logging.debug('Autotest job starts on remote host: %s',
498                      host.hostname)
499        results_dir = os.path.abspath(results_dir)
500
501        if client_disconnect_timeout is None:
502            client_disconnect_timeout = host.DEFAULT_REBOOT_TIMEOUT
503
504        if tag:
505            results_dir = os.path.join(results_dir, tag)
506
507        atrun = _Run(host, results_dir, tag, parallel_flag, background)
508        self._do_run(control_file, results_dir, host, atrun, timeout,
509                     client_disconnect_timeout, use_packaging=use_packaging)
510
511
512    def _get_host_and_setup(self, host, use_packaging=True):
513        if not host:
514            host = self.host
515        if not self.installed:
516            self.install(host, use_packaging=use_packaging)
517
518        host.wait_up(timeout=30)
519        return host
520
521
522    def _do_run(self, control_file, results_dir, host, atrun, timeout,
523                client_disconnect_timeout, use_packaging=True):
524        try:
525            atrun.verify_machine()
526        except:
527            logging.error("Verify failed on %s. Reinstalling autotest",
528                          host.hostname)
529            self.install(host)
530            atrun.verify_machine()
531        debug = os.path.join(results_dir, 'debug')
532        try:
533            os.makedirs(debug)
534        except Exception:
535            pass
536
537        delete_file_list = [atrun.remote_control_file,
538                            atrun.remote_control_file + '.state',
539                            atrun.manual_control_file,
540                            atrun.manual_control_file + '.state']
541        cmd = ';'.join('rm -f ' + control for control in delete_file_list)
542        host.run(cmd, ignore_status=True)
543
544        tmppath = utils.get(control_file, local_copy=True)
545
546        # build up the initialization prologue for the control file
547        prologue_lines = []
548
549        # Add the additional user arguments
550        prologue_lines.append("args = %r\n" % self.job.args)
551
552        # If the packaging system is being used, add the repository list.
553        repos = None
554        try:
555            if use_packaging:
556                repos = self.get_fetch_location()
557                prologue_lines.append('job.add_repository(%s)\n' % repos)
558            else:
559                logging.debug('use_packaging is set to False, do not add any '
560                              'repository.')
561        except global_config.ConfigError, e:
562            # If repos is defined packaging is enabled so log the error
563            if repos:
564                logging.error(e)
565
566        # on full-size installs, turn on any profilers the server is using
567        if not atrun.background:
568            running_profilers = host.job.profilers.add_log.iteritems()
569            for profiler, (args, dargs) in running_profilers:
570                call_args = [repr(profiler)]
571                call_args += [repr(arg) for arg in args]
572                call_args += ["%s=%r" % item for item in dargs.iteritems()]
573                prologue_lines.append("job.profilers.add(%s)\n"
574                                      % ", ".join(call_args))
575        cfile = "".join(prologue_lines)
576
577        cfile += open(tmppath).read()
578        open(tmppath, "w").write(cfile)
579
580        # Create and copy state file to remote_control_file + '.state'
581        state_file = host.job.preprocess_client_state()
582        host.send_file(state_file, atrun.remote_control_file + '.init.state')
583        os.remove(state_file)
584
585        # Copy control_file to remote_control_file on the host
586        host.send_file(tmppath, atrun.remote_control_file)
587        if os.path.abspath(tmppath) != os.path.abspath(control_file):
588            os.remove(tmppath)
589
590        atrun.execute_control(
591                timeout=timeout,
592                client_disconnect_timeout=client_disconnect_timeout)
593
594
595    @staticmethod
596    def extract_test_failure_msg(failure_status_line):
597        """Extract the test failure message from the status line.
598
599        @param failure_status_line:  String of test failure status line, it will
600            look like:
601          FAIL <test name>  <test name> timestamp=<ts> localtime=<lt> <reason>
602
603        @returns String of the reason, return empty string if we can't regex out
604            reason.
605        """
606        fail_msg = ''
607        match = _FAIL_STATUS_RE.match(failure_status_line)
608        if match:
609            fail_msg = match.group('fail_msg')
610        return fail_msg
611
612
613    @classmethod
614    def _check_client_test_result(cls, host, test_name):
615        """
616        Check result of client test.
617        Autotest will store results in the file name status.
618        We check that second to last line in that file begins with 'END GOOD'
619
620        @raises TestFail: If client test does not pass.
621        """
622        client_result_dir = '%s/results/default' % host.autodir
623        command = 'tail -2 %s/status | head -1' % client_result_dir
624        status = host.run(command).stdout.strip()
625        logging.info(status)
626        if status[:8] != 'END GOOD':
627            test_fail_status_line_cmd = (
628                    'grep "^\s*FAIL\s*%s" %s/status | tail -n 1' %
629                    (test_name, client_result_dir))
630            test_fail_msg = cls.extract_test_failure_msg(
631                    host.run(test_fail_status_line_cmd).stdout.strip())
632            test_fail_msg_reason = ('' if not test_fail_msg
633                                    else ' (reason: %s)' % test_fail_msg)
634            test_fail_status = '%s client test did not pass%s.' % (
635                    test_name, test_fail_msg_reason)
636            raise error.TestFail(test_fail_status)
637
638
639    def run_timed_test(self, test_name, results_dir='.', host=None,
640                       timeout=None, parallel_flag=False, background=False,
641                       client_disconnect_timeout=None, *args, **dargs):
642        """
643        Assemble a tiny little control file to just run one test,
644        and run it as an autotest client-side test
645        """
646        if not host:
647            host = self.host
648        if not self.installed:
649            self.install(host)
650        opts = ["%s=%s" % (o[0], repr(o[1])) for o in dargs.items()]
651        cmd = ", ".join([repr(test_name)] + map(repr, args) + opts)
652        control = "job.run_test(%s)\n" % cmd
653        self.run(control, results_dir, host, timeout=timeout,
654                 parallel_flag=parallel_flag, background=background,
655                 client_disconnect_timeout=client_disconnect_timeout)
656
657        if dargs.get('check_client_result', False):
658            self._check_client_test_result(host, test_name)
659
660
661    def run_test(self, test_name, results_dir='.', host=None,
662                 parallel_flag=False, background=False,
663                 client_disconnect_timeout=None, *args, **dargs):
664        self.run_timed_test(test_name, results_dir, host, timeout=None,
665                            parallel_flag=parallel_flag, background=background,
666                            client_disconnect_timeout=client_disconnect_timeout,
667                            *args, **dargs)
668
669
670    def run_static_method(self, module, method, results_dir='.', host=None,
671                          *args):
672        """Runs a non-instance method with |args| from |module| on the client.
673
674        This method runs a static/class/module autotest method on the client.
675        For example:
676          run_static_method("autotest_lib.client.cros.cros_ui", "reboot")
677
678        Will run autotest_lib.client.cros.cros_ui.reboot() on the client.
679
680        @param module: module name as you would refer to it when importing in a
681            control file. e.g. autotest_lib.client.common_lib.module_name.
682        @param method: the method you want to call.
683        @param results_dir: A str path where the results should be stored
684            on the local filesystem.
685        @param host: A Host instance on which the control file should
686            be run.
687        @param args: args to pass to the method.
688        """
689        control = "\n".join(["import %s" % module,
690                             "%s.%s(%s)\n" % (module, method,
691                                              ','.join(map(repr, args)))])
692        self.run(control, results_dir=results_dir, host=host)
693
694
695class _Run(object):
696    """
697    Represents a run of autotest control file.  This class maintains
698    all the state necessary as an autotest control file is executed.
699
700    It is not intended to be used directly, rather control files
701    should be run using the run method in Autotest.
702    """
703    def __init__(self, host, results_dir, tag, parallel_flag, background):
704        self.host = host
705        self.results_dir = results_dir
706        self.env = host.env
707        self.tag = tag
708        self.parallel_flag = parallel_flag
709        self.background = background
710        self.autodir = Autotest.get_installed_autodir(self.host)
711        control = os.path.join(self.autodir, 'control')
712        if tag:
713            control += '.' + tag
714        self.manual_control_file = control
715        self.remote_control_file = control + '.autoserv'
716        self.config_file = os.path.join(self.autodir, 'global_config.ini')
717
718
719    def verify_machine(self):
720        binary = os.path.join(self.autodir, 'bin/autotest')
721        try:
722            self.host.run('ls %s > /dev/null 2>&1' % binary)
723        except:
724            raise error.AutoservInstallError(
725                "Autotest does not appear to be installed")
726
727        if not self.parallel_flag:
728            tmpdir = os.path.join(self.autodir, 'tmp')
729            download = os.path.join(self.autodir, 'tests/download')
730            self.host.run('umount %s' % tmpdir, ignore_status=True)
731            self.host.run('umount %s' % download, ignore_status=True)
732
733
734    def get_base_cmd_args(self, section):
735        args = ['--verbose']
736        if section > 0:
737            args.append('-c')
738        if self.tag:
739            args.append('-t %s' % self.tag)
740        if self.host.job.use_external_logging():
741            args.append('-l')
742        if self.host.hostname:
743            args.append('--hostname=%s' % self.host.hostname)
744        args.append('--user=%s' % self.host.job.user)
745
746        args.append(self.remote_control_file)
747        return args
748
749
750    def get_background_cmd(self, section):
751        cmd = ['nohup', os.path.join(self.autodir, 'bin/autotest_client')]
752        cmd += self.get_base_cmd_args(section)
753        cmd += ['>/dev/null', '2>/dev/null', '&']
754        return ' '.join(cmd)
755
756
757    def get_daemon_cmd(self, section, monitor_dir):
758        cmd = ['nohup', os.path.join(self.autodir, 'bin/autotestd'),
759               monitor_dir, '-H autoserv']
760        cmd += self.get_base_cmd_args(section)
761        cmd += ['>/dev/null', '2>/dev/null', '&']
762        return ' '.join(cmd)
763
764
765    def get_monitor_cmd(self, monitor_dir, stdout_read, stderr_read):
766        cmd = [os.path.join(self.autodir, 'bin', 'autotestd_monitor'),
767               monitor_dir, str(stdout_read), str(stderr_read)]
768        return ' '.join(cmd)
769
770
771    def get_client_log(self):
772        """Find what the "next" client.* prefix should be
773
774        @returns A string of the form client.INTEGER that should be prefixed
775            to all client debug log files.
776        """
777        max_digit = -1
778        debug_dir = os.path.join(self.results_dir, 'debug')
779        client_logs = glob.glob(os.path.join(debug_dir, 'client.*.*'))
780        for log in client_logs:
781            _, number, _ = log.split('.', 2)
782            if number.isdigit():
783                max_digit = max(max_digit, int(number))
784        return 'client.%d' % (max_digit + 1)
785
786
787    def copy_client_config_file(self, client_log_prefix=None):
788        """
789        Create and copy the client config file based on the server config.
790
791        @param client_log_prefix: Optional prefix to prepend to log files.
792        """
793        client_config_file = self._create_client_config_file(client_log_prefix)
794        self.host.send_file(client_config_file, self.config_file)
795        os.remove(client_config_file)
796
797
798    def _create_client_config_file(self, client_log_prefix=None):
799        """
800        Create a temporary file with the [CLIENT] section configuration values
801        taken from the server global_config.ini.
802
803        @param client_log_prefix: Optional prefix to prepend to log files.
804
805        @return: Path of the temporary file generated.
806        """
807        config = global_config.global_config.get_section_values('CLIENT')
808        if client_log_prefix:
809            config.set('CLIENT', 'default_logging_name', client_log_prefix)
810        return self._create_aux_file(config.write)
811
812
813    def _create_aux_file(self, func, *args):
814        """
815        Creates a temporary file and writes content to it according to a
816        content creation function. The file object is appended to *args, which
817        is then passed to the content creation function
818
819        @param func: Function that will be used to write content to the
820                temporary file.
821        @param *args: List of parameters that func takes.
822        @return: Path to the temporary file that was created.
823        """
824        fd, path = tempfile.mkstemp(dir=self.host.job.tmpdir)
825        aux_file = os.fdopen(fd, "w")
826        try:
827            list_args = list(args)
828            list_args.append(aux_file)
829            func(*list_args)
830        finally:
831            aux_file.close()
832        return path
833
834
835    @staticmethod
836    def is_client_job_finished(last_line):
837        return bool(re.match(r'^END .*\t----\t----\t.*$', last_line))
838
839
840    @staticmethod
841    def is_client_job_rebooting(last_line):
842        return bool(re.match(r'^\t*GOOD\t----\treboot\.start.*$', last_line))
843
844
845    def _diagnose_dut(self, old_boot_id=None):
846        """
847        Run diagnostic checks on a DUT.
848
849        1. ping: A dead host will not respond to pings.
850        2. ssh (happens with 3.): DUT hangs usually fail in authentication
851            but respond to pings.
852        3. Check if a reboot occured: A healthy but unexpected reboot leaves the
853            host running with a new boot id.
854
855        This method will always raise an exception from the AutotestFailure
856        family and should only get called when the reason for a test failing
857        is ambiguous.
858
859        @raises AutotestDeviceNotPingable: If the DUT doesn't respond to ping.
860        @raises AutotestDeviceNotSSHable: If we cannot SSH into the DUT.
861        @raises AutotestDeviceRebooted: If the boot id changed.
862        @raises AutotestAbort: If none of the above exceptions were raised.
863            Since we have no recourse we must abort at this stage.
864        """
865        msg = 'Autotest client terminated unexpectedly: '
866        if utils.ping(self.host.hostname, tries=1, deadline=1) != 0:
867            msg += 'DUT is no longer pingable, it may have rebooted or hung.\n'
868            raise AutotestDeviceNotPingable(msg)
869
870        if old_boot_id:
871            try:
872                new_boot_id = self.host.get_boot_id(timeout=60)
873            except Exception as e:
874                msg += ('DUT is pingable but not SSHable, it most likely'
875                        ' sporadically rebooted during testing. %s\n' % str(e))
876                raise AutotestDeviceNotSSHable(msg)
877            else:
878                if new_boot_id != old_boot_id:
879                    msg += 'DUT rebooted during the test run.\n'
880                    raise AutotestDeviceRebooted(msg)
881
882            msg += ('DUT is pingable, SSHable and did NOT restart '
883                    'un-expectedly. We probably lost connectivity during the '
884                    'test.')
885        else:
886            msg += ('DUT is pingable, could not determine if an un-expected '
887                    'reboot occured during the test.')
888
889        raise AutotestAbort(msg)
890
891
892    def log_unexpected_abort(self, stderr_redirector, old_boot_id=None):
893        """
894        Logs that something unexpected happened, then tries to diagnose the
895        failure. The purpose of this function is only to close out the status
896        log with the appropriate error message, not to critically terminate
897        the program.
898
899        @param stderr_redirector: log stream.
900        @param old_boot_id: boot id used to infer if a reboot occured.
901        """
902        stderr_redirector.flush_all_buffers()
903        try:
904            self._diagnose_dut(old_boot_id)
905        except AutotestFailure as e:
906            self.host.job.record('END ABORT', None, None, str(e))
907
908
909    def _execute_in_background(self, section, timeout):
910        full_cmd = self.get_background_cmd(section)
911        devnull = open(os.devnull, "w")
912
913        self.copy_client_config_file(self.get_client_log())
914
915        self.host.job.push_execution_context(self.results_dir)
916        try:
917            result = self.host.run(full_cmd, ignore_status=True,
918                                   timeout=timeout,
919                                   stdout_tee=devnull,
920                                   stderr_tee=devnull)
921        finally:
922            self.host.job.pop_execution_context()
923
924        return result
925
926
927    @staticmethod
928    def _strip_stderr_prologue(stderr):
929        """Strips the 'standard' prologue that get pre-pended to every
930        remote command and returns the text that was actually written to
931        stderr by the remote command."""
932        stderr_lines = stderr.split("\n")[1:]
933        if not stderr_lines:
934            return ""
935        elif stderr_lines[0].startswith("NOTE: autotestd_monitor"):
936            del stderr_lines[0]
937        return "\n".join(stderr_lines)
938
939
940    def _execute_daemon(self, section, timeout, stderr_redirector,
941                        client_disconnect_timeout):
942        monitor_dir = self.host.get_tmp_dir()
943        daemon_cmd = self.get_daemon_cmd(section, monitor_dir)
944
945        # grab the location for the server-side client log file
946        client_log_prefix = self.get_client_log()
947        client_log_path = os.path.join(self.results_dir, 'debug',
948                                       client_log_prefix + '.log')
949        client_log = open(client_log_path, 'w', 0)
950        self.copy_client_config_file(client_log_prefix)
951
952        stdout_read = stderr_read = 0
953        self.host.job.push_execution_context(self.results_dir)
954        try:
955            self.host.run(daemon_cmd, ignore_status=True, timeout=timeout)
956            disconnect_warnings = []
957            while True:
958                monitor_cmd = self.get_monitor_cmd(monitor_dir, stdout_read,
959                                                   stderr_read)
960                try:
961                    result = self.host.run(monitor_cmd, ignore_status=True,
962                                           timeout=timeout,
963                                           stdout_tee=client_log,
964                                           stderr_tee=stderr_redirector)
965                except error.AutoservRunError, e:
966                    result = e.result_obj
967                    result.exit_status = None
968                    disconnect_warnings.append(e.description)
969
970                    stderr_redirector.log_warning(
971                        "Autotest client was disconnected: %s" % e.description,
972                        "NETWORK")
973                except error.AutoservSSHTimeout:
974                    result = utils.CmdResult(monitor_cmd, "", "", None, 0)
975                    stderr_redirector.log_warning(
976                        "Attempt to connect to Autotest client timed out",
977                        "NETWORK")
978
979                stdout_read += len(result.stdout)
980                stderr_read += len(self._strip_stderr_prologue(result.stderr))
981
982                if result.exit_status is not None:
983                    # TODO (crosbug.com/38224)- sbasi: Remove extra logging.
984                    logging.debug('Result exit status is %d.',
985                                  result.exit_status)
986                    return result
987                elif not self.host.wait_up(client_disconnect_timeout):
988                    raise error.AutoservSSHTimeout(
989                        "client was disconnected, reconnect timed out")
990        finally:
991            client_log.close()
992            self.host.job.pop_execution_context()
993
994
995    def execute_section(self, section, timeout, stderr_redirector,
996                        client_disconnect_timeout):
997        # TODO(crbug.com/684311) The claim is that section is never more than 0
998        # in pratice. After validating for a week or so, delete all support of
999        # multiple sections.
1000        metrics.Counter('chromeos/autotest/autotest/sections').increment(
1001                fields={'is_first_section': (section == 0)})
1002        logging.info("Executing %s/bin/autotest %s/control phase %d",
1003                     self.autodir, self.autodir, section)
1004
1005        if self.background:
1006            result = self._execute_in_background(section, timeout)
1007        else:
1008            result = self._execute_daemon(section, timeout, stderr_redirector,
1009                                          client_disconnect_timeout)
1010
1011        last_line = stderr_redirector.last_line
1012
1013        # check if we failed hard enough to warrant an exception
1014        if result.exit_status == 1:
1015            err = error.AutotestRunError("client job was aborted")
1016        elif not self.background and not result.stderr:
1017            err = error.AutotestRunError(
1018                "execute_section %s failed to return anything\n"
1019                "stdout:%s\n" % (section, result.stdout))
1020        else:
1021            err = None
1022
1023        # log something if the client failed AND never finished logging
1024        if err and not self.is_client_job_finished(last_line):
1025            self.log_unexpected_abort(stderr_redirector)
1026
1027        if err:
1028            raise err
1029        else:
1030            return stderr_redirector.last_line
1031
1032
1033    def _wait_for_reboot(self, old_boot_id):
1034        logging.info("Client is rebooting")
1035        logging.info("Waiting for client to halt")
1036        if not self.host.wait_down(self.host.WAIT_DOWN_REBOOT_TIMEOUT,
1037                                   old_boot_id=old_boot_id):
1038            err = "%s failed to shutdown after %d"
1039            err %= (self.host.hostname, self.host.WAIT_DOWN_REBOOT_TIMEOUT)
1040            raise error.AutotestRunError(err)
1041        logging.info("Client down, waiting for restart")
1042        if not self.host.wait_up(self.host.DEFAULT_REBOOT_TIMEOUT):
1043            # since reboot failed
1044            # hardreset the machine once if possible
1045            # before failing this control file
1046            warning = "%s did not come back up, hard resetting"
1047            warning %= self.host.hostname
1048            logging.warning(warning)
1049            try:
1050                self.host.hardreset(wait=False)
1051            except (AttributeError, error.AutoservUnsupportedError):
1052                warning = "Hard reset unsupported on %s"
1053                warning %= self.host.hostname
1054                logging.warning(warning)
1055            raise error.AutotestRunError("%s failed to boot after %ds" %
1056                                         (self.host.hostname,
1057                                          self.host.DEFAULT_REBOOT_TIMEOUT))
1058        self.host.reboot_followup()
1059
1060
1061    def execute_control(self, timeout=None, client_disconnect_timeout=None):
1062        if not self.background:
1063            collector = log_collector(self.host, self.tag, self.results_dir)
1064            hostname = self.host.hostname
1065            remote_results = collector.client_results_dir
1066            local_results = collector.server_results_dir
1067            self.host.job.add_client_log(hostname, remote_results,
1068                                         local_results)
1069            job_record_context = self.host.job.get_record_context()
1070
1071        section = 0
1072        start_time = time.time()
1073
1074        logger = client_logger(self.host, self.tag, self.results_dir)
1075        try:
1076            while not timeout or time.time() < start_time + timeout:
1077                if timeout:
1078                    section_timeout = start_time + timeout - time.time()
1079                else:
1080                    section_timeout = None
1081                boot_id = self.host.get_boot_id()
1082                last = self.execute_section(section, section_timeout,
1083                                            logger, client_disconnect_timeout)
1084                if self.background:
1085                    return
1086                section += 1
1087                if self.is_client_job_finished(last):
1088                    logging.info("Client complete")
1089                    return
1090                elif self.is_client_job_rebooting(last):
1091                    try:
1092                        self._wait_for_reboot(boot_id)
1093                    except error.AutotestRunError, e:
1094                        self.host.job.record("ABORT", None, "reboot", str(e))
1095                        self.host.job.record("END ABORT", None, None, str(e))
1096                        raise
1097                    continue
1098
1099                # If a test fails without probable cause we try to bucket it's
1100                # failure into one of 2 categories. If we can determine the
1101                # current state of the device and it is suspicious, we close the
1102                # status lines indicating a failure. If we either cannot
1103                # determine the state of the device, or it appears totally
1104                # healthy, we give up and abort.
1105                try:
1106                    self._diagnose_dut(boot_id)
1107                except AutotestDeviceError as e:
1108                    # The status lines of the test are pretty much tailed to
1109                    # our log, with indentation, from the client job on the DUT.
1110                    # So if the DUT goes down unexpectedly we'll end up with a
1111                    # malformed status log unless we manually unwind the status
1112                    # stack. Ideally we would want to write a nice wrapper like
1113                    # server_job methods run_reboot, run_group but they expect
1114                    # reboots and we don't.
1115                    self.host.job.record('FAIL', None, None, str(e))
1116                    self.host.job.record('END FAIL', None, None)
1117                    self.host.job.record('END GOOD', None, None)
1118                    self.host.job.failed_with_device_error = True
1119                    return
1120                except AutotestAbort as e:
1121                    self.host.job.record('ABORT', None, None, str(e))
1122                    self.host.job.record('END ABORT', None, None)
1123
1124                    # give the client machine a chance to recover from a crash
1125                    self.host.wait_up(
1126                        self.host.HOURS_TO_WAIT_FOR_RECOVERY * 3600)
1127                    logging.debug('Unexpected final status message from '
1128                                  'client %s: %s', self.host.hostname, last)
1129                    # The line 'last' may have sensitive phrases, like
1130                    # 'END GOOD', which breaks the tko parser. So the error
1131                    # message will exclude it, since it will be recorded to
1132                    # status.log.
1133                    msg = ("Aborting - unexpected final status message from "
1134                           "client on %s\n") % self.host.hostname
1135                    raise error.AutotestRunError(msg)
1136        finally:
1137            logging.debug('Autotest job finishes running. Below is the '
1138                          'post-processing operations.')
1139            logger.close()
1140            if not self.background:
1141                collector.collect_client_job_results()
1142                collector.remove_redundant_client_logs()
1143                state_file = os.path.basename(self.remote_control_file
1144                                              + '.state')
1145                state_path = os.path.join(self.results_dir, state_file)
1146                self.host.job.postprocess_client_state(state_path)
1147                self.host.job.remove_client_log(hostname, remote_results,
1148                                                local_results)
1149                job_record_context.restore()
1150
1151            logging.debug('Autotest job finishes.')
1152
1153        # should only get here if we timed out
1154        assert timeout
1155        raise error.AutotestTimeoutError()
1156
1157
1158class log_collector(object):
1159    def __init__(self, host, client_tag, results_dir):
1160        self.host = host
1161        if not client_tag:
1162            client_tag = "default"
1163        self.client_results_dir = os.path.join(host.get_autodir(), "results",
1164                                               client_tag)
1165        self.server_results_dir = results_dir
1166
1167
1168    def collect_client_job_results(self):
1169        """ A method that collects all the current results of a running
1170        client job into the results dir. By default does nothing as no
1171        client job is running, but when running a client job you can override
1172        this with something that will actually do something. """
1173        # make an effort to wait for the machine to come up
1174        try:
1175            self.host.wait_up(timeout=30)
1176        except error.AutoservError:
1177            # don't worry about any errors, we'll try and
1178            # get the results anyway
1179            pass
1180
1181        # Copy all dirs in default to results_dir
1182        try:
1183            # Build test result directory summary
1184            result_tools_runner.run_on_client(
1185                    self.host, self.client_results_dir)
1186
1187            with metrics.SecondsTimer(
1188                    'chromeos/autotest/job/log_collection_duration',
1189                    fields={'dut_host_name': self.host.hostname}):
1190                self.host.get_file(
1191                        self.client_results_dir + '/',
1192                        self.server_results_dir,
1193                        preserve_symlinks=True)
1194        except Exception:
1195            # well, don't stop running just because we couldn't get logs
1196            e_msg = "Unexpected error copying test result logs, continuing ..."
1197            logging.error(e_msg)
1198            traceback.print_exc(file=sys.stdout)
1199
1200
1201    def remove_redundant_client_logs(self):
1202        """Remove client.*.log files in favour of client.*.DEBUG files."""
1203        debug_dir = os.path.join(self.server_results_dir, 'debug')
1204        debug_files = [f for f in os.listdir(debug_dir)
1205                       if re.search(r'^client\.\d+\.DEBUG$', f)]
1206        for debug_file in debug_files:
1207            log_file = debug_file.replace('DEBUG', 'log')
1208            log_file = os.path.join(debug_dir, log_file)
1209            if os.path.exists(log_file):
1210                os.remove(log_file)
1211
1212
1213# a file-like object for catching stderr from an autotest client and
1214# extracting status logs from it
1215class client_logger(object):
1216    """Partial file object to write to both stdout and
1217    the status log file.  We only implement those methods
1218    utils.run() actually calls.
1219    """
1220    status_parser = re.compile(r"^AUTOTEST_STATUS:([^:]*):(.*)$")
1221    test_complete_parser = re.compile(r"^AUTOTEST_TEST_COMPLETE:(.*)$")
1222    fetch_package_parser = re.compile(
1223        r"^AUTOTEST_FETCH_PACKAGE:([^:]*):([^:]*):(.*)$")
1224    extract_indent = re.compile(r"^(\t*).*$")
1225    extract_timestamp = re.compile(r".*\ttimestamp=(\d+)\t.*$")
1226
1227    def __init__(self, host, tag, server_results_dir):
1228        self.host = host
1229        self.job = host.job
1230        self.log_collector = log_collector(host, tag, server_results_dir)
1231        self.leftover = ""
1232        self.last_line = ""
1233        self.logs = {}
1234
1235
1236    def _process_log_dict(self, log_dict):
1237        log_list = log_dict.pop("logs", [])
1238        for key in sorted(log_dict.iterkeys()):
1239            log_list += self._process_log_dict(log_dict.pop(key))
1240        return log_list
1241
1242
1243    def _process_logs(self):
1244        """Go through the accumulated logs in self.log and print them
1245        out to stdout and the status log. Note that this processes
1246        logs in an ordering where:
1247
1248        1) logs to different tags are never interleaved
1249        2) logs to x.y come before logs to x.y.z for all z
1250        3) logs to x.y come before x.z whenever y < z
1251
1252        Note that this will in general not be the same as the
1253        chronological ordering of the logs. However, if a chronological
1254        ordering is desired that one can be reconstructed from the
1255        status log by looking at timestamp lines."""
1256        log_list = self._process_log_dict(self.logs)
1257        for entry in log_list:
1258            self.job.record_entry(entry, log_in_subdir=False)
1259        if log_list:
1260            self.last_line = log_list[-1].render()
1261
1262
1263    def _process_quoted_line(self, tag, line):
1264        """Process a line quoted with an AUTOTEST_STATUS flag. If the
1265        tag is blank then we want to push out all the data we've been
1266        building up in self.logs, and then the newest line. If the
1267        tag is not blank, then push the line into the logs for handling
1268        later."""
1269        entry = base_job.status_log_entry.parse(line)
1270        if entry is None:
1271            return  # the line contains no status lines
1272        if tag == "":
1273            self._process_logs()
1274            self.job.record_entry(entry, log_in_subdir=False)
1275            self.last_line = line
1276        else:
1277            tag_parts = [int(x) for x in tag.split(".")]
1278            log_dict = self.logs
1279            for part in tag_parts:
1280                log_dict = log_dict.setdefault(part, {})
1281            log_list = log_dict.setdefault("logs", [])
1282            log_list.append(entry)
1283
1284
1285    def _process_info_line(self, line):
1286        """Check if line is an INFO line, and if it is, interpret any control
1287        messages (e.g. enabling/disabling warnings) that it may contain."""
1288        match = re.search(r"^\t*INFO\t----\t----(.*)\t[^\t]*$", line)
1289        if not match:
1290            return   # not an INFO line
1291        for field in match.group(1).split('\t'):
1292            if field.startswith("warnings.enable="):
1293                func = self.job.warning_manager.enable_warnings
1294            elif field.startswith("warnings.disable="):
1295                func = self.job.warning_manager.disable_warnings
1296            else:
1297                continue
1298            warning_type = field.split("=", 1)[1]
1299            func(warning_type)
1300
1301
1302    def _process_line(self, line):
1303        """Write out a line of data to the appropriate stream.
1304
1305        Returns the package checksum file if it exists.
1306
1307        Status lines sent by autotest will be prepended with
1308        "AUTOTEST_STATUS", and all other lines are ssh error messages.
1309        """
1310        logging.debug(line)
1311        fetch_package_match = self.fetch_package_parser.search(line)
1312        if fetch_package_match:
1313            pkg_name, dest_path, fifo_path = fetch_package_match.groups()
1314            serve_packages = _CONFIG.get_config_value(
1315                "PACKAGES", "serve_packages_from_autoserv", type=bool)
1316            if serve_packages and pkg_name == 'packages.checksum':
1317                try:
1318                    checksum_file = os.path.join(
1319                        self.job.pkgmgr.pkgmgr_dir, 'packages', pkg_name)
1320                    if os.path.exists(checksum_file):
1321                        self.host.send_file(checksum_file, dest_path)
1322                except error.AutoservRunError:
1323                    msg = "Package checksum file not found, continuing anyway"
1324                    logging.exception(msg)
1325
1326                try:
1327                    # When fetching a package, the client expects to be
1328                    # notified when the fetching is complete. Autotest
1329                    # does this pushing a B to a fifo queue to the client.
1330                    self.host.run("echo B > %s" % fifo_path)
1331                except error.AutoservRunError:
1332                    msg = "Checksum installation failed, continuing anyway"
1333                    logging.exception(msg)
1334                finally:
1335                    return
1336
1337        status_match = self.status_parser.search(line)
1338        test_complete_match = self.test_complete_parser.search(line)
1339        fetch_package_match = self.fetch_package_parser.search(line)
1340        if status_match:
1341            tag, line = status_match.groups()
1342            self._process_info_line(line)
1343            self._process_quoted_line(tag, line)
1344        elif test_complete_match:
1345            self._process_logs()
1346            fifo_path, = test_complete_match.groups()
1347            try:
1348                self.log_collector.collect_client_job_results()
1349                self.host.run("echo A > %s" % fifo_path)
1350            except Exception:
1351                msg = "Post-test log collection failed, continuing anyway"
1352                logging.exception(msg)
1353        elif fetch_package_match:
1354            pkg_name, dest_path, fifo_path = fetch_package_match.groups()
1355            serve_packages = global_config.global_config.get_config_value(
1356                "PACKAGES", "serve_packages_from_autoserv", type=bool)
1357            if serve_packages and pkg_name.endswith(".tar.bz2"):
1358                try:
1359                    self._send_tarball(pkg_name, dest_path)
1360                except Exception:
1361                    msg = "Package tarball creation failed, continuing anyway"
1362                    logging.exception(msg)
1363            try:
1364                self.host.run("echo B > %s" % fifo_path)
1365            except Exception:
1366                msg = "Package tarball installation failed, continuing anyway"
1367                logging.exception(msg)
1368        else:
1369            logging.info(line)
1370
1371
1372    def _send_tarball(self, pkg_name, remote_dest):
1373        """Uses tarballs in package manager by default."""
1374        try:
1375            server_package = os.path.join(self.job.pkgmgr.pkgmgr_dir,
1376                                          'packages', pkg_name)
1377            if os.path.exists(server_package):
1378              self.host.send_file(server_package, remote_dest)
1379              return
1380
1381        except error.AutoservRunError:
1382            msg = ("Package %s could not be sent from the package cache." %
1383                   pkg_name)
1384            logging.exception(msg)
1385
1386        name, pkg_type = self.job.pkgmgr.parse_tarball_name(pkg_name)
1387        src_dirs = []
1388        if pkg_type == 'test':
1389            for test_dir in ['site_tests', 'tests']:
1390                src_dir = os.path.join(self.job.clientdir, test_dir, name)
1391                if os.path.exists(src_dir):
1392                    src_dirs += [src_dir]
1393                    break
1394        elif pkg_type == 'profiler':
1395            src_dirs += [os.path.join(self.job.clientdir, 'profilers', name)]
1396        elif pkg_type == 'dep':
1397            src_dirs += [os.path.join(self.job.clientdir, 'deps', name)]
1398        elif pkg_type == 'client':
1399            return  # you must already have a client to hit this anyway
1400        else:
1401            return  # no other types are supported
1402
1403        # iterate over src_dirs until we find one that exists, then tar it
1404        for src_dir in src_dirs:
1405            if os.path.exists(src_dir):
1406                try:
1407                    logging.info('Bundling %s into %s', src_dir, pkg_name)
1408                    temp_dir = autotemp.tempdir(unique_id='autoserv-packager',
1409                                                dir=self.job.tmpdir)
1410                    tarball_path = self.job.pkgmgr.tar_package(
1411                        pkg_name, src_dir, temp_dir.name, " .")
1412                    self.host.send_file(tarball_path, remote_dest)
1413                finally:
1414                    temp_dir.clean()
1415                return
1416
1417
1418    def log_warning(self, msg, warning_type):
1419        """Injects a WARN message into the current status logging stream."""
1420        timestamp = int(time.time())
1421        if self.job.warning_manager.is_valid(timestamp, warning_type):
1422            self.job.record('WARN', None, None, msg)
1423
1424
1425    def write(self, data):
1426        # now start processing the existing buffer and the new data
1427        data = self.leftover + data
1428        lines = data.split('\n')
1429        processed_lines = 0
1430        try:
1431            # process all the buffered data except the last line
1432            # ignore the last line since we may not have all of it yet
1433            for line in lines[:-1]:
1434                self._process_line(line)
1435                processed_lines += 1
1436        finally:
1437            # save any unprocessed lines for future processing
1438            self.leftover = '\n'.join(lines[processed_lines:])
1439
1440
1441    def flush(self):
1442        sys.stdout.flush()
1443
1444
1445    def flush_all_buffers(self):
1446        if self.leftover:
1447            self._process_line(self.leftover)
1448            self.leftover = ""
1449        self._process_logs()
1450        self.flush()
1451
1452
1453    def close(self):
1454        self.flush_all_buffers()
1455