| #!/usr/bin/env python |
| # Copyright 2013 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # A Python library to read and store procfs (/proc) information on Linux. |
| # |
| # Each information storage class in this file stores original data as original |
| # as reasonablly possible. Translation is done when requested. It is to make it |
| # always possible to probe the original data. |
| |
| from __future__ import print_function |
| |
| import collections |
| import logging |
| import os |
| import re |
| import struct |
| import sys |
| |
| |
| class _NullHandler(logging.Handler): |
| def emit(self, record): |
| pass |
| |
| |
| _LOGGER = logging.getLogger('procfs') |
| _LOGGER.addHandler(_NullHandler()) |
| |
| |
| class ProcStat: |
| """Reads and stores information in /proc/pid/stat.""" |
| _PATTERN = re.compile(r'^' |
| '(?P<PID>-?[0-9]+) ' |
| '\((?P<COMM>.+)\) ' |
| '(?P<STATE>[RSDZTW]) ' |
| '(?P<PPID>-?[0-9]+) ' |
| '(?P<PGRP>-?[0-9]+) ' |
| '(?P<SESSION>-?[0-9]+) ' |
| '(?P<TTY_NR>-?[0-9]+) ' |
| '(?P<TPGID>-?[0-9]+) ' |
| '(?P<FLAGS>[0-9]+) ' |
| '(?P<MINFIT>[0-9]+) ' |
| '(?P<CMINFIT>[0-9]+) ' |
| '(?P<MAJFIT>[0-9]+) ' |
| '(?P<CMAJFIT>[0-9]+) ' |
| '(?P<UTIME>[0-9]+) ' |
| '(?P<STIME>[0-9]+) ' |
| '(?P<CUTIME>[0-9]+) ' |
| '(?P<CSTIME>[0-9]+) ' |
| '(?P<PRIORITY>[0-9]+) ' |
| '(?P<NICE>[0-9]+) ' |
| '(?P<NUM_THREADS>[0-9]+) ' |
| '(?P<ITREALVALUE>[0-9]+) ' |
| '(?P<STARTTIME>[0-9]+) ' |
| '(?P<VSIZE>[0-9]+) ' |
| '(?P<RSS>[0-9]+) ' |
| '(?P<RSSLIM>[0-9]+) ' |
| '(?P<STARTCODE>[0-9]+) ' |
| '(?P<ENDCODE>[0-9]+) ' |
| '(?P<STARTSTACK>[0-9]+) ' |
| '(?P<KSTKESP>[0-9]+) ' |
| '(?P<KSTKEIP>[0-9]+) ' |
| '(?P<SIGNAL>[0-9]+) ' |
| '(?P<BLOCKED>[0-9]+) ' |
| '(?P<SIGIGNORE>[0-9]+) ' |
| '(?P<SIGCATCH>[0-9]+) ' |
| '(?P<WCHAN>[0-9]+) ' |
| '(?P<NSWAP>[0-9]+) ' |
| '(?P<CNSWAP>[0-9]+) ' |
| '(?P<EXIT_SIGNAL>[0-9]+) ' |
| '(?P<PROCESSOR>[0-9]+) ' |
| '(?P<RT_PRIORITY>[0-9]+) ' |
| '(?P<POLICY>[0-9]+) ' |
| '(?P<DELAYACCT_BLKIO_TICKS>[0-9]+) ' |
| '(?P<GUEST_TIME>[0-9]+) ' |
| '(?P<CGUEST_TIME>[0-9]+)', re.IGNORECASE) |
| |
| def __init__(self, raw, pid, vsize, rss): |
| self._raw = raw |
| self._pid = pid |
| self._vsize = vsize |
| self._rss = rss |
| |
| @staticmethod |
| def load_file(stat_f): |
| raw = stat_f.readlines() |
| stat = ProcStat._PATTERN.match(raw[0]) |
| return ProcStat(raw, |
| stat.groupdict().get('PID'), |
| stat.groupdict().get('VSIZE'), |
| stat.groupdict().get('RSS')) |
| |
| @staticmethod |
| def load(pid): |
| try: |
| with open(os.path.join('/proc', str(pid), 'stat'), 'r') as stat_f: |
| return ProcStat.load_file(stat_f) |
| except IOError: |
| return None |
| |
| @property |
| def raw(self): |
| return self._raw |
| |
| @property |
| def pid(self): |
| return int(self._pid) |
| |
| @property |
| def vsize(self): |
| return int(self._vsize) |
| |
| @property |
| def rss(self): |
| return int(self._rss) |
| |
| |
| class ProcStatm: |
| """Reads and stores information in /proc/pid/statm.""" |
| _PATTERN = re.compile(r'^' |
| '(?P<SIZE>[0-9]+) ' |
| '(?P<RESIDENT>[0-9]+) ' |
| '(?P<SHARE>[0-9]+) ' |
| '(?P<TEXT>[0-9]+) ' |
| '(?P<LIB>[0-9]+) ' |
| '(?P<DATA>[0-9]+) ' |
| '(?P<DT>[0-9]+)', re.IGNORECASE) |
| |
| def __init__(self, raw, size, resident, share, text, lib, data, dt): |
| self._raw = raw |
| self._size = size |
| self._resident = resident |
| self._share = share |
| self._text = text |
| self._lib = lib |
| self._data = data |
| self._dt = dt |
| |
| @staticmethod |
| def load_file(statm_f): |
| try: |
| raw = statm_f.readlines() |
| except (IOError, OSError): |
| return None |
| statm = ProcStatm._PATTERN.match(raw[0]) |
| return ProcStatm(raw, |
| statm.groupdict().get('SIZE'), |
| statm.groupdict().get('RESIDENT'), |
| statm.groupdict().get('SHARE'), |
| statm.groupdict().get('TEXT'), |
| statm.groupdict().get('LIB'), |
| statm.groupdict().get('DATA'), |
| statm.groupdict().get('DT')) |
| |
| @staticmethod |
| def load(pid): |
| try: |
| with open(os.path.join('/proc', str(pid), 'statm'), 'r') as statm_f: |
| return ProcStatm.load_file(statm_f) |
| except (IOError, OSError): |
| return None |
| |
| @property |
| def raw(self): |
| return self._raw |
| |
| @property |
| def size(self): |
| return int(self._size) |
| |
| @property |
| def resident(self): |
| return int(self._resident) |
| |
| @property |
| def share(self): |
| return int(self._share) |
| |
| @property |
| def text(self): |
| return int(self._text) |
| |
| @property |
| def lib(self): |
| return int(self._lib) |
| |
| @property |
| def data(self): |
| return int(self._data) |
| |
| @property |
| def dt(self): |
| return int(self._dt) |
| |
| |
| class ProcStatus: |
| """Reads and stores information in /proc/pid/status.""" |
| _PATTERN = re.compile(r'^(?P<NAME>[A-Za-z0-9_]+):\s+(?P<VALUE>.*)') |
| |
| def __init__(self, raw, dct): |
| self._raw = raw |
| self._pid = dct.get('Pid') |
| self._name = dct.get('Name') |
| self._vm_peak = dct.get('VmPeak') |
| self._vm_size = dct.get('VmSize') |
| self._vm_lck = dct.get('VmLck') |
| self._vm_pin = dct.get('VmPin') |
| self._vm_hwm = dct.get('VmHWM') |
| self._vm_rss = dct.get('VmRSS') |
| self._vm_data = dct.get('VmData') |
| self._vm_stack = dct.get('VmStk') |
| self._vm_exe = dct.get('VmExe') |
| self._vm_lib = dct.get('VmLib') |
| self._vm_pte = dct.get('VmPTE') |
| self._vm_swap = dct.get('VmSwap') |
| |
| @staticmethod |
| def load_file(status_f): |
| raw = status_f.readlines() |
| dct = {} |
| for line in raw: |
| status_match = ProcStatus._PATTERN.match(line) |
| if status_match: |
| match_dict = status_match.groupdict() |
| dct[match_dict['NAME']] = match_dict['VALUE'] |
| else: |
| raise SyntaxError('Unknown /proc/pid/status format.') |
| return ProcStatus(raw, dct) |
| |
| @staticmethod |
| def load(pid): |
| with open(os.path.join('/proc', str(pid), 'status'), 'r') as status_f: |
| return ProcStatus.load_file(status_f) |
| |
| @property |
| def raw(self): |
| return self._raw |
| |
| @property |
| def pid(self): |
| return int(self._pid) |
| |
| @property |
| def vm_peak(self): |
| """Returns a high-water (peak) virtual memory size in kilo-bytes.""" |
| if self._vm_peak.endswith('kB'): |
| return int(self._vm_peak.split()[0]) |
| raise ValueError('VmPeak is not in kB.') |
| |
| @property |
| def vm_size(self): |
| """Returns a virtual memory size in kilo-bytes.""" |
| if self._vm_size.endswith('kB'): |
| return int(self._vm_size.split()[0]) |
| raise ValueError('VmSize is not in kB.') |
| |
| @property |
| def vm_hwm(self): |
| """Returns a high-water (peak) resident set size (RSS) in kilo-bytes.""" |
| if self._vm_hwm.endswith('kB'): |
| return int(self._vm_hwm.split()[0]) |
| raise ValueError('VmHWM is not in kB.') |
| |
| @property |
| def vm_rss(self): |
| """Returns a resident set size (RSS) in kilo-bytes.""" |
| if self._vm_rss.endswith('kB'): |
| return int(self._vm_rss.split()[0]) |
| raise ValueError('VmRSS is not in kB.') |
| |
| |
| class ProcMapsEntry: |
| """A class representing one line in /proc/pid/maps.""" |
| |
| def __init__( |
| self, begin, end, readable, writable, executable, private, offset, |
| major, minor, inode, name): |
| self.begin = begin |
| self.end = end |
| self.readable = readable |
| self.writable = writable |
| self.executable = executable |
| self.private = private |
| self.offset = offset |
| self.major = major |
| self.minor = minor |
| self.inode = inode |
| self.name = name |
| |
| def as_dict(self): |
| return { |
| 'begin': self.begin, |
| 'end': self.end, |
| 'readable': self.readable, |
| 'writable': self.writable, |
| 'executable': self.executable, |
| 'private': self.private, |
| 'offset': self.offset, |
| 'major': self.major, |
| 'minor': self.minor, |
| 'inode': self.inode, |
| 'name': self.name, |
| } |
| |
| |
| class ProcMaps: |
| """Reads and stores information in /proc/pid/maps.""" |
| |
| MAPS_PATTERN = re.compile( |
| r'^([a-f0-9]+)-([a-f0-9]+)\s+(.)(.)(.)(.)\s+([a-f0-9]+)\s+(\S+):(\S+)\s+' |
| r'(\d+)\s*(.*)$', re.IGNORECASE) |
| |
| EXECUTABLE_PATTERN = re.compile( |
| r'\S+\.(so|dll|dylib|bundle)((\.\d+)+\w*(\.\d+){0,3})?') |
| |
| def __init__(self): |
| self._sorted_indexes = [] |
| self._dictionary = {} |
| self._sorted = True |
| |
| def iter(self, condition): |
| if not self._sorted: |
| self._sorted_indexes.sort() |
| self._sorted = True |
| for index in self._sorted_indexes: |
| if not condition or condition(self._dictionary[index]): |
| yield self._dictionary[index] |
| |
| def __iter__(self): |
| if not self._sorted: |
| self._sorted_indexes.sort() |
| self._sorted = True |
| for index in self._sorted_indexes: |
| yield self._dictionary[index] |
| |
| @staticmethod |
| def load_file(maps_f): |
| table = ProcMaps() |
| for line in maps_f: |
| table.append_line(line) |
| return table |
| |
| @staticmethod |
| def load(pid): |
| try: |
| with open(os.path.join('/proc', str(pid), 'maps'), 'r') as maps_f: |
| return ProcMaps.load_file(maps_f) |
| except (IOError, OSError): |
| return None |
| |
| def append_line(self, line): |
| entry = self.parse_line(line) |
| if entry: |
| self._append_entry(entry) |
| return entry |
| |
| @staticmethod |
| def parse_line(line): |
| matched = ProcMaps.MAPS_PATTERN.match(line) |
| if matched: |
| return ProcMapsEntry( # pylint: disable=W0212 |
| int(matched.group(1), 16), # begin |
| int(matched.group(2), 16), # end |
| matched.group(3), # readable |
| matched.group(4), # writable |
| matched.group(5), # executable |
| matched.group(6), # private |
| int(matched.group(7), 16), # offset |
| matched.group(8), # major |
| matched.group(9), # minor |
| int(matched.group(10), 10), # inode |
| matched.group(11) # name |
| ) |
| return None |
| |
| @staticmethod |
| def constants(entry): |
| return entry.writable == '-' and entry.executable == '-' |
| |
| @staticmethod |
| def executable(entry): |
| return entry.executable == 'x' |
| |
| @staticmethod |
| def executable_and_constants(entry): |
| return ((entry.writable == '-' and entry.executable == '-') or |
| entry.executable == 'x') |
| |
| def _append_entry(self, entry): |
| if self._sorted_indexes and self._sorted_indexes[-1] > entry.begin: |
| self._sorted = False |
| self._sorted_indexes.append(entry.begin) |
| self._dictionary[entry.begin] = entry |
| |
| |
| class ProcSmaps: |
| """Reads and stores information in /proc/pid/smaps.""" |
| _SMAPS_PATTERN = re.compile(r'^(?P<NAME>[A-Za-z0-9_]+):\s+(?P<VALUE>.*)') |
| |
| class VMA: |
| def __init__(self): |
| self._size = 0 |
| self._rss = 0 |
| self._pss = 0 |
| |
| def append(self, name, value): |
| dct = { |
| 'Size': '_size', |
| 'Rss': '_rss', |
| 'Pss': '_pss', |
| 'Referenced': '_referenced', |
| 'Private_Clean': '_private_clean', |
| 'Shared_Clean': '_shared_clean', |
| 'KernelPageSize': '_kernel_page_size', |
| 'MMUPageSize': '_mmu_page_size', |
| } |
| if name in dct: |
| self.__setattr__(dct[name], value) |
| |
| def as_int_without_kb(arg): |
| """Returns `arg` as an int, with its "kB" suffix removed if present.""" |
| # The redundant use of `str(arg)` here (when `arg` is already known to be |
| # a string per `isinstance()`) is a workaround for a PyLint bug: |
| # https://github.com/PyCQA/pylint/issues/1162. |
| if isinstance(arg, str) and str(arg).endswith('kB'): |
| return int(str(arg).split()[0]) |
| return int(arg) |
| |
| @property |
| def size(self): |
| return self.as_int_without_kb(self._size) |
| |
| @property |
| def rss(self): |
| return self.as_int_without_kb(self._rss) |
| |
| @property |
| def pss(self): |
| return self.as_int_without_kb(self._pss) |
| |
| def __init__(self, raw, total_dct, maps, vma_internals): |
| self._raw = raw |
| self._size = total_dct['Size'] |
| self._rss = total_dct['Rss'] |
| self._pss = total_dct['Pss'] |
| self._referenced = total_dct['Referenced'] |
| self._shared_clean = total_dct['Shared_Clean'] |
| self._private_clean = total_dct['Private_Clean'] |
| self._kernel_page_size = total_dct['KernelPageSize'] |
| self._mmu_page_size = total_dct['MMUPageSize'] |
| self._maps = maps |
| self._vma_internals = vma_internals |
| |
| @staticmethod |
| def load(pid): |
| with open(os.path.join('/proc', str(pid), 'smaps'), 'r') as smaps_f: |
| raw = smaps_f.readlines() |
| |
| vma = None |
| vma_internals = collections.OrderedDict() |
| total_dct = collections.defaultdict(int) |
| maps = ProcMaps() |
| for line in raw: |
| maps_match = ProcMaps.MAPS_PATTERN.match(line) |
| if maps_match: |
| vma = maps.append_line(line.strip()) |
| vma_internals[vma] = ProcSmaps.VMA() |
| else: |
| smaps_match = ProcSmaps._SMAPS_PATTERN.match(line) |
| if smaps_match: |
| match_dict = smaps_match.groupdict() |
| vma_internals[vma].append(match_dict['NAME'], match_dict['VALUE']) |
| total_dct[match_dict['NAME']] += int(match_dict['VALUE'].split()[0]) |
| |
| return ProcSmaps(raw, total_dct, maps, vma_internals) |
| |
| @property |
| def size(self): |
| return self._size |
| |
| @property |
| def rss(self): |
| return self._rss |
| |
| @property |
| def referenced(self): |
| return self._referenced |
| |
| @property |
| def pss(self): |
| return self._pss |
| |
| @property |
| def private_clean(self): |
| return self._private_clean |
| |
| @property |
| def shared_clean(self): |
| return self._shared_clean |
| |
| @property |
| def kernel_page_size(self): |
| return self._kernel_page_size |
| |
| @property |
| def mmu_page_size(self): |
| return self._mmu_page_size |
| |
| @property |
| def vma_internals(self): |
| return self._vma_internals |
| |
| |
| class ProcPagemap: |
| """Reads and stores partial information in /proc/pid/pagemap. |
| |
| It picks up virtual addresses to read based on ProcMaps (/proc/pid/maps). |
| See https://www.kernel.org/doc/Documentation/vm/pagemap.txt for details. |
| """ |
| _BYTES_PER_PAGEMAP_VALUE = 8 |
| _BYTES_PER_OS_PAGE = 4096 |
| _VIRTUAL_TO_PAGEMAP_OFFSET = _BYTES_PER_OS_PAGE / _BYTES_PER_PAGEMAP_VALUE |
| |
| _MASK_PRESENT = 1 << 63 |
| _MASK_SWAPPED = 1 << 62 |
| _MASK_FILEPAGE_OR_SHAREDANON = 1 << 61 |
| _MASK_SOFTDIRTY = 1 << 55 |
| _MASK_PFN = (1 << 55) - 1 |
| |
| class VMA: |
| def __init__(self, vsize, present, swapped, pageframes): |
| self._vsize = vsize |
| self._present = present |
| self._swapped = swapped |
| self._pageframes = pageframes |
| |
| @property |
| def vsize(self): |
| return int(self._vsize) |
| |
| @property |
| def present(self): |
| return int(self._present) |
| |
| @property |
| def swapped(self): |
| return int(self._swapped) |
| |
| @property |
| def pageframes(self): |
| return self._pageframes |
| |
| def __init__(self, vsize, present, swapped, vma_internals, in_process_dup): |
| self._vsize = vsize |
| self._present = present |
| self._swapped = swapped |
| self._vma_internals = vma_internals |
| self._in_process_dup = in_process_dup |
| |
| @staticmethod |
| def load(pid, maps): |
| total_present = 0 |
| total_swapped = 0 |
| total_vsize = 0 |
| in_process_dup = 0 |
| vma_internals = collections.OrderedDict() |
| process_pageframe_set = set() |
| |
| try: |
| pagemap_fd = os.open( |
| os.path.join('/proc', str(pid), 'pagemap'), os.O_RDONLY) |
| except (IOError, OSError): |
| return None |
| for vma in maps: |
| present = 0 |
| swapped = 0 |
| vsize = 0 |
| pageframes = collections.defaultdict(int) |
| begin_offset = ProcPagemap._offset(vma.begin) |
| chunk_size = ProcPagemap._offset(vma.end) - begin_offset |
| try: |
| os.lseek(pagemap_fd, begin_offset, os.SEEK_SET) |
| buf = os.read(pagemap_fd, chunk_size) |
| except (IOError, OSError): |
| return None |
| if len(buf) < chunk_size: |
| _LOGGER.warn('Failed to read pagemap at 0x%x in %d.' % (vma.begin, pid)) |
| pagemap_values = struct.unpack( |
| '=%dQ' % (len(buf) / ProcPagemap._BYTES_PER_PAGEMAP_VALUE), buf) |
| for pagemap_value in pagemap_values: |
| vsize += ProcPagemap._BYTES_PER_OS_PAGE |
| if pagemap_value & ProcPagemap._MASK_PRESENT: |
| if (pagemap_value & ProcPagemap._MASK_PFN) in process_pageframe_set: |
| in_process_dup += ProcPagemap._BYTES_PER_OS_PAGE |
| else: |
| process_pageframe_set.add(pagemap_value & ProcPagemap._MASK_PFN) |
| if (pagemap_value & ProcPagemap._MASK_PFN) not in pageframes: |
| present += ProcPagemap._BYTES_PER_OS_PAGE |
| pageframes[pagemap_value & ProcPagemap._MASK_PFN] += 1 |
| if pagemap_value & ProcPagemap._MASK_SWAPPED: |
| swapped += ProcPagemap._BYTES_PER_OS_PAGE |
| vma_internals[vma] = ProcPagemap.VMA(vsize, present, swapped, pageframes) |
| total_present += present |
| total_swapped += swapped |
| total_vsize += vsize |
| try: |
| os.close(pagemap_fd) |
| except OSError: |
| return None |
| |
| return ProcPagemap(total_vsize, total_present, total_swapped, |
| vma_internals, in_process_dup) |
| |
| @staticmethod |
| def _offset(virtual_address): |
| return virtual_address / ProcPagemap._VIRTUAL_TO_PAGEMAP_OFFSET |
| |
| @property |
| def vsize(self): |
| return int(self._vsize) |
| |
| @property |
| def present(self): |
| return int(self._present) |
| |
| @property |
| def swapped(self): |
| return int(self._swapped) |
| |
| @property |
| def vma_internals(self): |
| return self._vma_internals |
| |
| |
| class _ProcessMemory: |
| """Aggregates process memory information from /proc for manual testing.""" |
| def __init__(self, pid): |
| self._pid = pid |
| self._maps = None |
| self._pagemap = None |
| self._stat = None |
| self._status = None |
| self._statm = None |
| self._smaps = [] |
| |
| def _read(self, proc_file): |
| lines = [] |
| with open(os.path.join('/proc', str(self._pid), proc_file), 'r') as proc_f: |
| lines = proc_f.readlines() |
| return lines |
| |
| def read_all(self): |
| self.read_stat() |
| self.read_statm() |
| self.read_status() |
| self.read_smaps() |
| self.read_maps() |
| self.read_pagemap(self._maps) |
| |
| def read_maps(self): |
| self._maps = ProcMaps.load(self._pid) |
| |
| def read_pagemap(self, maps): |
| self._pagemap = ProcPagemap.load(self._pid, maps) |
| |
| def read_smaps(self): |
| self._smaps = ProcSmaps.load(self._pid) |
| |
| def read_stat(self): |
| self._stat = ProcStat.load(self._pid) |
| |
| def read_statm(self): |
| self._statm = ProcStatm.load(self._pid) |
| |
| def read_status(self): |
| self._status = ProcStatus.load(self._pid) |
| |
| @property |
| def pid(self): |
| return self._pid |
| |
| @property |
| def maps(self): |
| return self._maps |
| |
| @property |
| def pagemap(self): |
| return self._pagemap |
| |
| @property |
| def smaps(self): |
| return self._smaps |
| |
| @property |
| def stat(self): |
| return self._stat |
| |
| @property |
| def statm(self): |
| return self._statm |
| |
| @property |
| def status(self): |
| return self._status |
| |
| |
| def main(argv): |
| """The main function for manual testing.""" |
| _LOGGER.setLevel(logging.WARNING) |
| handler = logging.StreamHandler() |
| handler.setLevel(logging.WARNING) |
| handler.setFormatter(logging.Formatter( |
| '%(asctime)s:%(name)s:%(levelname)s:%(message)s')) |
| _LOGGER.addHandler(handler) |
| |
| pids = [] |
| for arg in argv[1:]: |
| try: |
| pid = int(arg) |
| except ValueError as value_error: |
| raise SyntaxError("%s is not an integer." % arg) from value_error |
| else: |
| pids.append(pid) |
| |
| procs = {} |
| for pid in pids: |
| procs[pid] = _ProcessMemory(pid) |
| procs[pid].read_all() |
| |
| print('=== PID: %d ===' % pid) |
| |
| print(' stat: %d' % procs[pid].stat.vsize) |
| print(' statm: %d' % (procs[pid].statm.size * 4096)) |
| print(' status: %d (Peak:%d)' % (procs[pid].status.vm_size * 1024, |
| procs[pid].status.vm_peak * 1024)) |
| print(' smaps: %d' % (procs[pid].smaps.size * 1024)) |
| print('pagemap: %d' % procs[pid].pagemap.vsize) |
| print(' stat: %d' % (procs[pid].stat.rss * 4096)) |
| print(' statm: %d' % (procs[pid].statm.resident * 4096)) |
| print(' status: %d (Peak:%d)' % (procs[pid].status.vm_rss * 1024, |
| procs[pid].status.vm_hwm * 1024)) |
| print(' smaps: %d' % (procs[pid].smaps.rss * 1024)) |
| print('pagemap: %d' % procs[pid].pagemap.present) |
| |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv)) |