#******************************************************************** # # Copyright (C) 2005-2006 Hari Krishna Dara # # This file is part of p4admin. # # p4admin is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # p4admin 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, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # #******************************************************************* import os import os.path import stat import sys import re import logging import time import datetime import subprocess import atexit import threading log = logging.getLogger(__name__) shell_error = 0 # So that we can abort it while exiting. curProcess = None procMonThread = None def sendWarning(jobName, expectedRunDuration, lastRunStTime): log.debug('JobMonitorThread.run done waiting, no '+ 'notification, sending email') import notify notify.sendWarning(('%s job running for too long, '+ 'started at: %s') % (jobName, lastRunStTime)) # From commands.getstatusoutput() def execute(cmd, verbosity=3, expectedRunDuration=None, callback=sendWarning): """Return output of executing cmd in a shell as a string.""" #if verbosity >= 2: # log.info("Executing: %s", cmd) #text = pipe.read() #sts = pipe.close() #if sts is None: sts = 0 #global shell_error #shell_error = sts #if text[-1:] == '\n': text = text[:-1] #log.info(text) #return text output = execute2(cmd, verbosity, expectedRunDuration, callback) return ''.join(output) def execute2(cmd, verbosity=True, expectedRunDuration=None, callback=sendWarning): """Return output of executing cmd in a shell as a tuple of lines. If verbosity is: 0 - don't log any information 1 - log only warnings 2 - log only the command being executed 3 - log both command and output of the command""" global shell_error global curProcess global procMonThread if not procMonThread: procMonThread = JobMonitorThread() procMonThread.start() if verbosity >= 2: log.info(">>> %s", cmd) else: log.debug(">>> %s", cmd) #pipe = os.popen(cmd + ' 2>&1', 'r') if sys.modules.has_key('win32service'): # If service module is loaded. # This avoids the TypeError for missing stdin while running as service. # Based on the workaround suggested in the bug report at: # http://python.org/sf/1238747 log.debug('using nul stdin for child') stdin = file('nul', 'r') else: log.debug('using default stdin for child') stdin = None try: curProcess = subprocess.Popen(cmd, shell=True, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) finally: if stdin != None: stdin.close() log.debug('execute: created proces pid: %d', curProcess.pid) pipe = curProcess.stdout output = [] procMonThread.startMonitoring(cmd.split()[0]+"(pid:"+ str(curProcess.pid)+")", expectedRunDuration, callback) try: for line in pipe: #output[len(output):0] = line output.append(line) if verbosity >= 3: log.info("<<< %s", line.rstrip("\n")) finally: procMonThread.endMonitoring() log.debug('execute: closing pipe') #sts = pipe.close() pipe.close() curProcess.wait() sts = curProcess.returncode log.debug('execute: process exited, returncode: ' + str(sts)) curProcess = None if sts is None: sts = 0 if sts != 0 and verbosity > 0: log.warning('Command returned failure, return code: %d', sts) shell_error = sts # TODO: I remember to have seen a way to determine the LHS type and decide # whether to return the sequence or convert it to a String (is it called # covariant return type?). I will not then need two variants of execute. return output def killProcess(popen): """Kill the process object created by subprocess.Popen""" if popen == None: return log.debug('killProcess: %d', popen.pid) if os.name == 'nt': # From: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/347462 import win32api PROCESS_TERMINATE = 1 try: handle = win32api.OpenProcess(PROCESS_TERMINATE, False, popen.pid) win32api.TerminateProcess(handle, -1) win32api.CloseHandle(handle) except: # Most probably, the process already exited. pass else: import signal try: os.kill(popen.pid, signal.SIGTERM) time.sleep(1) os.kill(popen.pid, signal.SIGHUP) time.sleep(1) os.kill(popen.pid, signal.SIGKILL) except OSError, ex: # The process already exited. pass def killCurrentProcess(): if curProcess: log.info("At exit, killing process: %d", curProcess.pid) killProcess(curProcess) atexit.register(killCurrentProcess) def getScriptsRoot(): # Based on http://starship.python.net/crew/theller/moin.cgi/HowToDetermineIfRunningFromExe if hasattr(sys, "frozen"): # py2exe return os.path.dirname(os.path.dirname(sys.executable)) else: # This will have back-slashes if run using ActiveState python. return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def extractFieldValue(field, sep, output): m = re.compile(field + sep + r'(.*)\r?').search(output) if m != None: return m.group(1) else: return '' def escape(str, chars): return re.compile('(['+chars+'])').sub(r'\\\1', str) class RestartProgram(Exception): def __init__(self, msg): Exception.__init__(self, msg) def updateScriptFiles(): import config import notify if not config.packageSyncEnabled: return # If scripts are not in perforce, do nothing. if not config.packageInPerforce: return result = execute('p4 '+config.p4OptionsRemote+ ' sync '+config.scriptsRootNtv +'/scripts/... '+config.scriptsRootNtv+'/cfg/...') if shell_error != 0: import notify notify.sendError('Error updating the script files: '+result) if re.search(' - updating ', result) or re.search(' - added ', result) or\ re.search(' - deleted ', result): log.info('Script files have been updated, requesting a restart: %s', result) # If the scripts are loaded from a zip file, we need to refresh the zip # file itself. if re.search(r'.zip', __file__): library = os.path.dirname(__file__) log.info('Updating files in: %s', library) try: updateZip(library) except IOError, ex: log.exception('during updateZip') import notify notify.sendError('updation of '+library+' failed.\n'+str(ex)) except OSError, ex: log.exception('during updateZip') import notify notify.sendError('updation of '+library+' failed.\n'+str(ex)) raise RestartProgram( 'Restart required, one or more files have been updated') if config.p4RemoteClient: result = execute(config.remoteShell+' '+config.p4HostRemote+' p4 '+ config.p4RemoteOptions+' sync '+config.rsyncRemoteConfig) if shell_error != 0: import notify notify.sendError('Error updating the remote configuratil file: '+ config.rsyncRemoteConfig) def listCheckPts(checkptsDir): """Returnes a list of all the .ckp files sorted in reverse order""" checkpts = [f for f in os.listdir(checkptsDir) if (f.find('.ckp') > 0)] checkpts.sort(key=checkpointSeqNum, reverse=True) return checkpts def listJournals(journalsDir): """Returnes a list of all the .jnl files sorted""" journals = [f for f in os.listdir(journalsDir) if (f.find('.jnl') > 0)] journals.sort(key=journalSeqNum, reverse=False) return journals def removeOldCheckpoints(checkptsDir, journalsDir): """Remove old checkpoint and journal files""" import config checkpts = listCheckPts(checkptsDir) if len(checkpts) > config.numOldCheckPoints: # Find the sequence number of the first checkpoint that is too old. We # will remove all checkpoints and journals that has a sequence number # that is smaller than that. cutOffSeqNum = checkpointSeqNum(checkpts[config.numOldCheckPoints]) log.debug('Checkpoint cutOffSeqNum: %d', cutOffSeqNum) fileErrors = [] for file in checkpts[config.numOldCheckPoints:]: file = os.path.normpath(os.path.join(checkptsDir, file)) log.debug("Removing old checkpoint: %s", file) try: os.chmod(file, stat.S_IWRITE) os.remove(file) except OSError, ex: fileErrors.append(file+": "+str(ex)) #file = checkptsDirPsx+'/'+file #log.debug("Removing old checkpoint: %s", file) #result = execute('rm -f '+file) #if shell_error != 0: # fileErrors.append(file+": "+result) for file in listJournals(journalsDir): if journalSeqNum(file) > cutOffSeqNum: continue file = os.path.normpath(os.path.join(journalsDir, file)) log.debug("Removing old journal: %s", file) try: os.chmod(file, stat.S_IWRITE) os.remove(file) except OSError, ex: fileErrors.append(file+": "+str(ex)) #file = journalsDirPsx+'/'+file #log.debug("Removing old journal: %s", file) #result = execute('rm -f '+file) #if shell_error != 0: # fileErrors.append(file+": "+result) if fileErrors: import notify notify.sendError("There were errors clearing some old " "checkpoint/journal files:\n"+"\t\n".join(fileErrors)) def journalSeqNum(j): return int(re.search(r'jnl\.(\d+)(\.gz|$)', j).group(1)) def checkpointSeqNum(j): return int(re.search(r'ckp\.(\d+)(\.gz|$)', j).group(1)) class RestartProgram(Exception): """Just an exception designed to indicate that a program restart is requird.""" def __init__(self, msg): # Exception is an old style class, so we can't use super() here. Exception.__init__(self, msg) def getDateTime24hr(timeStr, refDate = datetime.datetime.now()): """Given a time string in 24hr format (%H:%M:%S), compute the datetime for today at the given time.""" timeOfDay = time.strptime(timeStr, "%H:%M:%S") return time.mktime(refDate.replace( hour=timeOfDay.tm_hour, minute=timeOfDay.tm_min, second=timeOfDay.tm_sec, microsecond=0).timetuple()) def getDateTime7day(timeStr, weekDay, refDate = datetime.datetime.now()): """Given a time string in 24hr format (%H:%M:%S), compute the datetime for the next given week-day at the given time. If today is already the given week-day, computes it for today (works like getDateTime24hr). weekDay is 0-6 for Mon-Sun.""" timeOfDay = time.strptime(timeStr, "%H:%M:%S") # Rollforward to the next Saturday. It is ok to do verify today at any time, as # long as today is Saturday. daysToRoll = (weekDay - refDate.weekday()) if refDate.weekday() > weekDay: daysToRoll += 7 return time.mktime((refDate + datetime.timedelta(days=daysToRoll)).replace( hour=timeOfDay.tm_hour, minute=timeOfDay.tm_min, second=timeOfDay.tm_sec, microsecond=0).timetuple()) def computeFirstRunTime(pattern): """Pattern is of the format: [weekday,]%H:%M:%S. For time format, see time.strptime()""" if pattern == None: # Run immediately. return time.time() #return 0 m = re.match(r'((?P\d),)?(?P