2014-05-09 20:16:13 +00:00
|
|
|
#!/usr/bin/python
|
2014-05-08 21:37:57 +00:00
|
|
|
# -*- coding: utf8 -*-
|
2014-05-09 01:27:29 +00:00
|
|
|
# author : Brian Ernst
|
2014-05-09 03:05:55 +00:00
|
|
|
# python_version : 2.7.6 and 3.4.0
|
2014-05-09 01:27:29 +00:00
|
|
|
# =================================
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-09 20:16:13 +00:00
|
|
|
# todo: switch to `p4 fstat ...`, and parse the output for clientFile and cache it.
|
2014-05-08 21:37:57 +00:00
|
|
|
# todo: have a backup feature, make sure files are moved to the recycle bin or a temporary file.
|
|
|
|
# todo: switch to faster method of calling p4 fstat on an entire directory and parsing it's output
|
|
|
|
# todo: add option of using send2trash
|
2014-05-09 03:05:55 +00:00
|
|
|
# todo: buffer output, after exceeding a certain amount print to the output.
|
|
|
|
# todo: allow logging output besides console output, or redirection altogether
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-09 23:36:49 +00:00
|
|
|
import datetime, inspect, multiprocessing, optparse, os, re, stat, subprocess, sys, threading, traceback
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-09 18:15:32 +00:00
|
|
|
# trying ntpath, need to test on linux
|
|
|
|
import ntpath
|
|
|
|
|
2014-05-08 21:37:57 +00:00
|
|
|
|
|
|
|
re_remove_comment = re.compile( "#.*$" )
|
|
|
|
def remove_comment( s ):
|
|
|
|
return re.sub( re_remove_comment, "", s )
|
|
|
|
|
|
|
|
|
|
|
|
try: input = raw_input
|
|
|
|
except: pass
|
|
|
|
|
2014-05-09 03:05:55 +00:00
|
|
|
def enum(*sequential, **named):
|
|
|
|
enums = dict(zip(sequential, range(len(sequential))), **named)
|
|
|
|
return type('Enum', (), enums)
|
|
|
|
|
2014-05-15 01:09:46 +00:00
|
|
|
MSG = enum('SHUTDOWN', 'PARSE_DIRECTORY', 'RUN_FUNCTION')
|
2014-05-09 03:05:55 +00:00
|
|
|
|
|
|
|
p4_ignore = ".p4ignore"
|
|
|
|
|
2014-05-09 04:55:12 +00:00
|
|
|
main_pid = os.getpid( )
|
|
|
|
|
2014-05-09 03:05:55 +00:00
|
|
|
|
2014-05-09 23:19:44 +00:00
|
|
|
#if os.name == 'nt' or sys.platform == 'cygwin'
|
2014-05-09 18:15:32 +00:00
|
|
|
def basename( path ):
|
2014-05-09 23:19:44 +00:00
|
|
|
# TODO: import based on platform
|
|
|
|
# https://docs.python.org/2/library/os.path.html
|
|
|
|
# posixpath for UNIX-style paths
|
|
|
|
# ntpath for Windows paths
|
|
|
|
# macpath for old-style MacOS paths
|
|
|
|
# os2emxpath for OS/2 EMX paths
|
|
|
|
|
2014-05-09 18:15:32 +00:00
|
|
|
#return os.path.basename( path )
|
|
|
|
return ntpath.basename( path )
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-09 23:19:44 +00:00
|
|
|
def normpath( path ):
|
|
|
|
return ntpath.normpath( path )
|
|
|
|
|
|
|
|
def join( patha, pathb ):
|
|
|
|
return ntpath.join( patha, pathb )
|
|
|
|
|
|
|
|
def splitdrive( path ):
|
|
|
|
return ntpath.splitdrive( path )
|
|
|
|
|
2014-05-09 03:05:55 +00:00
|
|
|
def get_ignore_list( path, files_to_ignore ):
|
|
|
|
# have to split path and test top directory
|
|
|
|
dirs = path.split( os.sep )
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-09 03:05:55 +00:00
|
|
|
ignore_list = [ ]
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-09 03:05:55 +00:00
|
|
|
for i, val in enumerate( dirs ):
|
|
|
|
path_to_find = os.sep.join( dirs[ : i + 1] )
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-09 03:05:55 +00:00
|
|
|
if path_to_find in files_to_ignore:
|
|
|
|
ignore_list.extend( files_to_ignore[ path_to_find ] )
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-09 03:05:55 +00:00
|
|
|
return ignore_list
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-09 03:05:55 +00:00
|
|
|
def match_in_ignore_list( path, ignore_list ):
|
|
|
|
for r in ignore_list:
|
|
|
|
if re.match( r, path ):
|
|
|
|
return True
|
|
|
|
return False
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-14 02:18:16 +00:00
|
|
|
def call_process( args ):
|
|
|
|
return subprocess.call( args.split( ), stdout=subprocess.PIPE, stderr=subprocess.PIPE )
|
|
|
|
|
2014-05-10 01:11:23 +00:00
|
|
|
use_bytearray_str_conversion = type( b"str" ) is not str
|
|
|
|
def get_str_from_process_stdout( line ):
|
|
|
|
if use_bytearray_str_conversion:
|
|
|
|
return ''.join( map( chr, line ) )
|
|
|
|
else:
|
|
|
|
return line
|
|
|
|
|
2014-05-14 02:18:16 +00:00
|
|
|
def singular_pulural( val, singular, plural ):
|
|
|
|
return singular if val == 1 else plural
|
|
|
|
|
2014-05-09 23:19:44 +00:00
|
|
|
# Keep these in mind if you have issues:
|
|
|
|
# https://stackoverflow.com/questions/16557908/getting-output-of-a-process-at-runtime
|
|
|
|
# https://stackoverflow.com/questions/4417546/constantly-print-subprocess-output-while-process-is-running
|
|
|
|
def get_client_set( path ):
|
|
|
|
files = set( [ ] )
|
|
|
|
|
|
|
|
make_drive_upper = True if os.name == 'nt' or sys.platform == 'cygwin' else False
|
|
|
|
|
|
|
|
command = "p4 fstat ..."
|
|
|
|
|
2014-05-13 20:08:16 +00:00
|
|
|
proc = subprocess.Popen( command.split( ), stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=path )
|
2014-05-09 23:19:44 +00:00
|
|
|
for line in proc.stdout:
|
2014-05-10 01:11:23 +00:00
|
|
|
line = get_str_from_process_stdout( line )
|
|
|
|
|
2014-05-09 23:19:44 +00:00
|
|
|
clientFile_tag = "... clientFile "
|
|
|
|
if not line.startswith( clientFile_tag ):
|
|
|
|
continue
|
|
|
|
|
|
|
|
local_path = normpath( line[ len( clientFile_tag ) : ].strip( ) )
|
|
|
|
if make_drive_upper:
|
|
|
|
drive, path = splitdrive( local_path )
|
|
|
|
local_path = ''.join( [ drive.upper( ), path ] )
|
|
|
|
|
|
|
|
files.add( local_path )
|
|
|
|
|
2014-05-14 02:18:16 +00:00
|
|
|
# TODO: check error to see if the path is not in the client view. Prompt anyway?
|
|
|
|
|
2014-05-09 23:19:44 +00:00
|
|
|
return files
|
|
|
|
|
2014-05-09 03:05:55 +00:00
|
|
|
class PTable( list ):
|
|
|
|
def __init__( self, *args ):
|
|
|
|
list.__init__( self, args )
|
|
|
|
self.mutex = multiprocessing.Semaphore( )
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-09 03:05:55 +00:00
|
|
|
class PDict( dict ):
|
|
|
|
def __init__( self, *args ):
|
|
|
|
dict.__init__( self, args )
|
|
|
|
self.mutex = multiprocessing.Semaphore( )
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-10 01:11:23 +00:00
|
|
|
# TODO: Create a child thread for triggering autoflush events
|
2014-05-09 04:55:12 +00:00
|
|
|
class Console( threading.Thread ):
|
|
|
|
MSG = enum('WRITE', 'FLUSH', 'SHUTDOWN', 'CLEAR' )
|
|
|
|
|
2014-05-09 23:36:49 +00:00
|
|
|
# auto_flush_time is time in milliseconds since last flush to trigger a flush when writing
|
2014-05-09 23:19:44 +00:00
|
|
|
def __init__( self, auto_flush_num = None, auto_flush_time = None ):
|
2014-05-09 04:55:12 +00:00
|
|
|
threading.Thread.__init__( self )
|
|
|
|
self.buffers = {}
|
2014-05-09 23:36:49 +00:00
|
|
|
self.buffer_write_times = {}
|
2014-05-09 04:55:12 +00:00
|
|
|
self.running = True
|
|
|
|
self.queue = multiprocessing.JoinableQueue( )
|
2014-05-09 23:19:44 +00:00
|
|
|
self.auto_flush_num = auto_flush_num if auto_flush_num is not None else -1
|
2014-05-09 23:36:49 +00:00
|
|
|
self.auto_flush_time = auto_flush_time * 1000 if auto_flush_time is not None else -1
|
2014-05-10 01:11:23 +00:00
|
|
|
self.shutting_down = False
|
2014-05-09 04:55:12 +00:00
|
|
|
|
2014-05-09 23:19:44 +00:00
|
|
|
def write( self, data, pid = None ):
|
|
|
|
self.queue.put( ( Console.MSG.WRITE, pid if pid is not None else os.getpid(), data ) )
|
2014-05-09 04:55:12 +00:00
|
|
|
|
2014-05-09 23:19:44 +00:00
|
|
|
def writeflush( self, data, pid = None ):
|
|
|
|
pid = pid if pid is not None else os.getpid()
|
|
|
|
self.queue.put( ( Console.MSG.WRITE, pid, data ) )
|
|
|
|
self.queue.put( ( Console.MSG.FLUSH, pid ) )
|
2014-05-09 04:55:12 +00:00
|
|
|
|
2014-05-09 23:19:44 +00:00
|
|
|
def flush( self, pid = None ):
|
|
|
|
self.queue.put( ( Console.MSG.FLUSH, pid if pid is not None else os.getpid() ) )
|
|
|
|
|
|
|
|
def clear( self, pid = None ):
|
|
|
|
self.queue.put( ( Console.MSG.CLEAR, pid if pid is not None else os.getpid() ) )
|
2014-05-09 04:55:12 +00:00
|
|
|
|
|
|
|
def __enter__( self ):
|
|
|
|
self.start( )
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __exit__( self, type, value, tb ):
|
2014-05-09 20:16:13 +00:00
|
|
|
self.queue.put( ( Console.MSG.SHUTDOWN, ) )
|
|
|
|
self.queue.join( )
|
2014-05-09 04:55:12 +00:00
|
|
|
|
|
|
|
def run( self ):
|
|
|
|
while True:
|
|
|
|
data = self.queue.get( )
|
|
|
|
event = data[0]
|
|
|
|
|
|
|
|
if event == Console.MSG.SHUTDOWN:
|
|
|
|
# flush remaining buffers before shutting down
|
2014-05-09 23:19:44 +00:00
|
|
|
for ( pid, buffer ) in self.buffers.items( ):
|
2014-05-09 04:55:12 +00:00
|
|
|
for line in buffer:
|
|
|
|
print( line )
|
2014-05-09 20:16:13 +00:00
|
|
|
self.buffers.clear( )
|
2014-05-10 01:11:23 +00:00
|
|
|
self.buffer_write_times.clear( )
|
2014-05-09 20:16:13 +00:00
|
|
|
self.queue.task_done( )
|
2014-05-10 01:11:23 +00:00
|
|
|
|
|
|
|
print(self.queue.qsize())
|
|
|
|
print(self.queue.empty())
|
2014-05-09 04:55:12 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
elif event == Console.MSG.WRITE:
|
|
|
|
pid, s = data[ 1 : ]
|
|
|
|
|
|
|
|
if pid not in self.buffers:
|
|
|
|
self.buffers[ pid ] = []
|
2014-05-09 23:36:49 +00:00
|
|
|
if pid not in self.buffer_write_times:
|
|
|
|
self.buffer_write_times[ pid ] = datetime.datetime.now( )
|
2014-05-09 04:55:12 +00:00
|
|
|
self.buffers[ pid ].append( s )
|
2014-05-09 23:19:44 +00:00
|
|
|
|
2014-05-09 23:36:49 +00:00
|
|
|
if self.auto_flush_num >= 0 and len( self.buffers[ pid ] ) >= self.auto_flush_num:
|
|
|
|
self.flush( pid )
|
|
|
|
elif self.auto_flush_time >= 0 and ( datetime.datetime.now( ) - self.buffer_write_times[ pid ] ).microseconds >= self.auto_flush_time:
|
2014-05-09 23:19:44 +00:00
|
|
|
self.flush( pid )
|
2014-05-10 01:11:23 +00:00
|
|
|
# TODO: if buffer is not empty and we don't auto flush on write, sleep until a time then auto flush according to auto_flush_time
|
2014-05-09 04:55:12 +00:00
|
|
|
elif event == Console.MSG.FLUSH:
|
|
|
|
pid = data[ 1 ]
|
|
|
|
if pid in self.buffers:
|
|
|
|
for line in self.buffers[ pid ]:
|
|
|
|
print( line )
|
2014-05-09 17:12:41 +00:00
|
|
|
self.buffers.pop( pid, None )
|
2014-05-09 23:36:49 +00:00
|
|
|
self.buffer_write_times[ pid ] = datetime.datetime.now( )
|
2014-05-09 04:55:12 +00:00
|
|
|
elif event == Console.MSG.CLEAR:
|
|
|
|
pid = data[ 1 ]
|
|
|
|
if pid in self.buffers:
|
2014-05-09 17:12:41 +00:00
|
|
|
self.buffers.pop( pid, None )
|
2014-05-09 00:43:05 +00:00
|
|
|
|
2014-05-09 20:16:13 +00:00
|
|
|
self.queue.task_done( )
|
|
|
|
|
2014-05-15 01:09:46 +00:00
|
|
|
class Task( threading.Event ):
|
|
|
|
def __init__( data, cmd = None ):
|
|
|
|
threading.Event.__init__( self )
|
|
|
|
|
|
|
|
self.cmd = cmd if cmd is None MSG.RUN_FUNCTION
|
|
|
|
self.data = data
|
|
|
|
|
|
|
|
def isDone( self ):
|
|
|
|
return self.isSet()
|
|
|
|
|
|
|
|
def join( self ):
|
|
|
|
self.wait( )
|
|
|
|
|
2014-05-14 02:45:55 +00:00
|
|
|
class Worker( threading.Thread ):
|
|
|
|
def __init__( self, console, queue, files_to_ignore ):
|
|
|
|
threading.Thread.__init__( self )
|
|
|
|
|
|
|
|
self.console = console
|
|
|
|
self.queue = queue
|
|
|
|
self.files_to_ignore = files_to_ignore
|
|
|
|
|
|
|
|
def run( self ):
|
|
|
|
while True:
|
|
|
|
( cmd, data ) = self.queue.get( )
|
|
|
|
|
|
|
|
if cmd == MSG.SHUTDOWN:
|
|
|
|
self.console.flush( )
|
2014-05-15 01:09:46 +00:00
|
|
|
self.queue.task_done( )
|
|
|
|
break
|
|
|
|
|
|
|
|
if cmd == MSG.RUN_FUNCTION:
|
2014-05-14 02:45:55 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
if cmd != MSG.PARSE_DIRECTORY or data is None:
|
|
|
|
self.console.flush( )
|
|
|
|
self.queue.task_done( )
|
|
|
|
continue
|
|
|
|
|
|
|
|
directory = data
|
|
|
|
|
|
|
|
# add threading stuffs
|
|
|
|
|
|
|
|
self.queue.task_done( )
|
|
|
|
|
2014-05-09 03:05:55 +00:00
|
|
|
def main( args ):
|
|
|
|
# check requirements
|
2014-05-14 02:18:16 +00:00
|
|
|
if call_process( 'p4 -V' ) != 0:
|
2014-05-09 03:05:55 +00:00
|
|
|
print( 'Perforce Command-line Client(p4) is required for this script.' )
|
|
|
|
sys.exit( 1 )
|
2014-05-08 21:37:57 +00:00
|
|
|
|
2014-05-09 20:16:13 +00:00
|
|
|
#http://docs.python.org/library/optparse.html
|
|
|
|
parser = optparse.OptionParser( )
|
2014-05-09 03:05:55 +00:00
|
|
|
|
2014-05-09 04:55:12 +00:00
|
|
|
parser.add_option( "-d", "--dir", dest="directory", help="Desired directory to crawl.", default=None )
|
2014-05-09 17:25:53 +00:00
|
|
|
parser.add_option( "-t", "--threads", dest="thread_count", help="Number of threads to crawl your drive and poll p4.", default=100 )
|
2014-05-14 02:18:16 +00:00
|
|
|
parser.add_option( "-q", "--quiet", action="store_true", dest="quiet", help="This overrides verbose", default=False )
|
2014-05-09 03:05:55 +00:00
|
|
|
parser.add_option( "-v", "--verbose", action="store_true", dest="verbose", default=True )
|
|
|
|
|
|
|
|
( options, args ) = parser.parse_args( )
|
|
|
|
|
2014-05-09 23:19:44 +00:00
|
|
|
directory = normpath( options.directory if options.directory is not None else os.getcwd( ) )
|
2014-05-09 03:05:55 +00:00
|
|
|
|
2014-05-14 02:18:16 +00:00
|
|
|
# Files are added from .p4ignore
|
|
|
|
# Key is the file root, the value is the table of file regexes for that directory.
|
|
|
|
files_to_ignore = PDict()
|
|
|
|
|
|
|
|
remove_file_count = 0
|
|
|
|
remove_dir_count = 0
|
2014-05-13 20:08:16 +00:00
|
|
|
warning_count = 0
|
|
|
|
error_count = 0
|
|
|
|
|
2014-05-09 23:19:44 +00:00
|
|
|
with Console( auto_flush_num=20, auto_flush_time=1000 ) as c:
|
2014-05-13 20:08:16 +00:00
|
|
|
if not options.quiet:
|
2014-05-14 02:33:11 +00:00
|
|
|
c.writeflush( "Caching files in depot, this may take a little while..." )
|
2014-05-14 02:45:55 +00:00
|
|
|
|
|
|
|
# TODO: push this off to a thread and walk the directory so we get a headstart.
|
2014-05-09 23:19:44 +00:00
|
|
|
files_in_depot = get_client_set( directory )
|
2014-05-09 03:05:55 +00:00
|
|
|
|
2014-05-14 02:45:55 +00:00
|
|
|
# TODO: push a os.walk request off to a thread to build a list of files in the directory; create batch based on directory?
|
|
|
|
|
|
|
|
# TODO: at this point join on both tasks to wait until they're done
|
|
|
|
|
|
|
|
# TODO: kick off file removal, make batches from the files for threads to work on since testing has to be done for each.
|
|
|
|
# need to figure out the best way to do this since the ignore list needs to be properly built for each directory;
|
|
|
|
# will at least need to redo how the ignore lists are handled for efficiencies sake.
|
|
|
|
|
2014-05-13 20:08:16 +00:00
|
|
|
if not options.quiet:
|
|
|
|
c.writeflush( "Checking " + directory)
|
2014-05-09 23:19:44 +00:00
|
|
|
for root, dirs, files in os.walk( directory ):
|
2014-05-14 02:18:16 +00:00
|
|
|
ignore_list = get_ignore_list( root, files_to_ignore )
|
2014-05-09 04:55:12 +00:00
|
|
|
|
2014-05-13 20:08:16 +00:00
|
|
|
if not options.quiet:
|
|
|
|
c.write( "|Checking " + root )
|
2014-05-09 04:55:12 +00:00
|
|
|
|
|
|
|
for d in dirs:
|
2014-05-09 23:19:44 +00:00
|
|
|
path = join( root, d )
|
2014-05-09 04:55:12 +00:00
|
|
|
|
|
|
|
if match_in_ignore_list( path, ignore_list ):
|
|
|
|
# add option of using send2trash
|
2014-05-13 20:08:16 +00:00
|
|
|
if not options.quiet:
|
|
|
|
c.write( "| ignoring " + d )
|
2014-05-09 04:55:12 +00:00
|
|
|
dirs.remove( d )
|
2014-05-09 23:19:44 +00:00
|
|
|
|
|
|
|
for f in files:
|
|
|
|
path = normpath( join( root, f ) )
|
|
|
|
|
|
|
|
if path not in files_in_depot:
|
2014-05-13 20:08:16 +00:00
|
|
|
if not options.quiet:
|
|
|
|
c.write( "| " + f + " is unversioned, removing it." )
|
|
|
|
try:
|
|
|
|
os.chmod( path, stat.S_IWRITE )
|
|
|
|
os.remove( path )
|
2014-05-14 02:18:16 +00:00
|
|
|
remove_file_count += 1
|
2014-05-13 20:08:16 +00:00
|
|
|
except OSError as ex:
|
2014-05-14 01:08:53 +00:00
|
|
|
c.writeflush( "| " + type( ex ).__name__ )
|
|
|
|
c.writeflush( "| " + repr( ex ) )
|
|
|
|
c.writeflush( "| ^ERROR^" )
|
2014-05-13 20:08:16 +00:00
|
|
|
|
|
|
|
error_count += 1
|
2014-05-14 02:18:16 +00:00
|
|
|
if not options.quiet:
|
|
|
|
c.write( "|Done." )
|
|
|
|
|
|
|
|
if not options.quiet:
|
|
|
|
c.write( os.linesep + "Removing empty directories...")
|
|
|
|
# remove empty directories in reverse order
|
|
|
|
for root, dirs, files in os.walk( directory, topdown=False ):
|
|
|
|
ignore_list = get_ignore_list( root, files_to_ignore )
|
|
|
|
|
|
|
|
for d in dirs:
|
|
|
|
path = os.path.join( root, d )
|
2014-05-13 20:08:16 +00:00
|
|
|
|
2014-05-14 02:18:16 +00:00
|
|
|
if match_in_ignore_list( path, ignore_list ):
|
|
|
|
# add option of using send2trash
|
|
|
|
if not options.quiet:
|
|
|
|
c.write( "| ignoring " + d )
|
|
|
|
dirs.remove( d )
|
|
|
|
try:
|
|
|
|
os.rmdir(path)
|
|
|
|
remove_dir_count += 1
|
|
|
|
if not options.quiet:
|
|
|
|
c.write( "| " + d + " was removed." )
|
|
|
|
except OSError:
|
|
|
|
# Fails on non-empty directory
|
|
|
|
pass
|
2014-05-13 20:08:16 +00:00
|
|
|
if not options.quiet:
|
|
|
|
c.write( "|Done." )
|
|
|
|
|
2014-05-14 02:18:16 +00:00
|
|
|
if not options.quiet:
|
|
|
|
output = "\nRemoved " + str( remove_file_count ) + singular_pulural( remove_file_count, " file, ", " files, " )
|
|
|
|
output += str( remove_dir_count ) + singular_pulural( remove_dir_count, " directory", " directories")
|
2014-05-13 20:08:16 +00:00
|
|
|
|
|
|
|
if warning_count > 0:
|
2014-05-14 02:18:16 +00:00
|
|
|
output += " w/ " + str( warning_count ) + singular_pulural( warning_count, " warning", " warnings" )
|
2014-05-13 20:08:16 +00:00
|
|
|
if error_count > 0:
|
2014-05-14 02:18:16 +00:00
|
|
|
output += " w/ " + str( error_count ) + singular_pulural( error_count, " error", " errors" )
|
2014-05-13 20:08:16 +00:00
|
|
|
|
|
|
|
c.write( output + "." )
|
2014-05-09 03:05:55 +00:00
|
|
|
|
2014-05-08 21:37:57 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
try:
|
2014-05-09 03:05:55 +00:00
|
|
|
main( sys.argv )
|
2014-05-08 21:37:57 +00:00
|
|
|
except:
|
|
|
|
print( "Unexpected error!" )
|
2014-05-09 17:51:59 +00:00
|
|
|
traceback.print_exc( file = sys.stdout )
|