#!/usr/bin/python
#
# decentprotect.py: Perforce decentralized protect daemon
#
# Copyright (C) 2003-2008 Servaas Goossens (sgoossens@ortec.nl)
#
# ----
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License Version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# ----
#
# This script implements a decentralized protection scheme for perforce
# by combining multiple protect files located throughout the depot.
#
# Local permissions for a subtree are stored in a file in the root
# of that subtree. The script combines all these files into one list
# and applies it using p4 protect.
#
# It allows default permissions to be set at a higher level, which
# can be overruled by subtrees. It is also possible to enforce certain
# permissions, which cannot be overruled in a subtree. Permissions for
# editing these permission files may be given explicitly in the same
# files.
#
# There's one special file: the master protect file. It can be
# submitted anywhere in the depot. Only the master protect file may
# include lines with superuser privileges and may have absolute paths.
#
# Invoke this script with the -h option to get a usage summary. Or
# see the usage() function below.
#
#########################
# CONFIGURATION VARIABLES
# Set this to 1 in order to *prevent*
# - actually changing the perforce permissions
# - sending mail to perforce users
# Additionally:
# - ".test" is appended to the log file name
# - ".test" is appended to the output file name
# - repeat is set to zero (no looping)
# The cache file and the protect counter are still updated
justtesting = 0
# name of the files in the depot that contain protection info
protectfile = 'p4.protect'
# exact full path in the depot of the master protect file
# - the filename should not be equal to the protectfile
masterprotectfile = '//depot/admin/master.protect'
# Whether to allow wildcard ('*') in user (or group) name for
# non-exclusionary permission lines. This enforces the use of
# predefined groups.
allow_user_wildcards = 1
# filename where to store the resulting protections
# or None if they should not be saved in a file.
# This file contains comments indicating where each protect
# line comes from. Useful for testing and diagnosing problems
save_to_file = 'decentprotect.out'
# newline may be '\n' or '\n\r' (used only for writing
# protections) This is useful if this script runs on *NIX
# and you want to view the generated protections file using
# a (not so smart) Windows tool.
newline = '\r\n'
# When defined, some statistics will be written to the log
# when new permissions are generated
logfile = 'decentprotect.log'
# Timeformat used in the log file
timeformat = '%Y-%m-%d %H:%M:%S (%Z)'
# The Cache file contains all protect files (it's too expensive
# to ask Perforce everytime with p4 files //.../p4.protect)
cachefile = 'decentprotect.cache'
# email address of the person looking after this script.
administrator = 'jdoe@example.com'
# Send some statistics to the administrator everytime the
# protections change?
mail_statistics = 1
# For mails sent to a perforce user: whether to send a cc to
# the administrator, and whether reply_to should be set to
# the administrator
bcc_admin = 1
reply_to_admin = 1
# what SMTP server to use when sending mail.
mailhost = 'smtp.example.com'
# the p4 command line client
p4cmd ='/usr/local/bin/p4'
# Repeat allows decentprotect to work as a deamon, which keeps
# running. When set to 0, the script will poll once and then
# exit. This only applies when the -D option is given.
repeat = 1
# When working as a deamon and repeat is enabled,
# decentprotect will sleep before polling again
sleeptime = 5 # in seconds.
# when working as a deamon and repeat is enabled: if an error
# occurs, decentprotect will use increasing delays before
# trying again.
min_retry_delay = 60 # seconds
max_retry_delay = 60 * 60 * 2 # seconds
# name of the perforce counter used to determine whether
# any changelists have been submitted since the previous run
counter = 'protect'
# This user must have Perforce superuser privileges
p4user = 'administrator'
p4passwd = 'mysecret'
p4port = 'p4.example.com:1666'
# ip address of the machine running this script, as seen by
# the perforce server. This is used for the appended
# permissions below. You may have to set this to 127.0.0.1
# if the script runs on the perforce server
myipaddress = '192.168.1.111'
# This text will always be appended to the permissions.
# This will ensure that this script will never
# accidentally lose the necessary permissions.
append_block = \
( '# Permissions appended by the decentprotect script' + newline + \
'\tsuper user %(user)s %(ipaddress)s //none' + newline + \
'\tread user %(user)s %(ipaddress)s //.../%(protectfile)s' + newline + \
'\tread user %(user)s %(ipaddress)s %(masterprotectfile)s' + newline
) % {
'ipaddress': myipaddress,
'user': p4user,
'protectfile': protectfile,
'masterprotectfile': masterprotectfile
}
# END OF CONFIGURATION VARIABLES
################################
import sys, os, math, string, smtplib, re, marshal, time, traceback, p4, getopt, shelve
p4.p4 = p4cmd
os.environ['P4USER'] = p4user
os.environ['P4PASSWD'] = p4passwd
os.environ['P4PORT'] = p4port
# global var for delaying the retry after errors occur
retrydelay = 0
# override some config params for testing
if justtesting:
repeat = 0
if save_to_file:
save_to_file += ".test"
if logfile:
logfile += ".test"
supportedsections = ['default', 'enforce', 'protect']
if administrator and reply_to_admin:
replyto_line='Reply-To: '+administrator+'\n'
else:
replyto_line=''
def formatException(exc_info):
'''Returns a formatted traceback, given the output of sys.exc_info().'''
type, value, tb = exc_info
result = "Traceback (innermost last):\n"
list = traceback.format_tb(tb, 1000) + traceback.format_exception_only(type, value)
result = result + "%-20s %s" % (
string.join(list[:-1], ""),
list[-1],
)
return result
def formatDuration(seconds):
'''Returns a formatted duration string, given a number of seconds'''
if seconds >= 60 * 60 * 24:
hours = float(seconds) / (60 * 60)
result = '%d day%s%d hour%s' % \
(int(math.floor(hours / 24)),
(int(math.floor(hours / 24)) == 1) and ' ' or 's ',
int(round(hours % 24)),
(int(round(hours % 24)) == 1) and ' ' or 's ')
elif seconds >= 60 * 60:
minutes = float(seconds) / 60
result = '%d hour%s%d minute%s' % \
(int(math.floor(minutes / 60)),
(int(math.floor(minutes / 60)) == 1) and ' ' or 's ',
int(round(minutes % 60)),
(int(round(minutes % 60)) == 1) and ' ' or 's ')
elif seconds >= 60:
seconds = float(seconds)
result = '%d minute%s%d second%s' % \
(int(math.floor(seconds / (60))),
(int(math.floor(seconds / (60))) == 1) and ' ' or 's ',
int(round(seconds % 60)),
(int(round(seconds % 60)) == 1) and ' ' or 's ')
else:
result = '%.1f seconds' % float(seconds)
return result.strip()
def log(text):
'''writes the given text to the log file'''
f = file(logfile, 'a')
f.write('%s %s\n' % ( time.strftime(timeformat, time.localtime()), text))
f.close()
theMailPort = None
def mailport():
'''Lazy create function of smtp mailport. Returns the mailport.'''
global theMailPort
if not theMailPort:
theMailPort = smtplib.SMTP(mailhost)
return theMailPort
def closemailport():
'''closes the mailport and clears the global mailport object.'''
global theMailPort
if theMailPort:
theMailPort.quit()
theMailPort = None
def complain(mailport,complaint):
'''Send a plaintive message to the human looking after this script if we
have any difficulties. If no email address for such a human is given,
send the complaint to stderr.
'''
complaint = complaint + '\n'
if administrator:
mailport.sendmail('"Perforce Protect Daemon" <%s>' % administrator,
[administrator],
'Subject: Perforce Protect Daemon Problem\n\n' + complaint)
else:
sys.stderr.write(complaint)
def mailit(mailport, sender, recipients, message):
'''Try to mail message from sender to list of recipients using SMTP object
mailport. complain() if there are any problems.
'''
try:
failed = mailport.sendmail(sender, recipients, message)
except:
failed = 'Exception ' + repr(sys.exc_info()[0]) + ' was raised.\n%s\n.' % \
str(sys.exc_info()[1])
if failed:
complain( mailport, 'The following errors\n' +\
str(failed) +\
'\noccurred while trying to email from\n' + str(sender) + '\nto ' +\
str(recipients) + '\nwith body\n\n' + str(message))
def complain_to_submitter(mailport, file, changenr, complaint):
'''Send a message to the submitter of the given change, with a cc to
the person responsible for the protections (if configured).
'''
user, email, fullname = '', '', ''
try:
# find email address of the submitter
change = p4.describe(change)
username = change['user']
user = p4.user(username)
email = user['Email']
fullname = user['FullName']
if justtesting:
# don't send mail to perforce users
email = administrator
recipients_with_fullname = '"%s" <%s>' % (fullname, email)
recipients = [email]
if bcc_admin and administrator and (administrator <> email):
recipients.append(administrator)
message = ('From: "Perforce Protection Daemon" <%s>\n' % administrator) +\
'To: "%s" <%s>\n' % (fullname, email) + \
'Subject: Error in p4.protect file: change %s.\n' % (str(changenr)) +\
replyto_line +\
'\n' +\
'The following problem(s) occured when processing the file\n' + \
'\t' + file + '\n\n' + \
complaint
mailit(mailport, administrator, recipients, message)
except:
complain(mailport,
('Could not send mail to the submitter of change %s.\n' % str(changenr)) + \
'Exception ' + repr(sys.exc_info()[0]) + ' was raised.' + \
str(sys.exc_info()[1]) + \
('File %s has the following problems:\n\n%s' % (file, complaint)))
def protectFilesInChange(change):
'''Returns a tuple of two lists (newandupdated, deleted) of the protect files
contained in the given change.'''
newandupdated, deleted = [], []
i = 0
while ('depotFile' + str(i)) in change:
filename = change['depotFile'+str(i)]
if filename.endswith('/' + protectfile) or (filename == masterprotectfile):
if change['action' + str(i)] == 'delete':
deleted.append(filename)
else:
newandupdated.append(filename)
i += 1
return newandupdated, deleted
def generate_protectline(fields, sectionname, path,
allow_superuser=0, allow_absolutepath=0):
'''Fields are checked for their validity. Returns a tuple (success, resultline).
If any field is invalid, then a line with the reason is returned,
otherwise the fields are turned into a line in the format accepted by
p4 protect:
mode group/user name host path
'''
reason = ''
# a protect line contains the following fields, the last two are optional:
# mode, 'group' or 'user', name, host, path
modes = ['list', 'read', 'open', 'write', 'admin', 'super', 'review']
# if len(fields) == 3:
# fields.append('*') # default host
# if len(fields) == 4:
# fields.append('...') # default (relative) path
if len(fields) != 5:
# reason = 'Each protect line must have at least 3 and at most 5 fields separated by spaces.'
reason = 'Each protect line must have exactly 5 fields separated by spaces.'
elif not (fields[0] in modes):
reason = 'Mode must be one of %s.' % str(modes)[1:-1]
elif not (fields[1] in ['group', 'user']):
reason = "The second field must be either 'group' or 'user'."
elif fields[4][-1] == '/':
reason = "A null directory (path ending with '/') is not allowed."
elif (fields[0] == 'super') and not allow_superuser:
reason = 'Superuser privileges are not allowed here.'
elif not allow_absolutepath and \
((fields[4][0] == '/') or (fields[4][0:2] == '-/')):
reason = "An absolute path (beginning with '/') is not allowed here."
elif not allow_user_wildcards and \
(fields[4][0] <> '-') and ('*' in fields[2]):
reason = "A wildcard ('*') is not allowed in the username or groupname, except when the path begins with a dash ('-')."
elif (sectionname == 'protect') and not (fields[0] in ['write', 'read']):
reason = 'Only write and read permissions are allowed in the protect section.'
elif (sectionname == 'protect') and \
(fields[4][-len(protectfile):] != protectfile):
# in the protect section, lines that specify anything other than a
# protectfile are not allowed
reason = "In the protect section, the path field should end with '%s'." %\
protectfile
else:
# Everything Ok.
if (fields[4][0] != '/') and (fields[4][0:2] != '-/'):
# make relative path absolute (take care to keep the dash in front)
if fields[4][0] == '-':
path = '-' + path
fields[4] = fields[4][1:]
fields[4] = path + fields[4]
line = ' '.join(fields)
if reason:
# include reason and the invalid line as a comment
return 0, reason + '\nThe line where the problem occured is:\n\t' + line + '\n'
else:
return 1, '\t' + line
def parse_protectfile(content, filespec, path, change,
allow_superuser=0, allow_absolutepath=0):
'''Parse the contents of a protectfile. Return the dictionary
{sectionname: [protectlines], ... }
A protect file constists of comments, sectionheaders and indented
lines. Sections default, enforce and protect are supported.
'''
sections = {}
for sect in supportedsections:
sections[sect] = []
sectionheader = ''
sectionlines = []
problems = ''
linenr = 0
for line in content.split('\n'):
linenr += 1
try:
# ignore empty lines and lines beginning with #
if not line.strip() or (line.strip()[0] == '#'):
continue
elif line[0] in string.whitespace:
# indented line, should be a protect line
fields = line.split()
success, result = generate_protectline(fields, sectionheader, path,
allow_superuser, allow_absolutepath)
if not success:
problems += 'Error in line %d (the line has been skipped)\n%s\n' %\
(linenr, result)
else:
sectionlines.append(result)
else:
# start of new section, save contents of previous section
if sectionheader in supportedsections:
sections[sectionheader] = sectionlines
sectionlines = []
sectionheader = line.strip().lower()
if sectionheader[-1] == ':':
sectionheader = sectionheader[:-1]
if (sectionheader in sections.keys()) and sections[sectionheader]:
raise Exception('Section %s should not occur more than once' %\
sectionheader)
if sectionheader not in supportedsections:
problems += \
'Error in line %d: Unsupported section %s has been skipped.\n' % \
sectionheader
except:
raise Exception('Error when parsing %s, line %d:\n%s.' %
(filespec, linenr, str(sys.exc_info()[1])))
if sectionheader in supportedsections:
sections[sectionheader] = sectionlines
if problems:
complain_to_submitter(mailport(), filespec, change, problems)
return sections
def compare_depotPath(f1, f2):
'''Compare function for sorting a filelist according to the depotPath value.
f1 and f2 are dictionaries.'''
return cmp(f1['depotPath'], f2['depotPath'])
def depotFileToDepotPath(depotFile):
'''converts the depotFile attribute to a depotPath attribute.
DepotPath for the masterprotectfile is '//' to ensure it is first
in the sorted list '''
if depotFile == masterprotectfile:
return '//'
elif depotFile.endswith(protectfile):
return depotFile[:-len(protectfile)]
else:
raise Exception('Cannot convert to depotPath (not a protect file): ' + depotFile);
def get_protectfiles(filenames = 'all'):
'''Retrieves the requested protectfiles, or all protectfiles if filenames is 'all'.
Returns a list of dictionaries (as generated by perforce, plus a depotPath key).
Warning: retrieving all protectfiles may be an expensive operation for Perforce.
'''
if filenames == 'all':
command = p4.p4 + ' -G files %s //.../%s' % (masterprotectfile, protectfile)
elif not filenames:
return []
else:
# just to be sure, strip the filenames (newlines!)
command = p4.p4 + ' -G files ' + ' '.join(map(string.strip, filenames))
filelist = p4.executeM(command)
# remove deleted files
filelist = filter(lambda x: x['action'] != 'delete', filelist)
# add depotPath to all items
for item in filelist:
item['depotPath'] = depotFileToDepotPath(item['depotFile'])
# sort by depotPath
filelist.sort(compare_depotPath)
# Retrieve file contents from perforce depot.
filenames = map(lambda item: item['depotFile'] + '@' + str(item['change']), filelist)
data = p4.readM(filenames)
# copy file content from elements of 'data' to the elements of 'filelist'
for filename, element in zip(filenames, filelist):
element['content'] = data[filename]['content']
return filelist
def generate_localprotections(filelist, allow_superuser=0, allow_absolutepath=0):
'''Recursive backend for generate_protections(). Returns a tuple
(remainingfilelist, mainblock, protectblock) where remainingfilelist
contains all elements of filelist not processed by this call, mainblock
contains default and enforce sections and protectblock contains protect
sections for files that belong to the hierarchy of the first file
in filelist. This assumes that filelist is sorted.
'''
if not filelist:
return ([], '', '')
file = filelist.pop(0)
changenr = file['change']
filename = file['depotFile'] + '@' + str(changenr)
path = file['depotPath']
content = file['content']
try:
sections = parse_protectfile(content, filename, path, changenr,
allow_superuser, allow_absolutepath)
except:
complain_to_submitter(mailport(), filename, changenr,
('An error occured when parsing %s.\n' % filename) +\
'This file is therefore not included in the updated protections.\n' +\
('Exception Info: %s' % str(sys.exc_info()[1]))
)
skipped = '# File %s was skipped due to a parse error.%s' % (filename, newline)
sections = {'default':[skipped], 'enforce':[skipped], 'protect':[skipped]}
mainblock = ''
protectblock = ''
if sections['default']:
mainblock += '# default section from ' + filename + newline
mainblock += newline.join(sections['default'])
mainblock += newline
# recurse: process all protectfiles below the current path
while filelist and (filelist[0]['depotPath'].find(path) == 0):
filelist, newmainblock, newprotectblock = generate_localprotections(filelist)
mainblock += newmainblock
protectblock += newprotectblock
if sections['enforce']:
mainblock += '# enforce section from ' + filename + newline
mainblock += newline.join(sections['enforce'])
mainblock += newline
if sections['protect']:
protectblock += '# protect section from ' + filename + newline
protectblock += newline.join(sections['protect'])
protectblock += newline
return filelist, mainblock, protectblock
def generate_protections(filelist):
'''Generate the protections from given list containing dictionaries with file data.
The generated protect file is returned as a string.
'''
lastchange = reduce(lambda change, item: max(change, int(item['change'])), filelist, 0)
header = '# Generated by decentprotect.py on %s.%s# Last change: %s.%sProtections:' % \
(time.strftime(timeformat, time.localtime()), newline, str(lastchange), newline + newline)
# delegate the real work to recursive back-end
filelist, mainblock, protectblock = generate_localprotections(filelist, 1, 1)
return header + newline + \
mainblock + newline + \
protectblock + newline + \
append_block
def applyprotections(protections):
'''Applies the protections in Perforce. Returns True if protections were
actually modified, False otherwise.'''
# Will provide regular text as input. (p4 cmd without -G option)
# So I need to interpret the output text of the regular command
command = p4.p4 + ' protect -i'
# outstream is stdout + stderr
instream, outstream = os.popen4(command, 't')
try:
instream.write(protections)
instream.close()
output = outstream.read()
finally:
instream.close() # duplicate, in case the write fails
outstream.close()
if output == 'Protections not changed.\n':
return False
elif output == 'Protections saved.\n':
return True
else:
raise Exception('The command "%s" gives unexpected results:\n%s' % \
(command, output))
def getNewChanges():
'''Returns the list of new change descriptions, submitted since
the last processed change. Uses p4 -G review -t protect.'''
reviewlist = p4.review_t(counter)
changenrs = map(lambda x: x['change'], reviewlist)
return p4.describeM(changenrs)
def writeCacheFile(filelist=None):
'''Stores the list of files in the cache file (a shelf).
If no filelist is provided, it is retrieved from perforce,'''
cache = shelve.open(cachefile, flag='n')
try:
if not filelist:
filelist = get_protectfiles()
for item in filelist:
key = item['depotPath']
cache[key] = item
finally:
cache.close()
log('Cache file (re-)created with %d items' % len(filelist))
def updateCacheFile(updates, deletes):
'''Writes given updates to the cache file. deletes is a list of filenames.
updates is a list of dictionaries containing metadata and contents
of protect files.'''
if not updates and not deletes:
return None
cache = shelve.open(cachefile, flag='w')
try:
for fname in deletes:
key = depotFileToDepotPath(fname)
if key in cache:
del cache[key]
for item in updates:
key = item['depotPath']
cache[key] = item
finally:
cache.close()
def verifyCacheFile(filelist=None):
'''Compares the list of files with the contents of the cache.
If no filelist is provided, it is retrieved from perforce,
Returns differences as a list of tuples (filename, code) where code
can be "differs", "missing" or "deleted".'''
result = []
cache = shelve.open(cachefile, flag='r')
try:
if not filelist:
filelist = get_protectfiles()
keyset = {} # set() # for the keys found in filelist
for item in filelist:
key = item['depotPath']
keyset[key] = key # keyset.add(key)
if key not in cache:
result.append( (item['depotFile'], 'missing') )
elif cmp(item, cache[key]):
result.append( (item['depotFile'], 'differs') )
for key in cache.keys():
if key not in keyset:
item = cache[key]
result.append( (item['depotFile'], 'deleted') )
finally:
cache.close()
return result
def fixCacheFile():
'''Verifies the cache file and re-writes it when inconsistencies are found.
Returns differences found by verifyCacheFile.'''
filelist = get_protectfiles()
result = verifyCacheFile(filelist)
if result:
log('Inconsistencies found in cache file:\n%s' % '\n'.join(map(lambda r: r[1] + ': ' + r[0], result)))
writeCacheFile(filelist)
return result
def processChange(change):
'''Reads any protect files contained in the given change description
and updates the cache file accordingly. Returns the list of protect
files contained in the change.'''
newandupdatedfiles, deletedfiles = protectFilesInChange(change)
if newandupdatedfiles:
filelist = get_protectfiles(newandupdatedfiles)
else:
filelist = []
updateCacheFile(filelist, deletedfiles)
return newandupdatedfiles + deletedfiles
def updateProtections(dry_run=True):
'''Generates protections and (if dry_run==False) applies the protections
in Perforce. Returns True when protections in Perforce have been modified,
False otherwise.'''
# Note: dry_run=True by default, for safety reasons
# read cache contents
cache = shelve.open(cachefile, flag='r')
try:
filelist = []
for value in cache.itervalues():
filelist.append(value)
finally:
cache.close()
# sort by depotPath
filelist.sort(compare_depotPath)
protections = generate_protections(filelist)
if save_to_file:
file = open(save_to_file, 'wb')
try:
file.write(protections)
finally:
file.close()
if not dry_run:
return applyprotections(protections)
else:
return False
def reportSuccess(change, duration, updateditems):
'''Write a short report to the logfile and also send it by mail to the administrator,
(if that is what she wants)'''
try:
# in order to report latency, ask current time from server (just in case clocks are not synced)
# format: "2008/02/05 10:30:01 +0100 W. Europe Standard Time"
p4serverDate = p4.info()['serverDate']
localtime = time.time()
# remove the timezone info (time module won't understand it)
p4serverDate = ' '.join(p4serverDate.split()[:2])
p4time = time.mktime(time.strptime(p4serverDate, '%Y/%m/%d %H:%M:%S'))
secondsSinceSubmit = p4time - int(change['time'])
durationSinceSubmit = formatDuration(secondsSinceSubmit)
if abs(p4time - localtime) > 2:
# this has nothing to do with permissions anymore...
p4timediff = 'Time on the Perforce Server differs from localtime by approximately %s.\n' % formatDuration(p4time - localtime)
else:
p4timediff = ''
except:
durationSinceSubmit = '(error)'
p4timediff = ''
logline = 'change %s processed %s after submit (processing duration %.1f s)\n%s%s' % \
( str(change['change']),
durationSinceSubmit,
duration,
p4timediff,
'\n'.join(map(lambda x: '\t' + x, updateditems)))
log(logline)
# send administrator a notice of the statistics
if mail_statistics and administrator:
mailport().sendmail('"Perforce Protect Daemon" <%s>' % administrator,
[administrator],
'Subject: Perforce Protect Daemon statistics\n\n' + logline)
def loop_body():
'''Body of the main loop. Checks whether some protect files have been changed,
If that is the case, the cache is updated and new protections are generated and stored.'''
global retrydelay
try:
newChanges = getNewChanges()
for change in newChanges:
starttime = time.time()
updateditems = processChange(change)
if updateditems:
updateProtections(dry_run=bool(justtesting))
reportSuccess(change, time.time() - starttime, updateditems)
p4.setcounter(counter, change['change'])
# reset retrydelay
retrydelay = 0
except:
body = formatException(sys.exc_info())
if repeat:
# use increasing delays before retrying...
retrydelay = min(max_retry_delay, max(min_retry_delay, retrydelay * 2))
body = "Will retry after %s.\n" % (formatDuration(retrydelay)) + body
complain(mailport(),
'An error occured. The protections may not have been updated.\n%s\n' % body)
# clean up smtp connection
closemailport()
if retrydelay:
time.sleep(retrydelay)
def usage():
'''print usage information'''
print """\
Usage (initial setup and testing):
decentprotect -c | -f | -h | -p | -P | -u | -v
Usage (as a deamon):
decentprotect -D
Options:
-c Create a new cache file from the perforce depot.
-D Work as a deamon
-f Fix the cache if inconsistencies with the perforce depot
are found, inconsistencies are reported.
-h print usage info.
-u Update the cache based on the given changenr
-p Generate protections based on the cache, but don't apply them
in perforce (dry-run).
-P Generate protections based on the cache and apply them in
perforce.
-v Verify the cache with the perforce depot and report any
inconsistencies.
Notes:
"""
print " * The name of the cache file is %s" % cachefile
if save_to_file:
print " * For -p and -P generated protections are saved to %s" % save_to_file
if justtesting:
print " ============================================================="
print " * Now working in TEST MODE: no changes will be made in Perforce"
print " ============================================================="
print ""
if __name__ == '__main__':
if len(sys.argv) > 1:
opts, args = getopt.getopt(sys.argv[1:], 'cDfhHpPvu:')
if args or len(opts) != 1:
raise Exception('Unrecognized commandline arguments. (-h for usage).')
option, value = opts[0]
if option == '-c':
writeCacheFile()
elif option == '-f':
problems = fixCacheFile()
for f, c in problems:
print c + ':', f
if problems:
print "The cache has been re-created."
elif option == '-v':
problems = verifyCacheFile()
for f, c in problems:
print c + ':', f
elif option == '-u':
change = p4.describe(int(value))
processChange(change)
elif option == '-p':
updateProtections(dry_run=True)
elif option == '-P':
if updateProtections():
print 'Protections saved.\n'
else:
print 'Protections not changed.\n'
elif option == '-D':
# work as a deamon
if repeat:
log('Starting main loop (%ds sleeptime)' % sleeptime)
while (repeat):
loop_body()
time.sleep(sleeptime)
else:
loop_body()
else:
usage()
else:
usage()