2 # Copyright 2012 The Swarming Authors. All rights reserved.
3 # Use of this source code is governed under the Apache License, Version 2.0 that
4 # can be found in the LICENSE file.
6 """Reads a .isolated, creates a tree of hardlinks and runs the test.
8 To improve performance, it keeps a local cache. The local cache can safely be
11 Any ${ISOLATED_OUTDIR} on the command line will be replaced by the location of a
12 temporary directory upon execution of the command specified in the .isolated
13 file. All content written to this directory will be uploaded upon termination
14 and the .isolated file describing this directory will be printed to stdout.
31 from third_party.depot_tools import fix_encoding
34 from utils import threading_utils
35 from utils import tools
36 from utils import zip_package
42 # Absolute path to this file (can be None if running from zip on Mac).
43 THIS_FILE_PATH = os.path.abspath(__file__) if __file__ else None
45 # Directory that contains this file (might be inside zip package).
46 BASE_DIR = os.path.dirname(THIS_FILE_PATH) if __file__ else None
48 # Directory that contains currently running script file.
49 if zip_package.get_main_script_path():
50 MAIN_DIR = os.path.dirname(
51 os.path.abspath(zip_package.get_main_script_path()))
53 # This happens when 'import run_isolated' is executed at the python
54 # interactive prompt, in that case __file__ is undefined.
57 # Types of action accepted by link_file().
58 HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY = range(1, 5)
60 # The name of the log file to use.
61 RUN_ISOLATED_LOG_FILE = 'run_isolated.log'
63 # The name of the log to use for the run_test_cases.py command
64 RUN_TEST_CASES_LOG = 'run_test_cases.log'
67 # Used by get_flavor().
73 'freebsd7': 'freebsd',
74 'freebsd8': 'freebsd',
78 def get_as_zip_package(executable=True):
79 """Returns ZipPackage with this module and all its dependencies.
81 If |executable| is True will store run_isolated.py as __main__.py so that
82 zip package is directly executable be python.
84 # Building a zip package when running from another zip package is
85 # unsupported and probably unneeded.
86 assert not zip_package.is_zipped_module(sys.modules[__name__])
89 package = zip_package.ZipPackage(root=BASE_DIR)
90 package.add_python_file(THIS_FILE_PATH, '__main__.py' if executable else None)
91 package.add_python_file(os.path.join(BASE_DIR, 'isolateserver.py'))
92 package.add_python_file(os.path.join(BASE_DIR, 'auth.py'))
93 package.add_directory(os.path.join(BASE_DIR, 'third_party'))
94 package.add_directory(os.path.join(BASE_DIR, 'utils'))
99 """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py."""
100 return FLAVOR_MAPPING.get(sys.platform, 'linux')
103 def hardlink(source, link_name):
106 Add support for os.link() on Windows.
108 if sys.platform == 'win32':
109 if not ctypes.windll.kernel32.CreateHardLinkW(
110 unicode(link_name), unicode(source), 0):
113 os.link(source, link_name)
116 def readable_copy(outfile, infile):
117 """Makes a copy of the file that is readable by everyone."""
118 shutil.copy2(infile, outfile)
119 read_enabled_mode = (os.stat(outfile).st_mode | stat.S_IRUSR |
120 stat.S_IRGRP | stat.S_IROTH)
121 os.chmod(outfile, read_enabled_mode)
124 def link_file(outfile, infile, action):
125 """Links a file. The type of link depends on |action|."""
126 logging.debug('Mapping %s to %s' % (infile, outfile))
127 if action not in (HARDLINK, HARDLINK_WITH_FALLBACK, SYMLINK, COPY):
128 raise ValueError('Unknown mapping action %s' % action)
129 if not os.path.isfile(infile):
130 raise isolateserver.MappingError('%s is missing' % infile)
131 if os.path.isfile(outfile):
132 raise isolateserver.MappingError(
133 '%s already exist; insize:%d; outsize:%d' %
134 (outfile, os.stat(infile).st_size, os.stat(outfile).st_size))
137 readable_copy(outfile, infile)
138 elif action == SYMLINK and sys.platform != 'win32':
139 # On windows, symlink are converted to hardlink and fails over to copy.
140 os.symlink(infile, outfile) # pylint: disable=E1101
143 hardlink(infile, outfile)
145 if action == HARDLINK:
146 raise isolateserver.MappingError(
147 'Failed to hardlink %s to %s: %s' % (infile, outfile, e))
148 # Probably a different file system.
150 'Failed to hardlink, failing back to copy %s to %s' % (
152 readable_copy(outfile, infile)
155 def set_read_only(path, read_only):
156 """Sets or resets the write bit on a file or directory.
158 Zaps out access to 'group' and 'others'.
160 assert isinstance(read_only, bool), read_only
161 mode = os.lstat(path).st_mode
162 # TODO(maruel): Stop removing GO bits.
167 if hasattr(os, 'lchmod'):
168 os.lchmod(path, mode) # pylint: disable=E1101
170 if stat.S_ISLNK(mode):
171 # Skip symlink without lchmod() support.
173 'Can\'t change %sw bit on symlink %s',
174 '-' if read_only else '+', path)
177 # TODO(maruel): Implement proper DACL modification on Windows.
181 def make_tree_read_only(root):
182 """Makes all the files in the directories read only.
184 Also makes the directories read only, only if it makes sense on the platform.
186 This means no file can be created or deleted.
188 logging.debug('make_tree_read_only(%s)', root)
189 assert os.path.isabs(root), root
190 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
191 for filename in filenames:
192 set_read_only(os.path.join(dirpath, filename), True)
193 if sys.platform != 'win32':
194 # It must not be done on Windows.
195 for dirname in dirnames:
196 set_read_only(os.path.join(dirpath, dirname), True)
197 if sys.platform != 'win32':
198 set_read_only(root, True)
201 def make_tree_files_read_only(root):
202 """Makes all the files in the directories read only but not the directories
205 This means files can be created or deleted.
207 logging.debug('make_tree_files_read_only(%s)', root)
208 assert os.path.isabs(root), root
209 if sys.platform != 'win32':
210 set_read_only(root, False)
211 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
212 for filename in filenames:
213 set_read_only(os.path.join(dirpath, filename), True)
214 if sys.platform != 'win32':
215 # It must not be done on Windows.
216 for dirname in dirnames:
217 set_read_only(os.path.join(dirpath, dirname), False)
220 def make_tree_writeable(root):
221 """Makes all the files in the directories writeable.
223 Also makes the directories writeable, only if it makes sense on the platform.
225 It is different from make_tree_deleteable() because it unconditionally affects
228 logging.debug('make_tree_writeable(%s)', root)
229 assert os.path.isabs(root), root
230 if sys.platform != 'win32':
231 set_read_only(root, False)
232 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
233 for filename in filenames:
234 set_read_only(os.path.join(dirpath, filename), False)
235 if sys.platform != 'win32':
236 # It must not be done on Windows.
237 for dirname in dirnames:
238 set_read_only(os.path.join(dirpath, dirname), False)
241 def make_tree_deleteable(root):
242 """Changes the appropriate permissions so the files in the directories can be
245 On Windows, the files are modified. On other platforms, modify the directory.
246 It only does the minimum so the files can be deleted safely.
248 Warning on Windows: since file permission is modified, the file node is
249 modified. This means that for hard-linked files, every directory entry for the
250 file node has its file permission modified.
252 logging.debug('make_tree_deleteable(%s)', root)
253 assert os.path.isabs(root), root
254 if sys.platform != 'win32':
255 set_read_only(root, False)
256 for dirpath, dirnames, filenames in os.walk(root, topdown=True):
257 if sys.platform == 'win32':
258 for filename in filenames:
259 set_read_only(os.path.join(dirpath, filename), False)
261 for dirname in dirnames:
262 set_read_only(os.path.join(dirpath, dirname), False)
266 """Wrapper around shutil.rmtree() to retry automatically on Windows."""
267 make_tree_deleteable(root)
268 logging.info('rmtree(%s)', root)
269 if sys.platform == 'win32':
274 except WindowsError: # pylint: disable=E0602
278 print >> sys.stderr, (
279 'Failed to delete %s. Maybe the test has subprocess outliving it.'
280 ' Sleep %d seconds.' % (root, delay))
286 def try_remove(filepath):
287 """Removes a file without crashing even if it doesn't exist."""
289 # TODO(maruel): Not do it unless necessary since it slows this function
291 if sys.platform == 'win32':
292 # Deleting a read-only file will fail if it is read-only.
293 set_read_only(filepath, False)
295 # Deleting a read-only file will fail if the directory is read-only.
296 set_read_only(os.path.dirname(filepath), False)
302 def is_same_filesystem(path1, path2):
303 """Returns True if both paths are on the same filesystem.
305 This is required to enable the use of hardlinks.
307 assert os.path.isabs(path1), path1
308 assert os.path.isabs(path2), path2
309 if sys.platform == 'win32':
310 # If the drive letter mismatches, assume it's a separate partition.
311 # TODO(maruel): It should look at the underlying drive, a drive letter could
312 # be a mount point to a directory on another drive.
313 assert re.match(r'^[a-zA-Z]\:\\.*', path1), path1
314 assert re.match(r'^[a-zA-Z]\:\\.*', path2), path2
315 if path1[0].lower() != path2[0].lower():
317 return os.stat(path1).st_dev == os.stat(path2).st_dev
320 def get_free_space(path):
321 """Returns the number of free bytes."""
322 if sys.platform == 'win32':
323 free_bytes = ctypes.c_ulonglong(0)
324 ctypes.windll.kernel32.GetDiskFreeSpaceExW(
325 ctypes.c_wchar_p(path), None, None, ctypes.pointer(free_bytes))
326 return free_bytes.value
327 # For OSes other than Windows.
328 f = os.statvfs(path) # pylint: disable=E1101
329 return f.f_bfree * f.f_frsize
332 def make_temp_dir(prefix, root_dir):
333 """Returns a temporary directory on the same file system as root_dir."""
335 if root_dir and not is_same_filesystem(root_dir, tempfile.gettempdir()):
336 base_temp_dir = os.path.dirname(root_dir)
337 return tempfile.mkdtemp(prefix=prefix, dir=base_temp_dir)
340 class CachePolicies(object):
341 def __init__(self, max_cache_size, min_free_space, max_items):
344 - max_cache_size: Trim if the cache gets larger than this value. If 0, the
345 cache is effectively a leak.
346 - min_free_space: Trim if disk free space becomes lower than this value. If
347 0, it unconditionally fill the disk.
348 - max_items: Maximum number of items to keep in the cache. If 0, do not
351 self.max_cache_size = max_cache_size
352 self.min_free_space = min_free_space
353 self.max_items = max_items
356 class DiskCache(isolateserver.LocalCache):
357 """Stateful LRU cache in a flat hash table in a directory.
359 Saves its state as json file.
361 STATE_FILE = 'state.json'
363 def __init__(self, cache_dir, policies, algo):
366 cache_dir: directory where to place the cache.
367 policies: cache retention policies.
368 algo: hashing algorithm used.
370 super(DiskCache, self).__init__()
372 self.cache_dir = cache_dir
373 self.policies = policies
374 self.state_file = os.path.join(cache_dir, self.STATE_FILE)
376 # All protected methods (starting with '_') except _path should be called
377 # with this lock locked.
378 self._lock = threading_utils.LockWithAssert()
379 self._lru = lru.LRUDict()
386 with tools.Profiler('Setup'):
393 def __exit__(self, _exc_type, _exec_value, _traceback):
394 with tools.Profiler('CleanupTrimming'):
400 len(self._added), sum(self._added) / 1024)
402 '%5d (%8dkb) current',
404 sum(self._lru.itervalues()) / 1024)
406 '%5d (%8dkb) removed',
407 len(self._removed), sum(self._removed) / 1024)
410 self._free_disk / 1024)
413 def cached_set(self):
415 return self._lru.keys_set()
417 def touch(self, digest, size):
418 """Verifies an actual file is valid.
420 Note that is doesn't compute the hash so it could still be corrupted if the
421 file size didn't change.
423 TODO(maruel): More stringent verification while keeping the check fast.
425 # Do the check outside the lock.
426 if not isolateserver.is_valid_file(self._path(digest), size):
429 # Update it's LRU position.
431 if digest not in self._lru:
433 self._lru.touch(digest)
436 def evict(self, digest):
438 self._lru.pop(digest)
439 self._delete_file(digest, isolateserver.UNKNOWN_FILE_SIZE)
441 def read(self, digest):
442 with open(self._path(digest), 'rb') as f:
445 def write(self, digest, content):
446 path = self._path(digest)
447 # A stale broken file may remain. It is possible for the file to have write
448 # access bit removed which would cause the file_write() call to fail to open
449 # in write mode. Take no chance here.
452 size = isolateserver.file_write(path, content)
454 # There are two possible places were an exception can occur:
455 # 1) Inside |content| generator in case of network or unzipping errors.
456 # 2) Inside file_write itself in case of disk IO errors.
457 # In any case delete an incomplete file and propagate the exception to
458 # caller, it will be logged there.
461 # Make the file read-only in the cache. This has a few side-effects since
462 # the file node is modified, so every directory entries to this file becomes
463 # read-only. It's fine here because it is a new file.
464 set_read_only(path, True)
466 self._add(digest, size)
468 def hardlink(self, digest, dest, file_mode):
469 """Hardlinks the file to |dest|.
471 Note that the file permission bits are on the file node, not the directory
472 entry, so changing the access bit on any of the directory entries for the
473 file node will affect them all.
475 path = self._path(digest)
476 link_file(dest, path, HARDLINK)
477 if file_mode is not None:
478 # Ignores all other bits.
479 os.chmod(dest, file_mode & 0500)
482 """Loads state of the cache from json file."""
483 self._lock.assert_locked()
485 if not os.path.isdir(self.cache_dir):
486 os.makedirs(self.cache_dir)
488 # Make sure the cache is read-only.
489 # TODO(maruel): Calculate the cost and optimize the performance
491 make_tree_read_only(self.cache_dir)
493 # Load state of the cache.
494 if os.path.isfile(self.state_file):
496 self._lru = lru.LRUDict.load(self.state_file)
497 except ValueError as err:
498 logging.error('Failed to load cache state: %s' % (err,))
499 # Don't want to keep broken state file.
500 try_remove(self.state_file)
502 # Ensure that all files listed in the state still exist and add new ones.
503 previous = self._lru.keys_set()
505 for filename in os.listdir(self.cache_dir):
506 if filename == self.STATE_FILE:
508 if filename in previous:
509 previous.remove(filename)
512 if not isolateserver.is_valid_hash(filename, self.algo):
513 logging.warning('Removing unknown file %s from cache', filename)
514 try_remove(self._path(filename))
516 # File that's not referenced in 'state.json'.
517 # TODO(vadimsh): Verify its SHA1 matches file name.
518 logging.warning('Adding unknown file %s to cache', filename)
519 unknown.append(filename)
522 # Add as oldest files. They will be deleted eventually if not accessed.
523 self._add_oldest_list(unknown)
524 logging.warning('Added back %d unknown files', len(unknown))
527 # Filter out entries that were not found.
528 logging.warning('Removed %d lost files', len(previous))
529 for filename in previous:
530 self._lru.pop(filename)
534 """Saves the LRU ordering."""
535 self._lock.assert_locked()
536 if sys.platform != 'win32':
537 d = os.path.dirname(self.state_file)
539 # Necessary otherwise the file can't be created.
540 set_read_only(d, False)
541 if os.path.isfile(self.state_file):
542 set_read_only(self.state_file, False)
543 self._lru.save(self.state_file)
546 """Trims anything we don't know, make sure enough free space exists."""
547 self._lock.assert_locked()
549 # Ensure maximum cache size.
550 if self.policies.max_cache_size:
551 total_size = sum(self._lru.itervalues())
552 while total_size > self.policies.max_cache_size:
553 total_size -= self._remove_lru_file()
555 # Ensure maximum number of items in the cache.
556 if self.policies.max_items and len(self._lru) > self.policies.max_items:
557 for _ in xrange(len(self._lru) - self.policies.max_items):
558 self._remove_lru_file()
560 # Ensure enough free space.
561 self._free_disk = get_free_space(self.cache_dir)
562 trimmed_due_to_space = False
564 self.policies.min_free_space and
566 self._free_disk < self.policies.min_free_space):
567 trimmed_due_to_space = True
568 self._remove_lru_file()
569 self._free_disk = get_free_space(self.cache_dir)
570 if trimmed_due_to_space:
571 total = sum(self._lru.itervalues())
573 'Trimmed due to not enough free disk space: %.1fkb free, %.1fkb '
574 'cache (%.1f%% of its maximum capacity)',
575 self._free_disk / 1024.,
577 100. * self.policies.max_cache_size / float(total),
581 def _path(self, digest):
582 """Returns the path to one item."""
583 return os.path.join(self.cache_dir, digest)
585 def _remove_lru_file(self):
586 """Removes the last recently used file and returns its size."""
587 self._lock.assert_locked()
588 digest, size = self._lru.pop_oldest()
589 self._delete_file(digest, size)
592 def _add(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
593 """Adds an item into LRU cache marking it as a newest one."""
594 self._lock.assert_locked()
595 if size == isolateserver.UNKNOWN_FILE_SIZE:
596 size = os.stat(self._path(digest)).st_size
597 self._added.append(size)
598 self._lru.add(digest, size)
600 def _add_oldest_list(self, digests):
601 """Adds a bunch of items into LRU cache marking them as oldest ones."""
602 self._lock.assert_locked()
604 for digest in digests:
605 size = os.stat(self._path(digest)).st_size
606 self._added.append(size)
607 pairs.append((digest, size))
608 self._lru.batch_insert_oldest(pairs)
610 def _delete_file(self, digest, size=isolateserver.UNKNOWN_FILE_SIZE):
611 """Deletes cache file from the file system."""
612 self._lock.assert_locked()
614 if size == isolateserver.UNKNOWN_FILE_SIZE:
615 size = os.stat(self._path(digest)).st_size
616 try_remove(self._path(digest))
617 self._removed.append(size)
619 logging.error('Error attempting to delete a file %s:\n%s' % (digest, e))
622 def change_tree_read_only(rootdir, read_only):
623 """Changes the tree read-only bits according to the read_only specification.
625 The flag can be 0, 1 or 2, which will affect the possibility to modify files
626 and create or delete files.
629 # Files and directories (except on Windows) are marked read only. This
630 # inhibits modifying, creating or deleting files in the test directory,
631 # except on Windows where creating and deleting files is still possible.
632 make_tree_read_only(rootdir)
634 # Files are marked read only but not the directories. This inhibits
635 # modifying files but creating or deleting files is still possible.
636 make_tree_files_read_only(rootdir)
637 elif read_only in (0, None):
638 # Anything can be modified. This is the default in the .isolated file
641 # TODO(maruel): This is currently dangerous as long as DiskCache.touch()
642 # is not yet changed to verify the hash of the content of the files it is
643 # looking at, so that if a test modifies an input file, the file must be
645 make_tree_writeable(rootdir)
648 'change_tree_read_only(%s, %s): Unknown flag %s' %
649 (rootdir, read_only, read_only))
652 def process_command(command, out_dir):
653 """Replaces isolated specific variables in a command line."""
654 return [c.replace('${ISOLATED_OUTDIR}', out_dir) for c in command]
657 def run_tha_test(isolated_hash, storage, cache, algo, extra_args):
658 """Downloads the dependencies in the cache, hardlinks them into a temporary
659 directory and runs the executable from there.
661 A temporary directory is created to hold the output files. The content inside
662 this directory will be uploaded back to |storage| packaged as a .isolated
666 isolated_hash: the sha-1 of the .isolated file that must be retrieved to
667 recreate the tree of files to run the target executable.
668 storage: an isolateserver.Storage object to retrieve remote objects. This
669 object has a reference to an isolateserver.StorageApi, which does
671 cache: an isolateserver.LocalCache to keep from retrieving the same objects
672 constantly by caching the objects retrieved. Can be on-disk or
674 algo: an hashlib class to hash content. Usually hashlib.sha1.
675 extra_args: optional arguments to add to the command stated in the .isolate
678 run_dir = make_temp_dir('run_tha_test', cache.cache_dir)
679 out_dir = unicode(tempfile.mkdtemp(prefix='run_tha_test'))
683 settings = isolateserver.fetch_isolated(
684 isolated_hash=isolated_hash,
689 os_flavor=get_flavor(),
690 require_command=True)
691 except isolateserver.ConfigError as e:
692 tools.report_error(e)
696 change_tree_read_only(run_dir, settings.read_only)
697 cwd = os.path.normpath(os.path.join(run_dir, settings.relative_cwd))
698 command = settings.command + extra_args
700 # subprocess.call doesn't consider 'cwd' when searching for executable.
701 # Yet isolate can specify command relative to 'cwd'. Convert it to absolute
703 if not os.path.isabs(command[0]):
704 command[0] = os.path.abspath(os.path.join(cwd, command[0]))
705 command = process_command(command, out_dir)
706 logging.info('Running %s, cwd=%s' % (command, cwd))
708 # TODO(csharp): This should be specified somewhere else.
709 # TODO(vadimsh): Pass it via 'env_vars' in manifest.
710 # Add a rotating log file if one doesn't already exist.
711 env = os.environ.copy()
713 env.setdefault('RUN_TEST_CASES_LOG_FILE',
714 os.path.join(MAIN_DIR, RUN_TEST_CASES_LOG))
716 with tools.Profiler('RunTest'):
717 result = subprocess.call(command, cwd=cwd, env=env)
719 tools.report_error('Failed to run %s; cwd=%s: %s' % (command, cwd, e))
722 # Upload out_dir and generate a .isolated file out of this directory. It is
723 # only done if files were written in the directory.
724 if os.listdir(out_dir):
725 with tools.Profiler('ArchiveOutput'):
726 results = isolateserver.archive_files_to_storage(
727 storage, algo, [out_dir], None)
728 # TODO(maruel): Implement side-channel to publish this information.
729 print('run_isolated output: %s' % results[0][0])
738 logging.warning('Leaking %s', run_dir)
739 # Swallow the exception so it doesn't generate an infrastructure error.
741 # It usually happens on Windows when a child process is not properly
742 # terminated, usually because of a test case starting child processes
743 # that time out. This causes files to be locked and it becomes
744 # impossible to delete them.
746 # Only report an infrastructure error if the test didn't fail. This is
747 # because a swarming bot will likely not reboot. This situation will
748 # cause accumulation of temporary hardlink trees.
755 tools.disable_buffering()
756 parser = tools.OptionParserWithLogging(
757 usage='%prog <options>',
759 log_file=RUN_ISOLATED_LOG_FILE)
761 data_group = optparse.OptionGroup(parser, 'Data source')
762 data_group.add_option(
765 help='File/url describing what to map or run')
766 data_group.add_option(
768 help='Hash of the .isolated to grab from the hash table')
769 isolateserver.add_isolate_server_options(data_group, True)
770 parser.add_option_group(data_group)
772 cache_group = optparse.OptionGroup(parser, 'Cache management')
773 cache_group.add_option(
777 help='Cache directory, default=%default')
778 cache_group.add_option(
782 default=20*1024*1024*1024,
783 help='Trim if the cache gets larger than this value, default=%default')
784 cache_group.add_option(
788 default=2*1024*1024*1024,
789 help='Trim if disk free space becomes lower than this value, '
791 cache_group.add_option(
796 help='Trim if more than this number of items are in the cache '
798 parser.add_option_group(cache_group)
800 auth.add_auth_options(parser)
801 options, args = parser.parse_args(args)
802 auth.process_auth_options(parser, options)
803 isolateserver.process_isolate_server_options(data_group, options)
805 if bool(options.isolated) == bool(options.hash):
806 logging.debug('One and only one of --isolated or --hash is required.')
807 parser.error('One and only one of --isolated or --hash is required.')
809 options.cache = os.path.abspath(options.cache)
810 policies = CachePolicies(
811 options.max_cache_size, options.min_free_space, options.max_items)
812 algo = isolateserver.get_hash_algo(options.namespace)
815 # |options.cache| may not exist until DiskCache() instance is created.
816 cache = DiskCache(options.cache, policies, algo)
817 remote = options.isolate_server or options.indir
818 with isolateserver.get_storage(remote, options.namespace) as storage:
820 options.isolated or options.hash, storage, cache, algo, args)
821 except Exception as e:
822 # Make sure any exception is logged.
823 tools.report_error(e)
828 if __name__ == '__main__':
829 # Ensure that we are always running with the correct encoding.
830 fix_encoding.fix_encoding()
831 sys.exit(main(sys.argv[1:]))