#!/usr/bin/python import gc import os import re import sys import shutil import logging import getopt import marshal import binascii import tempfile from subprocess import Popen, PIPE, STDOUT #### MODIFY THIS SECTION AS APPROPRIATE P4CMD="/usr/local/bin/p4" P4PORT="localhost:1666" # P4USER should have super access on the system, as it needs to update counters, etc. P4USER="p4super" # PASSFILE is not required, as long as the user above has a valid ticket (e.g. unlimited) PASSFILE="superpass" P4CLIENT="thumb_client" CONVERTCMD="/opt/local/bin/convert" THUMBNAIL_GENERATOR_COUNTER="thumb-generator" THUMBNAIL_FORMAT=".png" THUMBNAIL_SIZE="x160" # TEMP_DIR should be a temporary location where you can write. The directory will be # created, if necessary. it will be cleaned out, so ensure that you are not pointing # to a directory with existing information TEMP_DIR = "/tmp/thumbs" EXPLICIT_GC = True LOCK_FILE = "/tmp/thumbnail-generator.lock" #### END MODIFICATION SECTION #### GLOBAL VARIABLES forceUnlock = False #### # usage # provides usage information for the utility def usage(): PROGRAM_USAGE = """\ Usage: ThumbnailGenerator.py [-h] [-f depotFile] [-c changelistNumber] [-l logfile] [-v] [-g] [-i] -h show help (this message) -v verbose (debug mode) -f depotFile specify file (depot path and version //path/to/file#nn) -c changelistNumber process only the specified changelist number and exit -p pageNum specify page number of file (relevant for PDF files; default is 0 or first page) -l logfile specify logfile (default is STDOUT) -g generate sample client specification -i initialize the system, creating the logger counter and processing all changelists -x ignore the lock file and run anyway """ print(PROGRAM_USAGE) normalExit() # generateSampleClient # this routine generates a sample client specification. The output should be sanity # checked, but can make creating the client spec a little easier. def generateSampleClient(): CLIENT_PREAMBLE = """\ Client: {0} Owner: {1} Description: Created by p4admin. Root: {2} Options: noallwrite noclobber nocompress unlocked nomodtime normdir SubmitOptions: submitunchanged LineEnd: local""".format(P4CLIENT, P4USER, TEMP_DIR) CLIENT_VIEW = """\ //{0}/....ppm //{1}/{0}/....ppm //{0}/....PPM //{1}/{0}/....PPM //{0}/....bmp //{1}/{0}/....bmp //{0}/....BMP //{1}/{0}/....BMP //{0}/....jpg //{1}/{0}/....jpg //{0}/....JPG //{1}/{0}/....JPG //{0}/....bpm //{1}/{0}/....bpm //{0}/....BPM //{1}/{0}/....BPM //{0}/....gif //{1}/{0}/....gif //{0}/....GIF //{1}/{0}/....GIF //{0}/....pgm //{1}/{0}/....pgm //{0}/....PGM //{1}/{0}/....PGM //{0}/....png //{1}/{0}/....png //{0}/....PNG //{1}/{0}/....PNG //{0}/....xbm //{1}/{0}/....xbm //{0}/....XBM //{1}/{0}/....XBM //{0}/....xpm //{1}/{0}/....xpm //{0}/....XPM //{1}/{0}/....XPM //{0}/....tga //{1}/{0}/....tga //{0}/....TGA //{1}/{0}/....TGA //{0}/....psd //{1}/{0}/....psd //{0}/....PSD //{1}/{0}/....PSD //{0}/....PDF //{1}/{0}/....PDF //{0}/....pdf //{1}/{0}/....pdf //{0}/....AI //{1}/{0}/....AI //{0}/....ai //{1}/{0}/....ai """ depotList = [] cmd = ["depots"] results = p4MarshalCmd(cmd) for r in results: if 'type' in r and (r['type'] == "local" or r['type'] == "stream"): depotList.append(r['name']) print(CLIENT_PREAMBLE) print("View:") for d in depotList: print(CLIENT_VIEW.format(d, P4CLIENT)) normalExit() # normalExit # exits def normalExit(code = 0, msg = None): if(msg != None): print msg; if not forceUnlock and os.path.exists(LOCK_FILE): # remove the lock file os.unlink(LOCK_FILE) sys.exit(code) # errorExit # exits after logging the error message -- does not remove the lock file def errorExit(msg): print("### ERROR: " + msg) sys.exit(9) # cleanTmpDir # cleans up the temporary directory def cleanTempDir(): for file_object in os.listdir(TEMP_DIR): file_object_path = os.path.join(TEMP_DIR, file_object) if os.path.isfile(file_object_path): os.unlink(file_object_path) else: shutil.rmtree(file_object_path) # p4MarshalCmd # executes the p4 command, results sent to a list def p4MarshalCmd(cmd,quiet=False): if not quiet: logging.debug("p4 {0}".format(" ".join(cmd))) list = [] pipe = Popen([P4CMD, "-p", P4PORT, "-u", P4USER, "-c", P4CLIENT, "-G"] + cmd, stdout=PIPE).stdout try: while 1: record = marshal.load(pipe) list.append(record) except EOFError: pass pipe.close() return list # p4InputCmd # executes the p4 command with input def p4InputCmd(data,cmd,quiet=False): if not quiet: logging.debug("p4 {0}".format(" ".join(cmd))) list = [] proc = Popen([P4CMD, "-p", P4PORT, "-u", P4USER, "-c", P4CLIENT, "-G"] + cmd, stdout=PIPE, stdin=PIPE, stderr=PIPE) outPipe = proc.stdout proc.stdin.write(data) return proc.communicate() # p4Cmd # executes a p4 command, returns results def p4Cmd(cmd,quiet=False): if not quiet: logging.debug("p4 {0}".format(" ".join(cmd))) proc = Popen([P4CMD, "-p", P4PORT, "-u", P4USER, "-c", P4CLIENT] + cmd, stdout=PIPE, stderr=PIPE) return proc.communicate() # p4Cmd # executes a p4 command, returns results def convert(original,thumb,page=0): logging.debug("convert -thumbnail {0} {1}[{2}] {3}".format(THUMBNAIL_SIZE, original, page, thumb)) proc = Popen([CONVERTCMD, "-thumbnail", THUMBNAIL_SIZE, original + "[{0}]".format(page), thumb], stdout=PIPE, stderr=PIPE) return proc.communicate() # containsError # utility function to check for any error code in the results array def containsError(results=[],logError=True): foundError = False for r in results: if 'code' in r: if r['code'] == 'error': foundError = True if logError: logging.error(r['data'].strip()) elif r['code'] == 'info': #code info output can be important in troubleshooting logging.debug(r['data']) return foundError # checkLogin # check the login ticket on the server def checkLogin(username=""): cmd = ["login","-s"] result = p4MarshalCmd(cmd,quiet=True) if containsError(result, False): return False else: return True # login # logs the user in using the password in the password file def login(): if PASSFILE is not None and os.path.isfile(PASSFILE): f = open(PASSFILE) lines = f.readlines() f.close() adminpass = lines[0].strip() cmd = ["login"] result = p4InputCmd(adminpass, cmd) if len(result[1]) > 0: return False else: return True else: return False # isSubmitted # checks to see if the changelist exists and that it is a 'submitted' state def isSubmitted(change): cmd = ["describe", "-s", change] result=p4MarshalCmd(cmd) if containsError(result, False): return False if result[0]['status'] == "submitted": return True else: return False # setLoggerCounter def resetCounters(): cmd = ["counter", "logger", "0"] p4Cmd(cmd) cmd = ["counter", "-d", THUMBNAIL_GENERATOR_COUNTER] p4Cmd(cmd) # setThumbAttribute # sets the 'thumb' attribute on the revision to the hex value specified def setThumbAttribute(revision, hex): cmd = ["attribute", "-e", "-f", "-n", "thumb", "-i", revision] result = p4InputCmd(hex, cmd) # generateThumbnail # generates a thumbnail for the specified depot file def generateThumbnail(depotFile, page=0): logging.debug("generating thumbnail for " + depotFile) # create TEMP_DIR, if it does not exist if not os.path.exists(TEMP_DIR): os.makedirs(TEMP_DIR) file = None rev = 0 matchObj = re.match( r'(.*)#(\d+)$', depotFile, re.M|re.I) if matchObj: file = matchObj.group(1) rev = int(matchObj.group(2)) else: file = depotFile extension = os.path.splitext(file)[1] p4print = tempfile.NamedTemporaryFile(dir=TEMP_DIR, prefix="p4_", suffix=extension) p4print.close() thumb = tempfile.NamedTemporaryFile(dir=TEMP_DIR, prefix="thumb_", suffix=THUMBNAIL_FORMAT) thumb.close() logging.debug("getting file from depot...") cmd = ["print", "-q", "-o", p4print.name, depotFile] result = p4Cmd(cmd) logging.debug("trying to generate thumbnail...") convert(p4print.name, thumb.name, page) if os.path.exists(thumb.name): file = open(thumb.name, "rb") bytes = file.read() file.close() hex = binascii.hexlify(bytes) logging.debug("storing thumbnail in metadata...") setThumbAttribute(depotFile, hex) # clean up the temporary directory cleanTempDir() # processChange # processes the files in the specified changelist def processChange(change): path = "//...@{0},@{0}".format(change) cmd = ["sync", "-p", "-n", path] result=p4MarshalCmd(cmd) if not containsError(result, False): for r in result: if r['action'] == "deleted": continue depotFile = "{0}#{1}".format(r['depotFile'], r['rev']) generateThumbnail(depotFile, 0) if(EXPLICIT_GC): # explicitly invoke garbage collection because thumbnails are being # read into memory and we want to ensure that memory is freed as # quickly as possible gc.collect() # initialConfiguration # routine to initialize the system by (re)setting the logger counter, deleting # the thumbnail generator counter, and processing all of the submitted changelists # starting from the beginning. Needless to say, this can potentially take a long # time to run. if you simply want to turn on the functionality, you just have # to create the logger counter (p4 counter logger 0) and then schedule the thumbnail # generator to run. Only new changelists will be processed, however (i.e. only new # versions of files will have thumbnails created) def initialConfiguration(): logging.info("performing initial configuration") resetCounters() cmd = ["changes"] result = p4MarshalCmd(cmd) if(containsError(result, False)): errorExit("Error finding updates") for r in reversed(result): if r['status'] == "submitted": change = r['change'] processChange(change) # checkForChanges # uses the logger command to find any changelists since the last run def checkForChanges(): cmd=["logger", "-t", THUMBNAIL_GENERATOR_COUNTER] result=p4MarshalCmd(cmd) if(containsError(result, False)): errorExit("Error finding updates") last = 0 for r in result: sequence = r['sequence'] change = r['attr'] if (int(change) > last) and isSubmitted(change): logging.info("Processing changelist @{0}".format(change)) processChange(change) last = int(change) logging.debug("updating logger counter...") cmd=["logger", "-c", sequence, "-t", THUMBNAIL_GENERATOR_COUNTER] p4Cmd(cmd) ########################################################################### ##### MAIN PROGRAM STARTS HERE ##### def main(argv=None): global forceUnlock verbose = False logFile = None depotFile = None changeId = 0 pageNum = 0 generateSample = False initialize = False try: opts, args = getopt.getopt(argv, "hl:f:c:p:vgix") for opt, arg in opts: if opt == "-v": verbose = True elif opt == "-h": usage() elif opt == "-l": logFile = arg elif opt == "-f": depotFile = arg elif opt == "-c": changeId = int(arg) elif opt == "-p": pageNum = int(arg) elif opt == "-g": generateSample = True elif opt == "-i": initialize = True elif opt == "-x": forceUnlock = True logLevel = logging.WARN if verbose: logLevel = logging.DEBUG if logFile is not None: logging.basicConfig(filename=logFile, format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p', level=logLevel) else: logging.basicConfig(format='[%(levelname)s] %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p', level=logLevel) # check to see if the lockfile is present if os.path.exists(LOCK_FILE): if not forceUnlock: errorExit("Program cannot run... lockfile exists -- " + LOCK_FILE) # if we get to this point, create the lock file as long as we're not in a force condition if not forceUnlock: open(LOCK_FILE, 'a').close() # check to see if the user is logged in, and if not then log in if not checkLogin(): if not login(): errorExit("Not logged in") if generateSample: generateSampleClient() elif depotFile is not None: generateThumbnail(depotFile, pageNum) elif changeId != 0: processChange(changeId) else: if initialize: initialConfiguration() else: checkForChanges() normalExit() except getopt.GetoptError as e: print(e) print("ERROR: unknown argument\n") usage() normalExit(2) if __name__ == '__main__': main(sys.argv[1:])
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#1 | 16507 | perforce_software | Move to main branch. | ||
//guest/perforce_software/piper/scripts/ThumbnailGenerator.py | |||||
#3 | 13690 | alan_petersen |
UPDATE: added locking mechanism to ThumbnailGenerator to prevent multiple scripts from running simultaneously. |
||
#2 | 13686 | alan_petersen |
Some people reported high CPU/RAM usage with the ThumbnailGenerator script, so I made some modifications to it: 1. ImageMagick was leaving around some temporary files after it ran, so now I clean up the TEMP_DIR location after the thumbnail attribute is set. this should prevent the temp directory from filling up. 2. I explicitly call gc.collect() after each call to generateThumbnail(). The thumbnail and hex versions get manipulated in memory in generateThumbnail() and I was relying on garbage collection to clean things up when the variables go out of scope, but gc.collect() is supposed to make it a little more explicit and help with performance, so I figured I could give it a try. 3. Experimenting on my VM, I ran the script and it gobbled up CPU (~70%) when running. I found that setting the nice level made it much less CPU-intense. For example: nice -n 19 ./ThumbnailGenerator.py -v -l /tmp/thumb.log -c 351 When run like this, the CPU usage (reported via top) peaked at about 2%. Coolio! |
||
#1 | 8919 | Matt Attaway | Initial add of Piper, a lightweight Perforce client for artists and designers. |