You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
444 lines
15 KiB
444 lines
15 KiB
#!/usr/bin/python
|
|
# -*- coding: utf8 -*-
|
|
# author : Brian Ernst
|
|
# python_version : 2.7.6 and 3.4.0
|
|
# =================================
|
|
|
|
import datetime, inspect, itertools, marshal, multiprocessing, optparse, os, re, stat, subprocess, sys, threading
|
|
|
|
# trying ntpath, need to test on linux
|
|
import ntpath
|
|
|
|
|
|
try: input = raw_input
|
|
except: pass
|
|
|
|
|
|
#==============================================================
|
|
re_remove_comment = re.compile( "#.*$" )
|
|
def remove_comment( s ):
|
|
return re.sub( re_remove_comment, "", s )
|
|
|
|
def singular_pulural( val, singular, plural ):
|
|
return singular if val == 1 else plural
|
|
|
|
|
|
#==============================================================
|
|
def enum(*sequential, **named):
|
|
enums = dict(zip(sequential, range(len(sequential))), **named)
|
|
return type('Enum', (), enums)
|
|
|
|
MSG = enum('SHUTDOWN', 'PARSE_DIRECTORY', 'RUN_FUNCTION')
|
|
|
|
p4_ignore = ".p4ignore"
|
|
|
|
main_pid = os.getpid( )
|
|
|
|
|
|
#==============================================================
|
|
#if os.name == 'nt' or sys.platform == 'cygwin'
|
|
def basename( path ):
|
|
# 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
|
|
|
|
#return os.path.basename( path )
|
|
return ntpath.basename( path )
|
|
|
|
def normpath( path ):
|
|
return ntpath.normpath( path )
|
|
|
|
def join( patha, pathb ):
|
|
return ntpath.join( patha, pathb )
|
|
|
|
def splitdrive( path ):
|
|
return ntpath.splitdrive( path )
|
|
|
|
def p4FriendlyPath(path):
|
|
"""
|
|
Returns path with sanitized unsupported characters due to filename limitations.
|
|
"""
|
|
# http://www.perforce.com/perforce/doc.current/manuals/cmdref/filespecs.html#1041962
|
|
replace_items = {
|
|
'@' : '%40',
|
|
'#' : '%23',
|
|
'*' : '%2A',
|
|
'%' : '%25'
|
|
}
|
|
def replace(c):
|
|
return replace_items[c] if c in replace_items else c
|
|
return ''.join(map(replace, path))
|
|
|
|
#==============================================================
|
|
def get_ignore_list( path, files_to_ignore ):
|
|
# have to split path and test top directory
|
|
dirs = path.split( os.sep )
|
|
|
|
ignore_list = [ ]
|
|
|
|
for i, val in enumerate( dirs ):
|
|
path_to_find = os.sep.join( dirs[ : i + 1] )
|
|
|
|
if path_to_find in files_to_ignore:
|
|
ignore_list.extend( files_to_ignore[ path_to_find ] )
|
|
|
|
return ignore_list
|
|
|
|
def match_in_ignore_list( path, ignore_list ):
|
|
for r in ignore_list:
|
|
if re.match( r, path ):
|
|
return True
|
|
return False
|
|
|
|
|
|
#==============================================================
|
|
def call_process( args ):
|
|
return subprocess.call( args, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
|
|
|
|
def try_call_process( args, path=None ):
|
|
try:
|
|
subprocess.check_output( args, shell=False, cwd=path )#, stderr=subprocess.STDOUT )
|
|
return 0
|
|
except subprocess.CalledProcessError:
|
|
return 1
|
|
|
|
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
|
|
|
|
def parse_info_from_command( args, value, path = None ):
|
|
"""
|
|
|
|
:rtype : string
|
|
"""
|
|
proc = subprocess.Popen( args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=path )
|
|
for line in proc.stdout:
|
|
line = get_str_from_process_stdout( line )
|
|
|
|
if not line.startswith( value ):
|
|
continue
|
|
return line[ len( value ) : ].strip( )
|
|
return None
|
|
|
|
def get_p4_py_results( args, path = None ):
|
|
results = []
|
|
proc = subprocess.Popen( 'p4 -G ' + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=path )
|
|
try:
|
|
while True:
|
|
output = marshal.load( proc.stdout )
|
|
results.append( output )
|
|
except EOFError:
|
|
pass
|
|
finally:
|
|
proc.stdout.close()
|
|
return results
|
|
|
|
|
|
#==============================================================
|
|
def fail_if_no_p4():
|
|
if call_process( 'p4 -V' ) != 0:
|
|
print( 'Perforce Command-line Client(p4) is required for this script.' )
|
|
sys.exit( 1 )
|
|
|
|
# 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 ..."
|
|
|
|
proc = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=path )
|
|
for line in proc.stdout:
|
|
line = get_str_from_process_stdout( line )
|
|
|
|
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 )
|
|
|
|
proc.wait( )
|
|
|
|
for line in proc.stderr:
|
|
if "no such file" in line:
|
|
continue
|
|
raise Exception(line)
|
|
|
|
return files
|
|
|
|
def get_client_root( ):
|
|
"""
|
|
|
|
:rtype : string
|
|
"""
|
|
command = "p4 info"
|
|
|
|
proc = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
|
|
for line in proc.stdout:
|
|
line = get_str_from_process_stdout( line )
|
|
|
|
clientFile_tag = "Client root: "
|
|
if not line.startswith( clientFile_tag ):
|
|
continue
|
|
|
|
local_path = normpath( line[ len( clientFile_tag ) : ].strip( ) )
|
|
if local_path == "null":
|
|
local_path = None
|
|
return local_path
|
|
return None
|
|
|
|
class P4Workspace:
|
|
"""
|
|
Use this class when working in a workspace.
|
|
Makes sure the environmentals are setup correctly, and that you aren't working on a non-perforce directory accidentally;
|
|
otherwise you can delete files that shouldn't be deleted. Ex:
|
|
|
|
with P4Workspace( cwd ): #sets current workspace to cwd, or fails
|
|
# do stuff here
|
|
# on exit reverts to previous set workspace
|
|
"""
|
|
|
|
def __init__( self, directory):
|
|
self.directory = directory
|
|
|
|
def __enter__( self ):
|
|
# get user
|
|
#print("\nChecking p4 info...")
|
|
result = get_p4_py_results('info')
|
|
if len(result) == 0 or b'userName' not in result[0].keys():
|
|
print("Can't find perforce info, is it even setup? Possibly can't connect to server.")
|
|
sys.exit(1)
|
|
username = get_str_from_process_stdout(result[0][b'userName'])
|
|
client_host = get_str_from_process_stdout(result[0][b'clientHost'])
|
|
#print("|Done.")
|
|
|
|
# see if current directory is set to current workspace, if not, set it to current workspace.
|
|
client_root = get_client_root()
|
|
ldirectory = self.directory.lower()
|
|
oldworkspace_name = None
|
|
|
|
# If workspace root is null, it could be because there are multiple views and not a single root.
|
|
if client_root is None:
|
|
results = get_p4_py_results('where', self.directory)
|
|
for result in results:
|
|
path = result[b'path']
|
|
path = re.sub('...$', '', path)
|
|
path = normpath(path)
|
|
if ldirectory.startswith(path.lower()):
|
|
client_root = path
|
|
break
|
|
|
|
if client_root is None or not ldirectory.startswith(client_root.lower()):
|
|
#print("\nCurrent directory not in client view, checking other workspaces for user '" + username + "' ...")
|
|
|
|
oldworkspace_name = parse_info_from_command('p4 info', 'Client name: ')
|
|
|
|
# get user workspaces
|
|
results = get_p4_py_results('workspaces -u ' + username)
|
|
workspaces = []
|
|
for r in results:
|
|
whost = get_str_from_process_stdout(r[b'Host'])
|
|
if whost is not None and len(whost) != 0 and client_host != whost:
|
|
continue
|
|
workspace = {'root': get_str_from_process_stdout(r[b'Root']), 'name': get_str_from_process_stdout(r[b'client'])}
|
|
workspaces.append(workspace)
|
|
|
|
del results
|
|
|
|
# check current directory against current workspace, see if it matches existing workspaces.
|
|
for w in workspaces:
|
|
wname = w['name']
|
|
wlower = w['root'].lower()
|
|
if ldirectory.startswith(wlower):
|
|
# set current directory, don't forget to revert it back to the existing one
|
|
#print("|Setting client view to: " + wname)
|
|
|
|
if try_call_process( 'p4 set P4CLIENT=' + wname ):
|
|
#print("|There was a problem trying to set the p4 client view (workspace).")
|
|
sys.exit(1)
|
|
break
|
|
else:
|
|
# TODO: look up workspace/users for this computer
|
|
print( "Couldn't find a workspace root that matches the current directory for the current user." )
|
|
sys.exit(1)
|
|
#print("|Done.")
|
|
self.oldworkspace_name = oldworkspace_name
|
|
return self
|
|
|
|
def __exit__( self, type, value, tb ):
|
|
# If we changed the current workspace, switch it back.
|
|
if self.oldworkspace_name is not None:
|
|
#c.write("\nReverting back to original client view...")
|
|
# set workspace back to the original one
|
|
if try_call_process( 'p4 set P4CLIENT=' + self.oldworkspace_name ):
|
|
# error_count += 1 # have console log errors
|
|
# if not options.quiet:
|
|
print("There was a problem trying to restore the set p4 client view (workspace).")
|
|
sys.exit(1)
|
|
#else:
|
|
# if not options.quiet:
|
|
# c.write("|Reverted client view back to '" + self.oldworkspace_name + "'.")
|
|
#if not options.quiet:
|
|
# c.write("|Done.")
|
|
|
|
#==============================================================
|
|
class PTable( list ):
|
|
def __init__( self, *args ):
|
|
list.__init__( self, args )
|
|
self.mutex = multiprocessing.Semaphore( )
|
|
|
|
class PDict( dict ):
|
|
def __init__( self, *args ):
|
|
dict.__init__( self, args )
|
|
self.mutex = multiprocessing.Semaphore( )
|
|
|
|
|
|
#==============================================================
|
|
# TODO: Create a child thread for triggering autoflush events
|
|
# TODO: Hook console into stdout so it catches print
|
|
class Console( threading.Thread ):
|
|
MSG = enum('WRITE', 'FLUSH', 'SHUTDOWN', 'CLEAR' )
|
|
|
|
@staticmethod
|
|
def wake(thread):
|
|
thread.flush()
|
|
if not thread.shutting_down:
|
|
thread.wake_thread = threading.Timer(thread.auto_flush_time / 1000.0, Console.wake, [thread])
|
|
thread.wake_thread.daemon = True
|
|
thread.wake_thread.start()
|
|
|
|
# auto_flush_time is time in milliseconds since last flush to trigger a flush when writing
|
|
def __init__( self, auto_flush_num = None, auto_flush_time = None ):
|
|
threading.Thread.__init__( self )
|
|
self.buffers = {}
|
|
self.buffer_write_times = {}
|
|
self.running = True
|
|
self.queue = multiprocessing.JoinableQueue( )
|
|
self.auto_flush_num = auto_flush_num if auto_flush_num is not None else -1
|
|
self.auto_flush_time = auto_flush_time * 1000 if auto_flush_time is not None else -1
|
|
self.shutting_down = False
|
|
self.wake_thread = None
|
|
if self.auto_flush_time > 0:
|
|
Console.wake(self)
|
|
|
|
def write( self, data, pid = None ):
|
|
pid = pid if pid is not None else threading.current_thread().ident
|
|
self.queue.put( ( Console.MSG.WRITE, pid, data ) )
|
|
|
|
def writeflush( self, data, pid = None ):
|
|
pid = pid if pid is not None else threading.current_thread().ident
|
|
self.queue.put( ( Console.MSG.WRITE, pid, data ) )
|
|
self.queue.put( ( Console.MSG.FLUSH, pid ) )
|
|
|
|
def flush( self, pid = None ):
|
|
pid = pid if pid is not None else threading.current_thread().ident
|
|
self.queue.put( ( Console.MSG.FLUSH, pid ) )
|
|
|
|
def clear( self, pid = None ):
|
|
pid = pid if pid is not None else threading.current_thread().ident
|
|
self.queue.put( ( Console.MSG.CLEAR, pid ) )
|
|
|
|
def __enter__( self ):
|
|
self.start( )
|
|
return self
|
|
|
|
def __exit__( self, type, value, tb ):
|
|
self.shutting_down = True
|
|
if self.wake_thread:
|
|
self.wake_thread.cancel()
|
|
self.wake_thread.join()
|
|
self.queue.put( ( Console.MSG.SHUTDOWN, ) )
|
|
self.queue.join( )
|
|
|
|
def run( self ):
|
|
while True:
|
|
data = self.queue.get( )
|
|
event = data[0]
|
|
|
|
if event == Console.MSG.SHUTDOWN:
|
|
for ( pid, buffer ) in self.buffers.items( ):
|
|
for line in buffer:
|
|
print( line )
|
|
self.buffers.clear( )
|
|
self.buffer_write_times.clear( )
|
|
self.queue.task_done( )
|
|
break
|
|
|
|
elif event == Console.MSG.WRITE:
|
|
pid, s = data[ 1 : ]
|
|
|
|
if pid not in self.buffers:
|
|
self.buffers[ pid ] = []
|
|
if pid not in self.buffer_write_times:
|
|
self.buffer_write_times[ pid ] = datetime.datetime.now( )
|
|
self.buffers[ pid ].append( s )
|
|
|
|
try:
|
|
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:
|
|
self.flush( pid )
|
|
except TypeError:
|
|
print('"' + pid + '"')
|
|
raise
|
|
# 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
|
|
elif event == Console.MSG.FLUSH:
|
|
pid = data[ 1 ]
|
|
if pid in self.buffers:
|
|
buffer = self.buffers[ pid ]
|
|
for line in buffer:
|
|
print( line )
|
|
self.buffers.pop( pid, None )
|
|
self.buffer_write_times[ pid ] = datetime.datetime.now( )
|
|
elif event == Console.MSG.CLEAR:
|
|
pid = data[ 1 ]
|
|
if pid in self.buffers:
|
|
self.buffers.pop( pid, None )
|
|
|
|
self.queue.task_done( )
|
|
|
|
|
|
#==============================================================
|
|
# 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( )
|
|
|
|
|
|
#==============================================================
|
|
class Worker( threading.Thread ):
|
|
def __init__( self, console, queue, commands ):
|
|
threading.Thread.__init__( self )
|
|
|
|
self.queue = queue
|
|
self.commands = commands
|
|
|
|
def run( self ):
|
|
while True:
|
|
( cmd, data ) = self.queue.get( )
|
|
if not self.commands[cmd](data):
|
|
self.queue.task_done( )
|
|
break
|
|
self.queue.task_done( )
|
|
|