Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9292fa7
allow to run without an email server
Nov 30, 2012
d9c9f6d
disable smtp handler if server is not set
Nov 30, 2012
564762c
added some debug stuff
Nov 30, 2012
cf08499
first proof of concept plugin using the new features from the loop
Nov 30, 2012
4cf6093
added loadPlugin method
Dec 3, 2012
88990d2
need to retrieve list of plugins from Sg
Dec 3, 2012
e3c4dc1
retrieve plugins list from Shotgun
Dec 5, 2012
b5abd09
added getEngine method
Dec 5, 2012
565ae8d
Add getCollectionForPath method to Engine
Dec 6, 2012
6a10934
added init.d file
Dec 6, 2012
5e67b47
added autoDiscover ( on | off ) mode for plugins collection
Dec 7, 2012
203d671
added getPlugin to defer plugin loading to PluginCollection
Dec 7, 2012
38d7ebe
only load plugins if they are 'Active'
Dec 7, 2012
ff0f985
able to retrieve the status of the plugin and take some decision
Dec 7, 2012
422acfb
can load plugin on status change
Dec 7, 2012
59aecf9
able to unload plugins
Dec 10, 2012
b7ca072
moved loadPlugin onto the Engine
Dec 10, 2012
4ca3daf
iterating over plugins in a collection is safe, even if a plugin is u…
Dec 10, 2012
8e0131f
try to handle other events
Dec 10, 2012
6153057
decouple attribute and entity change callbacks
Dec 11, 2012
6f0348c
check if script exists before trying to load a plugin
Dec 11, 2012
58a6655
fixed typo
Dec 11, 2012
f8a99de
added getPlugin method to the Engine
Dec 11, 2012
63376d2
doc and removed useless lines
Dec 11, 2012
ce3cf65
added basic doc
Dec 11, 2012
6cdf2ad
added doc
Dec 11, 2012
4d742f1
moved extra stuff in extra directory
Dec 12, 2012
5483a71
minor changes
Dec 13, 2012
b3df6d3
fixed a bug preventing plugins from being unloaded
digisteph Dec 14, 2012
8f401dd
use space instead of tabs
Dec 14, 2012
d9ab909
check if file_path is in the old values before trying to use it
Jan 24, 2013
6c8f97d
added support for ssl smtp servers, add useSSL: True to your config file
Jan 24, 2013
8b82433
fixed an undefined value when smtp is on and use_ssl is not set
Feb 25, 2013
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions extra/README
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions extra/dumpEventIds.py
Original file line number Diff line number Diff line change
@@ -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() )
4 changes: 4 additions & 0 deletions extra/shotgunLoop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
base_dir=$(dirname $0)
export PYTHONPATH=${base_dir}/../shotgun_api:${PYTHONPATH} && ${base_dir}/shotgunEventDaemon.py "$@"

111 changes: 111 additions & 0 deletions extra/shotgunloop.centos.d
Original file line number Diff line number Diff line change
@@ -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 $?
154 changes: 154 additions & 0 deletions src/plugins/pluginManager.py
Original file line number Diff line number Diff line change
@@ -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']


Loading