1#!/usr/bin/python -u
2# Copyright 2007-2008 Martin J. Bligh <mbligh@google.com>, Google Inc.
3# Released under the GPL v2
4
5"""
6Run a control file through the server side engine
7"""
8
9import ast
10import datetime
11import getpass
12import logging
13import os
14import re
15import signal
16import socket
17import sys
18import traceback
19import time
20import urllib2
21
22import common
23
24from autotest_lib.client.common_lib import control_data
25from autotest_lib.client.common_lib import error
26from autotest_lib.client.common_lib import global_config
27from autotest_lib.client.common_lib import utils
28from autotest_lib.client.common_lib.cros.graphite import autotest_es
29from autotest_lib.client.common_lib.cros.graphite import autotest_stats
30try:
31    from autotest_lib.puppylab import results_mocker
32except ImportError:
33    results_mocker = None
34
35_CONFIG = global_config.global_config
36
37require_atfork = _CONFIG.get_config_value(
38        'AUTOSERV', 'require_atfork_module', type=bool, default=True)
39
40
41# Number of seconds to wait before returning if testing mode is enabled
42TESTING_MODE_SLEEP_SECS = 1
43
44try:
45    import atfork
46    atfork.monkeypatch_os_fork_functions()
47    import atfork.stdlib_fixer
48    # Fix the Python standard library for threading+fork safety with its
49    # internal locks.  http://code.google.com/p/python-atfork/
50    import warnings
51    warnings.filterwarnings('ignore', 'logging module already imported')
52    atfork.stdlib_fixer.fix_logging_module()
53except ImportError, e:
54    from autotest_lib.client.common_lib import global_config
55    if _CONFIG.get_config_value(
56            'AUTOSERV', 'require_atfork_module', type=bool, default=False):
57        print >>sys.stderr, 'Please run utils/build_externals.py'
58        print e
59        sys.exit(1)
60
61from autotest_lib.server import frontend
62from autotest_lib.server import server_logging_config
63from autotest_lib.server import server_job, utils, autoserv_parser, autotest
64from autotest_lib.server import utils as server_utils
65from autotest_lib.site_utils import job_directories
66from autotest_lib.site_utils import job_overhead
67from autotest_lib.site_utils import lxc
68from autotest_lib.site_utils import lxc_utils
69from autotest_lib.client.common_lib import pidfile, logging_manager
70from autotest_lib.client.common_lib.cros.graphite import autotest_stats
71
72# Control segment to stage server-side package.
73STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE = server_job._control_segment_path(
74        'stage_server_side_package')
75
76# Command line to start servod in a moblab.
77START_SERVOD_CMD = 'sudo start servod BOARD=%s PORT=%s'
78STOP_SERVOD_CMD = 'sudo stop servod'
79
80def log_alarm(signum, frame):
81    logging.error("Received SIGALARM. Ignoring and continuing on.")
82    sys.exit(1)
83
84
85def _get_machines(parser):
86    """Get a list of machine names from command line arg -m or a file.
87
88    @param parser: Parser for the command line arguments.
89
90    @return: A list of machine names from command line arg -m or the
91             machines file specified in the command line arg -M.
92    """
93    if parser.options.machines:
94        machines = parser.options.machines.replace(',', ' ').strip().split()
95    else:
96        machines = []
97    machines_file = parser.options.machines_file
98    if machines_file:
99        machines = []
100        for m in open(machines_file, 'r').readlines():
101            # remove comments, spaces
102            m = re.sub('#.*', '', m).strip()
103            if m:
104                machines.append(m)
105        logging.debug('Read list of machines from file: %s', machines_file)
106        logging.debug('Machines: %s', ','.join(machines))
107
108    if machines:
109        for machine in machines:
110            if not machine or re.search('\s', machine):
111                parser.parser.error("Invalid machine: %s" % str(machine))
112        machines = list(set(machines))
113        machines.sort()
114    return machines
115
116
117def _stage_ssp(parser):
118    """Stage server-side package.
119
120    This function calls a control segment to stage server-side package based on
121    the job and autoserv command line option. The detail implementation could
122    be different for each host type. Currently, only CrosHost has
123    stage_server_side_package function defined.
124    The script returns None if no server-side package is available. However,
125    it may raise exception if it failed for reasons other than artifact (the
126    server-side package) not found.
127
128    @param parser: Command line arguments parser passed in the autoserv process.
129
130    @return: url of the staged server-side package. Return None if server-
131             side package is not found for the build.
132    """
133    # If test_source_build is not specified, default to use server-side test
134    # code from build specified in --image.
135    namespace = {'machines': _get_machines(parser),
136                 'image': (parser.options.test_source_build or
137                           parser.options.image),}
138    script_locals = {}
139    execfile(STAGE_SERVER_SIDE_PACKAGE_CONTROL_FILE, namespace, script_locals)
140    return script_locals['ssp_url']
141
142
143def _run_with_ssp(job, container_name, job_id, results, parser, ssp_url):
144    """Run the server job with server-side packaging.
145
146    @param job: The server job object.
147    @param container_name: Name of the container to run the test.
148    @param job_id: ID of the test job.
149    @param results: Folder to store results. This could be different from
150                    parser.options.results:
151                    parser.options.results  can be set to None for results to be
152                    stored in a temp folder.
153                    results can be None for autoserv run requires no logging.
154    @param parser: Command line parser that contains the options.
155    @param ssp_url: url of the staged server-side package.
156    """
157    bucket = lxc.ContainerBucket()
158    control = (parser.args[0] if len(parser.args) > 0 and parser.args[0] != ''
159               else None)
160    try:
161        test_container = bucket.setup_test(container_name, job_id, ssp_url,
162                                           results, control=control)
163    except Exception as e:
164        job.record('FAIL', None, None,
165                   'Failed to setup container for test: %s. Check logs in '
166                   'ssp_logs folder for more details.' % e)
167        raise
168
169    args = sys.argv[:]
170    args.remove('--require-ssp')
171    # --parent_job_id is only useful in autoserv running in host, not in
172    # container. Include this argument will cause test to fail for builds before
173    # CL 286265 was merged.
174    if '--parent_job_id' in args:
175        index = args.index('--parent_job_id')
176        args.remove('--parent_job_id')
177        # Remove the actual parent job id in command line arg.
178        del args[index]
179
180    # A dictionary of paths to replace in the command line. Key is the path to
181    # be replaced with the one in value.
182    paths_to_replace = {}
183    # Replace the control file path with the one in container.
184    if control:
185        container_control_filename = os.path.join(
186                lxc.CONTROL_TEMP_PATH, os.path.basename(control))
187        paths_to_replace[control] = container_control_filename
188    # Update result directory with the one in container.
189    if parser.options.results:
190        container_result_dir = os.path.join(lxc.RESULT_DIR_FMT % job_id)
191        paths_to_replace[parser.options.results] = container_result_dir
192    # Update parse_job directory with the one in container. The assumption is
193    # that the result folder to be parsed is always the same as the results_dir.
194    if parser.options.parse_job:
195        container_parse_dir = os.path.join(lxc.RESULT_DIR_FMT % job_id)
196        paths_to_replace[parser.options.parse_job] = container_result_dir
197
198    args = [paths_to_replace.get(arg, arg) for arg in args]
199
200    # Apply --use-existing-results, results directory is aready created and
201    # mounted in container. Apply this arg to avoid exception being raised.
202    if not '--use-existing-results' in args:
203        args.append('--use-existing-results')
204
205    # Make sure autoserv running in container using a different pid file.
206    if not '--pidfile-label' in args:
207        args.extend(['--pidfile-label', 'container_autoserv'])
208
209    cmd_line = ' '.join(["'%s'" % arg if ' ' in arg else arg for arg in args])
210    logging.info('Run command in container: %s', cmd_line)
211    success = False
212    try:
213        test_container.attach_run(cmd_line)
214        success = True
215    except Exception as e:
216        # If the test run inside container fails without generating any log,
217        # write a message to status.log to help troubleshooting.
218        debug_files = os.listdir(os.path.join(results, 'debug'))
219        if not debug_files:
220            job.record('FAIL', None, None,
221                       'Failed to run test inside the container: %s. Check '
222                       'logs in ssp_logs folder for more details.' % e)
223        raise
224    finally:
225        counter_key = '%s.%s' % (lxc.STATS_KEY,
226                                 'success' if success else 'fail')
227        autotest_stats.Counter(counter_key).increment()
228        # metadata is uploaded separately so it can use http to upload.
229        metadata = {'drone': socket.gethostname(),
230                    'job_id': job_id,
231                    'success': success}
232        autotest_es.post(use_http=True,
233                         type_str=lxc.CONTAINER_RUN_TEST_METADB_TYPE,
234                         metadata=metadata)
235        test_container.destroy()
236
237
238def correct_results_folder_permission(results):
239    """Make sure the results folder has the right permission settings.
240
241    For tests running with server-side packaging, the results folder has the
242    owner of root. This must be changed to the user running the autoserv
243    process, so parsing job can access the results folder.
244    TODO(dshi): crbug.com/459344 Remove this function when test container can be
245    unprivileged container.
246
247    @param results: Path to the results folder.
248
249    """
250    if not results:
251        return
252
253    try:
254        utils.run('sudo -n chown -R %s "%s"' % (os.getuid(), results))
255        utils.run('sudo -n chgrp -R %s "%s"' % (os.getgid(), results))
256    except error.CmdError as e:
257        metadata = {'error': str(e),
258                    'result_folder': results,
259                    'drone': socket.gethostname()}
260        autotest_es.post(use_http=True, type_str='correct_results_folder_failure',
261                         metadata=metadata)
262        raise
263
264
265def _start_servod(machine):
266    """Try to start servod in moblab if it's not already running or running with
267    different board or port.
268
269    @param machine: Name of the dut used for test.
270    """
271    if not utils.is_moblab():
272        return
273
274    logging.debug('Trying to start servod.')
275    try:
276        afe = frontend.AFE()
277        board = server_utils.get_board_from_afe(machine, afe)
278        hosts = afe.get_hosts(hostname=machine)
279        servo_host = hosts[0].attributes.get('servo_host', None)
280        servo_port = hosts[0].attributes.get('servo_port', 9999)
281        if not servo_host in ['localhost', '127.0.0.1']:
282            logging.warn('Starting servod is aborted. The dut\'s servo_host '
283                         'attribute is not set to localhost.')
284            return
285    except (urllib2.HTTPError, urllib2.URLError):
286        # Ignore error if RPC failed to get board
287        logging.error('Failed to get board name from AFE. Start servod is '
288                      'aborted')
289        return
290
291    try:
292        pid = utils.run('pgrep servod').stdout
293        cmd_line = utils.run('ps -fp %s' % pid).stdout
294        if ('--board %s' % board in cmd_line and
295            '--port %s' % servo_port in cmd_line):
296            logging.debug('Servod is already running with given board and port.'
297                          ' There is no need to restart servod.')
298            return
299        logging.debug('Servod is running with different board or port. '
300                      'Stopping existing servod.')
301        utils.run('sudo stop servod')
302    except error.CmdError:
303        # servod is not running.
304        pass
305
306    try:
307        utils.run(START_SERVOD_CMD % (board, servo_port))
308        logging.debug('Servod is started')
309    except error.CmdError as e:
310        logging.error('Servod failed to be started, error: %s', e)
311
312
313def run_autoserv(pid_file_manager, results, parser, ssp_url, use_ssp):
314    """Run server job with given options.
315
316    @param pid_file_manager: PidFileManager used to monitor the autoserv process
317    @param results: Folder to store results.
318    @param parser: Parser for the command line arguments.
319    @param ssp_url: Url to server-side package.
320    @param use_ssp: Set to True to run with server-side packaging.
321    """
322    if parser.options.warn_no_ssp:
323        # Post a warning in the log.
324        logging.warn('Autoserv is required to run with server-side packaging. '
325                     'However, no drone is found to support server-side '
326                     'packaging. The test will be executed in a drone without '
327                     'server-side packaging supported.')
328
329    # send stdin to /dev/null
330    dev_null = os.open(os.devnull, os.O_RDONLY)
331    os.dup2(dev_null, sys.stdin.fileno())
332    os.close(dev_null)
333
334    # Create separate process group
335    os.setpgrp()
336
337    # Container name is predefined so the container can be destroyed in
338    # handle_sigterm.
339    job_or_task_id = job_directories.get_job_id_or_task_id(
340            parser.options.results)
341    container_name = (lxc.TEST_CONTAINER_NAME_FMT %
342                      (job_or_task_id, time.time(), os.getpid()))
343
344    # Implement SIGTERM handler
345    def handle_sigterm(signum, frame):
346        logging.debug('Received SIGTERM')
347        if pid_file_manager:
348            pid_file_manager.close_file(1, signal.SIGTERM)
349        logging.debug('Finished writing to pid_file. Killing process.')
350
351        # Update results folder's file permission. This needs to be done ASAP
352        # before the parsing process tries to access the log.
353        if use_ssp and results:
354            correct_results_folder_permission(results)
355
356        # TODO (sbasi) - remove the time.sleep when crbug.com/302815 is solved.
357        # This sleep allows the pending output to be logged before the kill
358        # signal is sent.
359        time.sleep(.1)
360        if use_ssp:
361            logging.debug('Destroy container %s before aborting the autoserv '
362                          'process.', container_name)
363            metadata = {'drone': socket.gethostname(),
364                        'job_id': job_or_task_id,
365                        'container_name': container_name,
366                        'action': 'abort',
367                        'success': True}
368            try:
369                bucket = lxc.ContainerBucket()
370                container = bucket.get(container_name)
371                if container:
372                    container.destroy()
373                else:
374                    metadata['success'] = False
375                    metadata['error'] = 'container not found'
376                    logging.debug('Container %s is not found.', container_name)
377            except:
378                metadata['success'] = False
379                metadata['error'] = 'Exception: %s' % sys.exc_info()
380                # Handle any exception so the autoserv process can be aborted.
381                logging.error('Failed to destroy container %s. Error: %s',
382                              container_name, sys.exc_info())
383            autotest_es.post(use_http=True,
384                             type_str=lxc.CONTAINER_RUN_TEST_METADB_TYPE,
385                             metadata=metadata)
386            # Try to correct the result file permission again after the
387            # container is destroyed, as the container might have created some
388            # new files in the result folder.
389            if results:
390                correct_results_folder_permission(results)
391
392        os.killpg(os.getpgrp(), signal.SIGKILL)
393
394    # Set signal handler
395    signal.signal(signal.SIGTERM, handle_sigterm)
396
397    # faulthandler is only needed to debug in the Lab and is not avaliable to
398    # be imported in the chroot as part of VMTest, so Try-Except it.
399    try:
400        import faulthandler
401        faulthandler.register(signal.SIGTERM, all_threads=True, chain=True)
402        logging.debug('faulthandler registered on SIGTERM.')
403    except ImportError:
404        sys.exc_clear()
405
406    # Ignore SIGTTOU's generated by output from forked children.
407    signal.signal(signal.SIGTTOU, signal.SIG_IGN)
408
409    # If we received a SIGALARM, let's be loud about it.
410    signal.signal(signal.SIGALRM, log_alarm)
411
412    # Server side tests that call shell scripts often depend on $USER being set
413    # but depending on how you launch your autotest scheduler it may not be set.
414    os.environ['USER'] = getpass.getuser()
415
416    label = parser.options.label
417    group_name = parser.options.group_name
418    user = parser.options.user
419    client = parser.options.client
420    server = parser.options.server
421    install_before = parser.options.install_before
422    install_after = parser.options.install_after
423    verify = parser.options.verify
424    repair = parser.options.repair
425    cleanup = parser.options.cleanup
426    provision = parser.options.provision
427    reset = parser.options.reset
428    job_labels = parser.options.job_labels
429    no_tee = parser.options.no_tee
430    parse_job = parser.options.parse_job
431    execution_tag = parser.options.execution_tag
432    if not execution_tag:
433        execution_tag = parse_job
434    ssh_user = parser.options.ssh_user
435    ssh_port = parser.options.ssh_port
436    ssh_pass = parser.options.ssh_pass
437    collect_crashinfo = parser.options.collect_crashinfo
438    control_filename = parser.options.control_filename
439    test_retry = parser.options.test_retry
440    verify_job_repo_url = parser.options.verify_job_repo_url
441    skip_crash_collection = parser.options.skip_crash_collection
442    ssh_verbosity = int(parser.options.ssh_verbosity)
443    ssh_options = parser.options.ssh_options
444    no_use_packaging = parser.options.no_use_packaging
445    host_attributes = parser.options.host_attributes
446    in_lab = bool(parser.options.lab)
447
448    # can't be both a client and a server side test
449    if client and server:
450        parser.parser.error("Can not specify a test as both server and client!")
451
452    if provision and client:
453        parser.parser.error("Cannot specify provisioning and client!")
454
455    is_special_task = (verify or repair or cleanup or collect_crashinfo or
456                       provision or reset)
457    if len(parser.args) < 1 and not is_special_task:
458        parser.parser.error("Missing argument: control file")
459
460    if ssh_verbosity > 0:
461        # ssh_verbosity is an integer between 0 and 3, inclusive
462        ssh_verbosity_flag = '-' + 'v' * ssh_verbosity
463    else:
464        ssh_verbosity_flag = ''
465
466    # We have a control file unless it's just a verify/repair/cleanup job
467    if len(parser.args) > 0:
468        control = parser.args[0]
469    else:
470        control = None
471
472    machines = _get_machines(parser)
473    if group_name and len(machines) < 2:
474        parser.parser.error('-G %r may only be supplied with more than one '
475                            'machine.' % group_name)
476
477    kwargs = {'group_name': group_name, 'tag': execution_tag,
478              'disable_sysinfo': parser.options.disable_sysinfo}
479    if parser.options.parent_job_id:
480        kwargs['parent_job_id'] = int(parser.options.parent_job_id)
481    if control_filename:
482        kwargs['control_filename'] = control_filename
483    if host_attributes:
484        kwargs['host_attributes'] = host_attributes
485    kwargs['in_lab'] = in_lab
486    job = server_job.server_job(control, parser.args[1:], results, label,
487                                user, machines, client, parse_job,
488                                ssh_user, ssh_port, ssh_pass,
489                                ssh_verbosity_flag, ssh_options,
490                                test_retry, **kwargs)
491
492    job.logging.start_logging()
493    job.init_parser()
494
495    # perform checks
496    job.precheck()
497
498    # run the job
499    exit_code = 0
500    auto_start_servod = _CONFIG.get_config_value(
501            'AUTOSERV', 'auto_start_servod', type=bool, default=False)
502    try:
503        try:
504            if repair:
505                if auto_start_servod and len(machines) == 1:
506                    _start_servod(machines[0])
507                job.repair(job_labels)
508            elif verify:
509                job.verify(job_labels)
510            elif provision:
511                job.provision(job_labels)
512            elif reset:
513                job.reset(job_labels)
514            elif cleanup:
515                job.cleanup(job_labels)
516            else:
517                if auto_start_servod and len(machines) == 1:
518                    _start_servod(machines[0])
519                if use_ssp:
520                    try:
521                        _run_with_ssp(job, container_name, job_or_task_id,
522                                      results, parser, ssp_url)
523                    finally:
524                        # Update the ownership of files in result folder.
525                        correct_results_folder_permission(results)
526                else:
527                    job.run(install_before, install_after,
528                            verify_job_repo_url=verify_job_repo_url,
529                            only_collect_crashinfo=collect_crashinfo,
530                            skip_crash_collection=skip_crash_collection,
531                            job_labels=job_labels,
532                            use_packaging=(not no_use_packaging))
533        finally:
534            while job.hosts:
535                host = job.hosts.pop()
536                host.close()
537    except:
538        exit_code = 1
539        traceback.print_exc()
540
541    if pid_file_manager:
542        pid_file_manager.num_tests_failed = job.num_tests_failed
543        pid_file_manager.close_file(exit_code)
544    job.cleanup_parser()
545
546    sys.exit(exit_code)
547
548
549def record_autoserv(options, duration_secs):
550    """Record autoserv end-to-end time in metadata db.
551
552    @param options: parser options.
553    @param duration_secs: How long autoserv has taken, in secs.
554    """
555    # Get machine hostname
556    machines = options.machines.replace(
557            ',', ' ').strip().split() if options.machines else []
558    num_machines = len(machines)
559    if num_machines > 1:
560        # Skip the case where atomic group is used.
561        return
562    elif num_machines == 0:
563        machines.append('hostless')
564
565    # Determine the status that will be reported.
566    s = job_overhead.STATUS
567    task_mapping = {
568            'reset': s.RESETTING, 'verify': s.VERIFYING,
569            'provision': s.PROVISIONING, 'repair': s.REPAIRING,
570            'cleanup': s.CLEANING, 'collect_crashinfo': s.GATHERING}
571    match = filter(lambda task: getattr(options, task, False) == True,
572                   task_mapping)
573    status = task_mapping[match[0]] if match else s.RUNNING
574    is_special_task = status not in [s.RUNNING, s.GATHERING]
575    job_or_task_id = job_directories.get_job_id_or_task_id(options.results)
576    job_overhead.record_state_duration(
577            job_or_task_id, machines[0], status, duration_secs,
578            is_special_task=is_special_task)
579
580
581def main():
582    start_time = datetime.datetime.now()
583    # White list of tests with run time measurement enabled.
584    measure_run_time_tests_names = _CONFIG.get_config_value(
585            'AUTOSERV', 'measure_run_time_tests', type=str)
586    if measure_run_time_tests_names:
587        measure_run_time_tests = [t.strip() for t in
588                                  measure_run_time_tests_names.split(',')]
589    else:
590        measure_run_time_tests = []
591    # grab the parser
592    parser = autoserv_parser.autoserv_parser
593    parser.parse_args()
594
595    if len(sys.argv) == 1:
596        parser.parser.print_help()
597        sys.exit(1)
598
599    # If the job requires to run with server-side package, try to stage server-
600    # side package first. If that fails with error that autotest server package
601    # does not exist, fall back to run the job without using server-side
602    # packaging. If option warn_no_ssp is specified, that means autoserv is
603    # running in a drone does not support SSP, thus no need to stage server-side
604    # package.
605    ssp_url = None
606    ssp_url_warning = False
607    if (not parser.options.warn_no_ssp and parser.options.require_ssp):
608        ssp_url = _stage_ssp(parser)
609        # The build does not have autotest server package. Fall back to not
610        # to use server-side package. Logging is postponed until logging being
611        # set up.
612        ssp_url_warning = not ssp_url
613
614    if parser.options.no_logging:
615        results = None
616    else:
617        results = parser.options.results
618        if not results:
619            results = 'results.' + time.strftime('%Y-%m-%d-%H.%M.%S')
620        results  = os.path.abspath(results)
621        resultdir_exists = False
622        for filename in ('control.srv', 'status.log', '.autoserv_execute'):
623            if os.path.exists(os.path.join(results, filename)):
624                resultdir_exists = True
625        if not parser.options.use_existing_results and resultdir_exists:
626            error = "Error: results directory already exists: %s\n" % results
627            sys.stderr.write(error)
628            sys.exit(1)
629
630        # Now that we certified that there's no leftover results dir from
631        # previous jobs, lets create the result dir since the logging system
632        # needs to create the log file in there.
633        if not os.path.isdir(results):
634            os.makedirs(results)
635
636    # Server-side packaging will only be used if it's required and the package
637    # is available. If warn_no_ssp is specified, it means that autoserv is
638    # running in a drone does not have SSP supported and a warning will be logs.
639    # Therefore, it should not run with SSP.
640    use_ssp = (not parser.options.warn_no_ssp and parser.options.require_ssp
641               and ssp_url)
642    if use_ssp:
643        log_dir = os.path.join(results, 'ssp_logs') if results else None
644        if log_dir and not os.path.exists(log_dir):
645            os.makedirs(log_dir)
646    else:
647        log_dir = results
648
649    logging_manager.configure_logging(
650            server_logging_config.ServerLoggingConfig(),
651            results_dir=log_dir,
652            use_console=not parser.options.no_tee,
653            verbose=parser.options.verbose,
654            no_console_prefix=parser.options.no_console_prefix)
655
656    if ssp_url_warning:
657        logging.warn(
658                'Autoserv is required to run with server-side packaging. '
659                'However, no server-side package can be found based on '
660                '`--image`, host attribute job_repo_url or host label of '
661                'cros-version. The test will be executed without '
662                'server-side packaging supported.')
663
664    if results:
665        logging.info("Results placed in %s" % results)
666
667        # wait until now to perform this check, so it get properly logged
668        if (parser.options.use_existing_results and not resultdir_exists and
669            not utils.is_in_container()):
670            logging.error("No existing results directory found: %s", results)
671            sys.exit(1)
672
673    logging.debug('autoserv is running in drone %s.', socket.gethostname())
674    logging.debug('autoserv command was: %s', ' '.join(sys.argv))
675
676    if parser.options.write_pidfile and results:
677        pid_file_manager = pidfile.PidFileManager(parser.options.pidfile_label,
678                                                  results)
679        pid_file_manager.open_file()
680    else:
681        pid_file_manager = None
682
683    autotest.BaseAutotest.set_install_in_tmpdir(
684        parser.options.install_in_tmpdir)
685
686    timer = None
687    try:
688        # Take the first argument as control file name, get the test name from
689        # the control file. If the test name exists in the list of tests with
690        # run time measurement enabled, start a timer to begin measurement.
691        if (len(parser.args) > 0 and parser.args[0] != '' and
692            parser.options.machines):
693            try:
694                test_name = control_data.parse_control(parser.args[0],
695                                                       raise_warnings=True).name
696            except control_data.ControlVariableException:
697                logging.debug('Failed to retrieve test name from control file.')
698                test_name = None
699            if test_name in measure_run_time_tests:
700                machines = parser.options.machines.replace(',', ' '
701                                                           ).strip().split()
702                try:
703                    afe = frontend.AFE()
704                    board = server_utils.get_board_from_afe(machines[0], afe)
705                    timer = autotest_stats.Timer('autoserv_run_time.%s.%s' %
706                                                 (board, test_name))
707                    timer.start()
708                except (urllib2.HTTPError, urllib2.URLError):
709                    # Ignore error if RPC failed to get board
710                    pass
711    except control_data.ControlVariableException as e:
712        logging.error(str(e))
713    exit_code = 0
714    # TODO(beeps): Extend this to cover different failure modes.
715    # Testing exceptions are matched against labels sent to autoserv. Eg,
716    # to allow only the hostless job to run, specify
717    # testing_exceptions: test_suite in the shadow_config. To allow both
718    # the hostless job and dummy_Pass to run, specify
719    # testing_exceptions: test_suite,dummy_Pass. You can figure out
720    # what label autoserv is invoked with by looking through the logs of a test
721    # for the autoserv command's -l option.
722    testing_exceptions = _CONFIG.get_config_value(
723            'AUTOSERV', 'testing_exceptions', type=list, default=[])
724    test_mode = _CONFIG.get_config_value(
725            'AUTOSERV', 'testing_mode', type=bool, default=False)
726    test_mode = (results_mocker and test_mode and not
727                 any([ex in parser.options.label
728                      for ex in testing_exceptions]))
729    is_task = (parser.options.verify or parser.options.repair or
730               parser.options.provision or parser.options.reset or
731               parser.options.cleanup or parser.options.collect_crashinfo)
732    try:
733        try:
734            if test_mode:
735                # The parser doesn't run on tasks anyway, so we can just return
736                # happy signals without faking results.
737                if not is_task:
738                    machine = parser.options.results.split('/')[-1]
739
740                    # TODO(beeps): The proper way to do this would be to
741                    # refactor job creation so we can invoke job.record
742                    # directly. To do that one needs to pipe the test_name
743                    # through run_autoserv and bail just before invoking
744                    # the server job. See the comment in
745                    # puppylab/results_mocker for more context.
746                    results_mocker.ResultsMocker(
747                            test_name if test_name else 'unknown-test',
748                            parser.options.results, machine
749                            ).mock_results()
750                return
751            else:
752                run_autoserv(pid_file_manager, results, parser, ssp_url,
753                             use_ssp)
754        except SystemExit as e:
755            exit_code = e.code
756            if exit_code:
757                logging.exception(e)
758        except Exception as e:
759            # If we don't know what happened, we'll classify it as
760            # an 'abort' and return 1.
761            logging.exception(e)
762            exit_code = 1
763    finally:
764        if pid_file_manager:
765            pid_file_manager.close_file(exit_code)
766        if timer:
767            timer.stop()
768        # Record the autoserv duration time. Must be called
769        # just before the system exits to ensure accuracy.
770        duration_secs = (datetime.datetime.now() - start_time).total_seconds()
771        record_autoserv(parser.options, duration_secs)
772    sys.exit(exit_code)
773
774
775if __name__ == '__main__':
776    main()
777