diff --git a/extra/README b/extra/README new file mode 100644 index 00000000..8618da26 --- /dev/null +++ b/extra/README @@ -0,0 +1,9 @@ +shotgunloop.centos.d : A starting point to add the event loop to your /etc/init.d on CentOs +- Copy the file in /etc/init.d +- Change the settings that need to be tweaked +- Use chkconfig to set up the wanted run level +- Change the exec path to either point to the shotgunLoop.sh wrapper or directly to your shotgunEventDaemon.py + +shotgunLoop.sh : A small shell wrapper to use a local shotgun_api module +- Copy this file in the directory where shotgunEventDaemon.py is +- Tweak the paths to point to the place where your local shotgun_api is installed \ No newline at end of file diff --git a/extra/dumpEventIds.py b/extra/dumpEventIds.py new file mode 100755 index 00000000..dcdf3b1f --- /dev/null +++ b/extra/dumpEventIds.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +try: + import cPickle as pickle +except ImportError: + import pickle +import sys +import pprint + +def main( ) : + eventIdFile = sys.argv[1] + with open(eventIdFile) as fh : + eventIdData = pickle.load(fh) + pprint.pprint( eventIdData ) + return 0 + +if __name__ == '__main__': + sys.exit( main() ) diff --git a/extra/shotgunLoop.sh b/extra/shotgunLoop.sh new file mode 100755 index 00000000..3a2c5550 --- /dev/null +++ b/extra/shotgunLoop.sh @@ -0,0 +1,4 @@ +#!/bin/sh +base_dir=$(dirname $0) +export PYTHONPATH=${base_dir}/../shotgun_api:${PYTHONPATH} && ${base_dir}/shotgunEventDaemon.py "$@" + diff --git a/extra/shotgunloop.centos.d b/extra/shotgunloop.centos.d new file mode 100755 index 00000000..762be1dd --- /dev/null +++ b/extra/shotgunloop.centos.d @@ -0,0 +1,111 @@ +#!/bin/bash +# +# shotgunloop Startup script for the Shotgun Loop daemon +# +# chkconfig: - 85 15 +# description: The shotgunloop daemon parses and filters Shotgun events and fires registered callbacks +# processname: shotgunloop +# config: /etc/sysconfig/shotgunloop +# pidfile: /var/run/shotgunloop.pid +# +### BEGIN INIT INFO +# Provides:shotgunloop +# Required-Start: $local_fs $remote_fs $network +# Required-Stop: $local_fs $remote_fs $network +# Short-Description: start and stop the shotgunloop daemon +# Description: The shotgunloop daemon parses and filters Shotgun events and fires registered callbacks +### END INIT INFO + +# Source function library. +. /etc/rc.d/init.d/functions + +if [ -f /etc/sysconfig/shotgunloop ]; then + . /etc/sysconfig/shotgunloop +fi + +exec=/var/local/shotgunLoop/src/shotgunLoop.sh +prog=shotgunloop +#Please make sure that the following is in sycnh with your shotgunEventDaemon.conf +pidfile=${PIDFILE-/var/run/shotgunloop.pid} +lockfile=${LOCKFILE-/var/lock/subsys/shotgunloop.lock} +#args="--daemon --pid-file=${pidfile} $OPTIONS" +args="" +[ -e /etc/sysconfig/$prog ] && . /etc/sysconfig/$prog + +lockfile=/var/lock/subsys/$prog + +start() { + [ -x $exec ] || exit 5 + [ -f $config ] || exit 6 + echo -n $"Starting $prog: " + daemon --pidfile=${pidfile} $exec start $args + retval=$? + echo + if [ $retval -eq 0 ]; then + touch $lockfile || retval=4 + fi + return $retval +} + +stop() { + echo -n $"Stopping $prog: " + killproc -p ${pidfile} $prog + retval=$? + echo + [ $retval -eq 0 ] && rm -f $lockfile + return $retval +} + +restart() { + stop + start +} + +reload() { + restart +} + +force_reload() { + restart +} + +rh_status() { + # run checks to determine if the service is running or use generic status + status -p ${pidfile} $prog +} + +rh_status_q() { + rh_status >/dev/null 2>&1 +} + +case "$1" in + start) + rh_status_q && exit 0 + $1 + ;; + stop) + rh_status_q || exit 0 + $1 + ;; + restart) + $1 + ;; + reload) + rh_status_q || exit 7 + $1 + ;; + force-reload) + force_reload + ;; + status) + rh_status + ;; + condrestart|try-restart) + rh_status_q || exit 0 + restart + ;; + *) + echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload}" + exit 2 +esac +exit $? diff --git a/src/plugins/pluginManager.py b/src/plugins/pluginManager.py new file mode 100644 index 00000000..52d86d6b --- /dev/null +++ b/src/plugins/pluginManager.py @@ -0,0 +1,154 @@ +""" +For detailed information about the event loop, please see + +http://shotgunsoftware.github.com/shotgunEvents/api.html + +This plugin allows to control plugins from Shotgun. +To use it : +- Enable a Custom Non Project Entity in Shotgun, rename it to Plugins ( or whatever name you fancy ). +- Change the status field to only accept 'Active' and 'Disabled' status +- Add a 'Script Path' File/Link field to the entity, to control where a plugin script will be. +- Add a 'Ignore Projects' multi entity Project field to the entity, to control the list of projects where a plugin shouldn't be active. +- Edit your shotgunEventDaemon.conf file, and add the section : + [pluginManager] + sgEntity : CustomNonProjectEntity15 # the entity you enabled + script_key = ??????? # The Shotgun script key to use by the pluginManager plugin + script_name = ?????? # The Shotgun script name to use by the pluginManager plugin +- Copy this file in a place where your shotgunEventDaemon.py script can find it +- You will have to create Local File storage for places where you want to release your plugins +""" + +import logging +import os +import shotgun_api3 as sg +import re +import sys + +def registerCallbacks(reg): + """ + Register attribute and entity changes callbacks for plugins registered in Shotgun. + Load plugins registered in Shotgun + """ + + reg.logger.debug('Loading pluginManager plugin.') + + # Retrieve config values + my_name = reg.getName() + cfg = reg.getConfig() + if not cfg : + raise ConfigError( "No config file found") + reg.logger.debug( "Loading config for %s" % reg.getName() ) + settings = {} + keys = [ 'sgEntity', 'script_key', 'script_name'] + for k in keys : + settings[k] = cfg.get( my_name, k ) + reg.logger.debug( "Using %s %s" % ( k, settings[k] ) ) + # We will need access to the Engine from callbacks + settings['engine'] = reg.getEngine() + + # Register all callbacks related to our custom entity + + # Attribute change callback + eventFilter = { r'Shotgun_%s_Change' % settings['sgEntity'] : ['sg_status_list', 'sg_script_path', 'sg_ignore_projects' ] } + reg.logger.debug("Registring %s", eventFilter ) + reg.registerCallback( settings['script_name'], settings['script_key'], changeEventCB, eventFilter, settings ) + + # Entity change callbacks + eventFilter = { + r'Shotgun_%s_New' % settings['sgEntity'] : None, + r'Shotgun_%s_Retirement' % settings['sgEntity'] : None, + r'Shotgun_%s_Revival' % settings['sgEntity'] : None + } + reg.logger.debug("Registring %s", eventFilter ) + reg.registerCallback( settings['script_name'], settings['script_key'], entityEventCB, eventFilter, settings ) + + # Get a list of all the existing plugins from Shotgun + sgHandle = sg.Shotgun( reg.getEngine().config.getShotgunURL(), settings['script_name'], settings['script_key'] ) + plugins = sgHandle.find( settings['sgEntity'], [], ['sg_script_path', 'sg_status_list', 'sg_ignore_projects'] ) + reg.logger.debug( "Plugins : %s", plugins ) + for p in plugins : + if p['sg_script_path'] and p['sg_script_path']['local_path'] and p['sg_status_list'] == 'act' and os.path.isfile( p['sg_script_path']['local_path'] ) : + reg.logger.info( "Loading %s", p['sg_script_path']['name'] ) + pl = reg.getEngine().loadPlugin( p['sg_script_path']['local_path'], autoDiscover=False ) + pl._pm_ignore_projects = p['sg_ignore_projects'] + + #reg.logger.setLevel(logging.ERROR) + +def changeEventCB(sg, logger, event, args): + """ + A callback that treats plugins attributes changes. + + @param sg: Shotgun instance. + @param logger: A preconfigured Python logging.Logger object + @param event: A Shotgun event. + @param args: The args passed in at the registerCallback call. + """ + logger.debug("%s" % str(event)) + etype = event['event_type'] + attribute = event['attribute_name'] + entity = event['entity'] + if attribute == 'sg_status_list' : + logger.info( "Status changed for %s", entity['name'] ) + # We need some details to know what to do + p = sg.find_one( entity['type'], [[ 'id', 'is', entity['id']]], ['sg_script_path', 'sg_ignore_projects'] ) + if p['sg_script_path'] and p['sg_script_path']['local_path'] and os.path.isfile( p['sg_script_path']['local_path'] ) : + if event['meta']['new_value'] == 'act' : + logger.info('Loading %s', p['sg_script_path']['name']) + pl = args['engine'].loadPlugin( p['sg_script_path']['local_path'], autoDiscover=False) + pl._pm_ignore_projects = p['sg_ignore_projects'] + else : #Disable the plugin + logger.info('Unloading %s', p['sg_script_path']['name']) + args['engine'].unloadPlugin( p['sg_script_path']['local_path']) + elif attribute == 'sg_script_path' : # Should unload and reload the plugin + logger.info( "Script path changed for %s", entity['name'] ) + # We need some details to know what to do + p = sg.find_one( entity['type'], [[ 'id', 'is', entity['id']]], ['sg_status_list', 'sg_script_path', 'sg_ignore_projects'] ) + old_val = event['meta']['old_value'] + # Unload the plugin if loaded + if old_val and 'file_path' in old_val : # Couldn't be loaded if empty or None + file_path = old_val['file_path'] # This is not the full path, it is local to the storage + # We need to rebuild the old path + local_path = { 'darwin' : 'mac_path', 'win32' : 'windows_path', 'linux' : 'linux_path', 'linux2' : 'linux_path' }[ sys.platform] + st = sg.find_one( 'LocalStorage', [[ 'id', 'is', old_val['local_storage_id'] ]], [local_path ] ) + path = os.path.join( st[ local_path], file_path ) + logger.info('Unloading %s', os.path.basename( path )) + args['engine'].unloadPlugin( path ) + # Reload the plugin if possible + if p['sg_script_path'] and p['sg_script_path']['local_path'] and p['sg_status_list'] == 'act' and os.path.isfile( p['sg_script_path']['local_path'] ) : + logger.info('Loading %s', p['sg_script_path']['name']) + pl = args['engine'].loadPlugin( p['sg_script_path']['local_path'], autoDiscover=False) + pl._pm_ignore_projects = p['sg_ignore_projects'] + elif attribute == 'sg_ignore_projects' : + logger.info( "'Ignore projects' changed for %s", entity['name'] ) + p = sg.find_one( entity['type'], [[ 'id', 'is', entity['id']]], ['sg_status_list', 'sg_script_path', 'sg_ignore_projects'] ) + if p['sg_script_path'] and p['sg_script_path']['local_path'] : + pl = args['engine'].getPlugin( p['sg_script_path']['local_path'] ) + if pl : + pl._pm_ignore_projects = p['sg_ignore_projects'] + +def entityEventCB(sg, logger, event, args): + """ + A callback that treat plugins entities changes + + @param sg: Shotgun instance. + @param logger: A preconfigured Python logging.Logger object + @param event: A Shotgun event. + @param args: The args passed in at the registerCallback call. + """ + logger.debug("%s" % str(event)) + etype = event['event_type'] + attribute = event['attribute_name'] + meta = event['meta'] + if re.search( 'Retirement$', etype ) : # Unload the plugin + p = sg.find_one( meta['entity_type'], [[ 'id', 'is', meta['entity_id']]], ['sg_script_path'], retired_only=True ) + if p['sg_script_path'] and p['sg_script_path']['local_path'] : + logger.info('Unloading %s', p['sg_script_path']['name']) + args['engine'].unloadPlugin( p['sg_script_path']['local_path']) + elif re.search( 'Revival$', etype ) or re.search( 'New$', etype ): #Should reload the plugin + p = sg.find_one( meta['entity_type'], [[ 'id', 'is', meta['entity_id']]], ['sg_script_path', 'sg_status_list', 'sg_ignore_projects'] ) + if p['sg_script_path'] and p['sg_script_path']['local_path'] and p['sg_status_list'] == 'act' and os.path.isfile( p['sg_script_path']['local_path'] ) : + logger.info('Loading %s', p['sg_script_path']['name']) + pl = args['engine'].loadPlugin( p['sg_script_path']['local_path'], autoDiscover=False) + pl._pm_ignore_projects = p['sg_ignore_projects'] + + diff --git a/src/shotgunEventDaemon.py b/src/shotgunEventDaemon.py index ef968e68..587163b5 100755 --- a/src/shotgunEventDaemon.py +++ b/src/shotgunEventDaemon.py @@ -99,7 +99,7 @@ def _removeHandlersFromLogger(logger, handlerTypes=None): logger.removeHandler(handler) -def _addMailHandlerToLogger(logger, smtpServer, fromAddr, toAddrs, emailSubject, username=None, password=None, secure=None): +def _addMailHandlerToLogger(logger, smtpServer, fromAddr, toAddrs, emailSubject, username=None, password=None, secure=None, use_ssl=None): """ Configure a logger with a handler that sends emails to specified addresses. @@ -115,7 +115,7 @@ def _addMailHandlerToLogger(logger, smtpServer, fromAddr, toAddrs, emailSubject, SMTPHandler. """ if smtpServer and fromAddr and toAddrs and emailSubject: - mailHandler = CustomSMTPHandler(smtpServer, fromAddr, toAddrs, emailSubject, (username, password), secure) + mailHandler = CustomSMTPHandler(smtpServer, fromAddr, toAddrs, emailSubject, (username, password), secure, use_ssl) mailHandler.setLevel(logging.ERROR) mailFormatter = logging.Formatter(EMAIL_FORMAT_STRING) mailHandler.setFormatter(mailFormatter) @@ -147,7 +147,9 @@ def getPluginPaths(self): return [s.strip() for s in self.get('plugins', 'paths').split(',')] def getSMTPServer(self): - return self.get('emails', 'server') + if self.has_option('emails', 'server'): + return self.get('emails', 'server') + return None def getSMTPPort(self): if self.has_option('emails', 'port'): @@ -178,6 +180,11 @@ def getSecureSMTP(self): return self.getboolean('emails', 'useTLS') or False return False + def getUseSSL(self): + if self.has_option('emails', 'useSSL'): + return self.getboolean('emails', 'useSSL') or False + return False + def getLogMode(self): return self.getint('daemon', 'logMode') @@ -277,6 +284,8 @@ def setEmailsOnLogger(self, logger, emails): return smtpServer = self.config.getSMTPServer() + if not smtpServer: + return smtpPort = self.config.getSMTPPort() fromAddr = self.config.getFromAddr() emailSubject = self.config.getEmailSubject() @@ -286,7 +295,12 @@ def setEmailsOnLogger(self, logger, emails): secure = (None, None) else: secure = None - + + if self.config.getUseSSL() : + use_ssl = True + else : + use_ssl= None + if emails is True: toAddrs = self.config.getToAddrs() elif isinstance(emails, (list, tuple)): @@ -295,8 +309,68 @@ def setEmailsOnLogger(self, logger, emails): msg = 'Argument emails should be True to use the default addresses, False to not send any emails or a list of recipient addresses. Got %s.' raise ValueError(msg % type(emails)) - _addMailHandlerToLogger(logger, (smtpServer, smtpPort), fromAddr, toAddrs, emailSubject, username, password, secure) + _addMailHandlerToLogger(logger, (smtpServer, smtpPort), fromAddr, toAddrs, emailSubject, username, password, secure, use_ssl) + + def getCollectionForPath( self, path, autoDiscover=True, ensureExists=True ) : + """ + Return a plugin collection to handle the given path + @param path : The path to return a collection for + @param autoDiscover : Should the collection check for new plugin and load them + @param ensureExists : Create the collection if it does not exist + @return: A collection that will handle the path or None. + @rtype: L{PluginCollection} + """ + # Check if we already have a plugin collection covering the directory path + for pc in self._pluginCollections : + if pc.path == path : + return pc + else : + if not ensureExists : + return None + # Need to create a new plugin collection + self._pluginCollections.append( PluginCollection(self, path) ) + pc = self._pluginCollections[-1] + pc._autoDiscover = autoDiscover + return pc + + + def loadPlugin( self, path, autoDiscover=True ) : + """ + Load the given plugin into the Engine + @param path : Full path to the plugin Python script + @param autoDiscover: Wether or not the collection should automatically discover new plugins + """ + # Check that everything looks right + if not os.path.isfile(path) : + raise ValueError( "%s is not a valid file path" % path ) + return self.getPlugin( path, ensureExists=True, autoDiscover=autoDiscover ) + def unloadPlugin( self, path ) : + """ + Unload the plugin with the given path + """ + # Get the collection for this path, if any + ( dir, file ) = os.path.split( path ) + pc = self.getCollectionForPath( dir, ensureExists=False ) + if pc : + self.log.debug("Unloading %s from %s", file, pc.path ) + pc.unloadPlugin( file ) + + def getPlugin( self , path, ensureExists=False, autoDiscover=False ) : + """ + Return the plugin with the given path if loaded in the Engine + If ensureExists is True, make sure the plugin is loaded if not already + @param path : Full path to the wanted plugin + @param ensureExists : Wether or not the plugin should be loaded if not already + @param autoDiscover : if a new Collection is created, wheter or not it should automatically check for new scripts + """ + ( dir, file ) = os.path.split( path ) + pc = self.getCollectionForPath( dir, ensureExists=ensureExists, autoDiscover=autoDiscover ) + p = None + if pc : + p = pc.getPlugin( file, ensureExists=ensureExists ) + return p + def _run(self): """ Start the processing of events. @@ -309,7 +383,6 @@ def _run(self): # Notify which version of shotgun api we are using self.log.info('Using Shotgun version %s' % sg.__version__) - try: for collection in self._pluginCollections: collection.load() @@ -412,6 +485,7 @@ def _mainLoop(self): while self._continue: # Process events for event in self._getNewEvents(): + self.log.debug( "Processing %s", event['id'] ) for collection in self._pluginCollections: collection.process(event) self._saveEventIdData() @@ -421,7 +495,7 @@ def _mainLoop(self): # Reload plugins for collection in self._pluginCollections: collection.load() - + # Make sure that newly loaded events have proper state. self._loadEventIdData() @@ -450,6 +524,7 @@ def _getNewEvents(self): conn_attempts = 0 while True: try: + self.log.debug("Checking events from %d", nextEventId ) return self._sg.find("EventLogEntry", filters, fields, order, limit=self.config.getMaxEventBatchSize()) if events: self.log.debug('Got %d events: %d to %d.', len(events), events[0]['id'], events[-1]['id']) @@ -496,6 +571,23 @@ def _checkConnectionAttempts(self, conn_attempts, msg): self.log.warning('Unable to connect to Shotgun (attempt %s of %s): %s', conn_attempts, self._max_conn_retries, msg) return conn_attempts + def _runSingleEvent( self, eventId ) : + # Setup the stdout logger + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(message)s")) + logging.getLogger().addHandler(handler) + # Retrieve the event + fields = ['id', 'event_type', 'attribute_name', 'meta', 'entity', 'user', 'project', 'session_uuid'] + result = self._sg.find_one("EventLogEntry", filters=[['id', 'is', eventId]], fields=fields ) + if not result : + raise ValueError("Couldn't find event %d" % eventId) + for collection in self._pluginCollections: + collection.load() + #Process the event + self.log.info( "Treating event %d", result['id']) + for collection in self._pluginCollections: + collection.process( result, forceEvent=True ) + class PluginCollection(object): """ @@ -507,6 +599,7 @@ def __init__(self, engine, path): self._engine = engine self.path = path + self._autoDiscover = True # Wether or not new plugins should automatically be discovered and loaded self._plugins = {} self._stateData = {} @@ -538,14 +631,15 @@ def getNextUnprocessedEventId(self): eId = newId return eId - def process(self, event): + def process(self, event, forceEvent=False ): for plugin in self: if plugin.isActive(): - plugin.process(event) + plugin.logger.debug( "Checking event %d", event['id']) + plugin.process(event, forceEvent ) else: plugin.logger.debug('Skipping: inactive.') - def load(self): + def load(self, configPath=None ): """ Load plugins from disk. @@ -563,16 +657,44 @@ def load(self): if basename in self._plugins: newPlugins[basename] = self._plugins[basename] - else: + elif self._autoDiscover : newPlugins[basename] = Plugin(self._engine, os.path.join(self.path, basename)) - newPlugins[basename].load() + if basename in newPlugins : + newPlugins[basename].load() + #TODO : report something when plugins are gone missing self._plugins = newPlugins + def getPlugin( self, file, ensureExists=True ) : + """ + Return the plugin from this collection. + If ensureExists is True, ensure it is loaded in this collection. + @param file : short name of the plugin to retrieve from this collection + @type file : I{str} + @param ensureExists : wether or not the plugin should be loaded if not already + @rtype : L{Plugin} or None + """ + if file not in self._plugins : # Plugin is not already loaded + if ensureExists : + self._plugins[file] = Plugin( self._engine, os.path.join( self.path, file)) + self._plugins[file].load() + else : + return None + return self._plugins[file] + + def unloadPlugin( self, file ) : + """ + Unload the given plugin + @param file : short name of the plugin to unload from this collection + """ + if file in self._plugins : + self._plugins.pop( file ) + def __iter__(self): for basename in sorted(self._plugins.keys()): - yield self._plugins[basename] + if basename in self._plugins : # handle the case where plugins were removed while looping + yield self._plugins[basename] class Plugin(object): @@ -591,7 +713,7 @@ def __init__(self, engine, path): """ self._engine = engine self._path = path - + self._configPath = _getConfigPath() if not os.path.isfile(path): raise ValueError('The path to the plugin is not a valid file - %s.' % path) @@ -659,7 +781,7 @@ def setEmails(self, *emails): """ self._engine.setEmailsOnLogger(self.logger, emails) - def load(self): + def load(self ): """ Load/Reload the plugin and all its callbacks. @@ -701,7 +823,7 @@ def load(self): regFunc = getattr(plugin, 'registerCallbacks', None) if callable(regFunc): try: - regFunc(Registrar(self)) + regFunc(Registrar(self, configPath = self._configPath )) except: self._engine.log.critical('Error running register callback function from plugin at %s.\n\n%s', self._path, traceback.format_exc()) self._active = False @@ -717,7 +839,13 @@ def registerCallback(self, sgScriptName, sgScriptKey, callback, matchEvents=None sgConnection = sg.Shotgun(self._engine.config.getShotgunURL(), sgScriptName, sgScriptKey) self._callbacks.append(Callback(callback, self, self._engine, sgConnection, matchEvents, args)) - def process(self, event): + def process(self, event, forceEvent=False ): + self.logger.debug( "Processing %s", event['id'] ) + + if forceEvent : # Perform a raw process of the event + self._process( event ) + return self._active + if event['id'] in self._backlog: if self._process(event): self.logger.info('Processed id %d from backlog.' % event['id']) @@ -777,12 +905,13 @@ class Registrar(object): """ See public API docs in docs folder. """ - def __init__(self, plugin): + def __init__(self, plugin, configPath=None): """ Wrap a plugin so it can be passed to a user. """ self._plugin = plugin self._allowed = ['logger', 'setEmails', 'registerCallback'] + self._configPath = configPath # Give the plugin the opportunity to read values from the config file def getLogger(self): """ @@ -794,6 +923,33 @@ def getLogger(self): # TODO: Fix this ugly protected member access return self.logger + def getConfig( self ): + """ + Return a config parser for this plugin + @return: A config parser for this plugin or None + @rtype: L{ConfigParser.ConfigParser} + """ + if self._configPath : + cfg = ConfigParser.ConfigParser() + cfg.read( self._configPath) + return cfg + return None + + def getEngine( self ) : + """ + Return the engine for this plugin + @return : The engine for this plugin + @rtype : L{Engine} + """ + return self._plugin._engine + def getName( self ) : + """ + Return the name of this plugin + @return: The name of this plugin + @rtype: I{str} + """ + return self._plugin.getName() + def __getattr__(self, name): if name in self._allowed: return getattr(self._plugin, name) @@ -857,6 +1013,7 @@ def canProcess(self, event): else: eventType = event['event_type'] if eventType not in self._matchEvents: + self._logger.debug('Rejecting %s not in %s', eventType, self._matchEvents) return False attributes = self._matchEvents[eventType] @@ -866,7 +1023,7 @@ def canProcess(self, event): if event['attribute_name'] and event['attribute_name'] in attributes: return True - + self._logger.debug('Rejecting %s not in %s', event['attribute_name'], attributes ) return False def process(self, event): @@ -930,7 +1087,7 @@ class CustomSMTPHandler(logging.handlers.SMTPHandler): logging.CRITICAL: 'CRITICAL - Shotgun event daemon.', } - def __init__(self, smtpServer, fromAddr, toAddrs, emailSubject, credentials=None, secure=None): + def __init__(self, smtpServer, fromAddr, toAddrs, emailSubject, credentials=None, secure=None, use_ssl=None): args = [smtpServer, fromAddr, toAddrs, emailSubject] if credentials: # Python 2.6 implemented the credentials argument @@ -943,11 +1100,18 @@ def __init__(self, smtpServer, fromAddr, toAddrs, emailSubject, credentials=None self.username = None # Python 2.7 implemented the secure argument + + # Could be wrong here, but I think this is not used at all + # emit is redefined and open its own connection + # so the one opened up by the handler is simply ignored ? S.D. if CURRENT_PYTHON_VERSION >= PYTHON_27: args.append(secure) else: self.secure = secure - + if use_ssl : + self.use_ssl = True + else : + self.use_ssl = None logging.handlers.SMTPHandler.__init__(self, *args) def getSubject(self, record): @@ -977,8 +1141,11 @@ def emit(self, record): port = self.mailport if not port: port = smtplib.SMTP_PORT - smtp = smtplib.SMTP() - smtp.connect(self.mailhost, port) + if self.use_ssl is not None : + smtp = smtplib.SMTP_SSL(self.mailhost, port) + smtp.ehlo() + else : + smtp = smtplib.SMTP(self.mailhost, port) msg = self.format(record) msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\n\r\n%s" % ( self.fromaddr, @@ -986,7 +1153,7 @@ def emit(self, record): self.getSubject(record), formatdate(), msg) if self.username: - if self.secure is not None: + if self.secure is not None and self.use_ssl is None : smtp.ehlo() smtp.starttls(*self.secure) smtp.ehlo() @@ -1016,15 +1183,24 @@ def main(): # Find the function to call on the daemon action = sys.argv[1] - func = getattr(daemon, action, None) - - # If no function was found, report error. - if action[:1] == '_' or func is None: - print "Unknown command: %s" % action - return 2 - - # Call the requested function - func() + # Special case where we give an integer on the command line + # Just treat this event + try : + eid = int( action ) + except ValueError : # Not an int + func = getattr(daemon, action, None) + + # If no function was found, report error. + if action[:1] == '_' or func is None: + print "Unknown command: %s" % action + return 2 + + # Call the requested function + func() + else : + print "Processing single event %d" % eid + daemon._runSingleEvent( eid ) + return 0 else: print "usage: %s start|stop|restart|foreground" % sys.argv[0] return 2