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