AMI Custom Protocol Handlers and the registry

When getting into AMI stuff, is the Registry Edit essential to making it work on windows? I don’t have admin rights to edit the registry on my workstation. The IT department may squash my request so I’m making sure it’s critical before making the ask.

Hey,
It definitely was for me but its not as bad as it sounds. You only need to register the protocol once…I ended up just registering shotgun:// so all URLs that have shotgun:// as their starting protocol get redirected to my shotgun_ami_handler.py that way I don’t have to register multiple protocols and keep bothering IT.

example .reg to register the needed protocol:
Windows Registry Editor Version 5.00

    [HKEY_CLASSES_ROOT\shotgun]
    @="URL:shotgun Protocol"
    "URL Protocol"=""

    [HKEY_CLASSES_ROOT\shotgun\DefaultIcon]
    @=""

    [HKEY_CLASSES_ROOT\shotgun\shell]

    [HKEY_CLASSES_ROOT\shotgun\shell\open]

    [HKEY_CLASSES_ROOT\shotgun\shell\open\command]
    @="python @root\\shotgun\\shotgun_ami\\shotgun_ami_handler.py %1"

replace @root with whereever/whatever server you decide to have your handler.

then you can create infinite action menu items that all get filtered through that handler, and fire off different scripts/actions

Curious to see if there’s a more updated approach to this, as I implemented this workflow 2-3years ago.

-Ross

5 Likes

Great, thanks. So in the @root path, is \shotgun referencing your shotgun toolkit install location? I agree it makes sense to just setup the protocol once in the registry. My SG install is on our server. So, the path is “Z:\Motion_Design\sg”. Would I need to include any of that in the path or just \shotgun to have it point there?

1 Like

Actually you can have the absolute path be anywhere that is convenient for your pipeline. I have a SERVER://shotgun_module directory that i house all pipeline related functions specific to shotgun, its not a project but it does have a headless shotgun_api3 that I can use, i keep that sync’d with the current core version that the studio is on. However you can make it easier and put the handler at the root of your directory that houses your internal active projects as well.

I did make the path absolute so in your case it would be:

@=“python Z:\Motion_Design\sg\shotgun_ami\shotgun_ami_handler.py %1”

there absolutely might be a way to make this path relative, that’s probably something I would discuss with your IT dept, they might have a clean way to register protocols at user logon.

Hope this helps a little bit,
Let me know if this doesn’t make sense, or there are other questions,
-Ross

1 Like

That helps, thanks. I’ll put it under that sg install location on my server.

One more question. You have the " before the file path and then close it after the %1". On your first example, you had the " at the beginning of the line. Which is correct?

3 Likes

I edited my second post, the quotes do indeed need to encapsulate the full command, if you wanted to run python 3 interpreter instead you would probably want to switch that “python” to “python3” or whatever you have called your python3 interpreter .exe.

Sorry for any mixup that might have caused.
PS: this sort of setup does require python to be installed on all host machines that you want to run any AMI commands, and python will also need to be registered in the windows environment variables under PATH so that the command “python” will find a valid .exe to execute your ami_handler.py, so you would need to work this out with IT… you’re gonna be their best friend :smiley:
-Ross

3 Likes

I have the registry key in place with the correct path from your post. I have the shotgun_ami_handler.py script copied from the help docs. I set my AMI in SG to read shotgun://processversion like it also said in the docs. However, I’m not getting the output file like it said I should. I tried making the path to the output file explicit. I do get a message from the browser asking to start “Shotgun”. Not sure what I’m doing wrong.

I can’t tell from the docs where the “processversion” part comes from? Is that a SG variable or something they made up for this example?

1 Like

Hey Byron,
I assume you were looking at this:
https://support.shotgunsoftware.com/hc/en-us/articles/219031308-Launching-applications-using-custom-browser-protocols

processVersion.py is their plugin they are trying to execute with sgTriggerScript.py … what is confusing with those older docs is that the news docs will have you create a much more robust ami_handler.py… which you probably found:
http://developer.shotgunsoftware.com/python-api/examples/ami_handler.html

wow… i’m looking at my old code and it looks like I added an execute action function to their ami_handler… so by default that ami_handler just writes the action/ids out to a log file… it wants you to write some firing off functions yourself… that is stated at the top:

" This is an example ActionMenu Python class to handle the GET request sent from an ActionMenuItem. It doesn’t manage dispatching custom protocols but rather takes the arguments from any GET data and parses them into the easily accessible and correctly typed instance variables for your Python scripts."

so… inside the init i have added:

if self.action != None:
    # print self.action
    # print self.selected_ids
    self.executeAction(self.action, self.selected_ids)

and the cooresponding function:

# ----------------------------------------------
# Call Action
# ----------------------------------------------
def executeAction(self, sg_action, id_list):
    try:
        SG_AMI_ROOT = os.path.dirname(__file__)
        SG_AMI_PLUGIN_PATH = SG_AMI_ROOT + "/plugins"
        sg_action_path = SG_AMI_PLUGIN_PATH + "/%s.py" % (sg_action)
        if os.path.exists(sg_action_path):
            #args = [str(sg_action_path), str(id_list)]
            sys.argv = [sg_action_path, id_list]
            print "SG_ACTION_PATH: %s" % (str(sg_action_path))
            output = execfile(sg_action_path)
            if output != None:
                print "SG_ACTION %s Reported Output: \n %s" % (sg_action, output)
                time.sleep(120)
    except Exception:
        print "Execute %s Reported Errors: \n %s" % (sg_action_path, str(traceback.format_exc()))
        time.sleep(120)

now the way I have written this, it will look inside a folder called “plugins” and if the returned “action” from “self.action” is the same as a .py script inside the folder, it will execute it.

so if we stick to that old example… “sgTriggerScript.py” is replaced by “shotgun_ami_handler.py” and shotgun_ami_handler.py is what is found:
http://developer.shotgunsoftware.com/python-api/cookbook/examples/ami_handler.html?highlight=handling
but with the addition of an actual execution function after successfully parsing the URL

so if you did want an AMI called processVersion… you’d go ahead and add that in the Shotgun Web Interface under Admin > Action Menu Items; with the URL shotgun://processVersion and the entityType (“Version”) so that you can access from Versions…
then you would go ahead and write a processVersion.py and stick it in “plugins” folder (based on my implementation)

my folder structure looks like
image

You can read the results from the shotgun_ami_handler.py inside the corresponding “log” folder that code is present in their ami_handler example.

I hope this helps,
and if the writer of those older docs has anymore insight if I misinterpreted anything, that would help
-Ross

2 Likes

Yeah, the docs have me thoroughly confused. Thank you for your help. To clarify, you have this code here added to the script from that link and saved as shotgun_ami_handler.py? I’m trying to understand what’s in the handler vs the env file.

My AMI needs to be on the shot entity. In the end I’d like it to collect some data and write out a .bat file. Possibly have the bat file get executed but not critical.

1 Like

yep, I can’t share my whole shotgun_ami_handler.py … but yes that code snippet is at the bottom of their init.py. So if we use their posted shotgun_ami_handler.py it would look like:

----------------------------------------------

ShotgunAction Class to manage ActionMenuItem call

----------------------------------------------

class ShotgunAction():

def __init__(self, url):
    self.logger = self._init_log(logfile)
    self.url = url
    self.protocol, self.action, self.params = self._parse_url()

    # entity type that the page was displaying
    self.entity_type = self.params['entity_type']

    # Project info (if the ActionMenuItem was launched from a page not belonging
    # to a Project (Global Page, My Page, etc.), this will be blank
    if 'project_id' in self.params:
        self.project = { 'id':int(self.params['project_id']), 'name':self.params['project_name'] }
    else:
        self.project = None

    # Internal column names currently displayed on the page
    self.columns = self.params['cols']

    # Human readable names of the columns currently displayed on the page
    self.column_display_names = self.params['column_display_names']

    # All ids of the entities returned by the query (not just those visible on the page)
    self.ids = []
    if len(self.params['ids']) > 0:
        ids = self.params['ids'].split(',')
        self.ids = [int(id) for id in ids]

    # All ids of the entities returned by the query in filter format ready
    # to use in a find() query
    self.ids_filter = self._convert_ids_to_filter(self.ids)

    # ids of entities that were currently selected
    self.selected_ids = []
    if len(self.params['selected_ids']) > 0:
        sids = self.params['selected_ids'].split(',')
        self.selected_ids = [int(id) for id in sids]

    # All selected ids of the entities returned by the query in filter format ready
    # to use in a find() query
    self.selected_ids_filter = self._convert_ids_to_filter(self.selected_ids)

    # sort values for the page
    # (we don't allow no sort anymore, but not sure if there's legacy here)
    if 'sort_column' in self.params:
        self.sort = { 'column':self.params['sort_column'], 'direction':self.params['sort_direction'] }
    else:
        self.sort = None

    # title of the page
    self.title = self.params['title']

    # user info who launched the ActionMenuItem
    self.user = { 'id':self.params['user_id'], 'login':self.params['user_login']}

    # session_uuid
    self.session_uuid = self.params['session_uuid']

   //New code here:
    if self.action != None:
        # print self.action
        # print self.selected_ids
        self.executeAction(self.action, self.selected_ids)

I assume by env file you mean my shotgun_ami_set_env.py?
you don’t need that, im using it to set some environment variables that I don’t have already set that are specific to this handler…
We don’t set static environment variables in our pipeline, so all our python paths are set dynamically through various called env scripts, so that I always know what each script has access to.

I did a little temp actionmenu creation to show you what it would look like based on your thoughts of “running a bat” from a “shot” entity:

this action menu item would show up from the shot entity page, and it would look for a shotgunRunBat.py within the plugins folder (if you are using my implementation)

im sure you found the menu to add these but it is here:
image
in the shotgun web interface (if you click on your profile picture its in that list of menus)

-Ross

2 Likes

Thanks for this and I apologize for this back and forth. I’ve done a bit of scripting but this is just kicking my tail. It’s embarrassing!

I made the edits to the sg_ami_handler.py file. Created the folders and put a very simple script in plugins to write a text file for testing. The script will write the file if I run it from the file explorer. I’m getting a message that Chrome is trying to send something to the Shotgun protocol. But nothing happens. I must have messed up something in my handler script. How is the script in the plugins folder getting information from Shotgun? I’m fuzzy on that concept.

2 Likes

No worries you’re making great progress! This was not an easy task for me as well when I implemented it.

Okay so, now you want your plugin script to do something, The way I have the executeAction function setup i’m using sys.argv to pass my plugin_path, and the selectedIds (selectedIds, are going to be the specific entity items the user has selected prior to clicking your action menu item)

we can see where I am assigning this infomation here:

        if os.path.exists(sg_action_path):
            sys.argv = [sg_action_path, id_list]
            print "SG_ACTION_PATH: %s" % (str(sg_action_path))
            output = execfile(sg_action_path)

then in my plugin script i am just using init again to execute functions within my plugin’s class.

//Contents of @root//plugins//shotgunActionScript.py

> class shotgunActionScript():
>     def __init__(self, selectedIds):
>         self.mainfunction(selectedIds)
>             
>         def mainfunction(self, selectedIds):
>           //run your action code here

you can call shotgunActionScript anything, it just needs to be whatever you called it in your action menu item URL field:
shotgun://shotgunActionScript (in this example)

this explains sys.argv verse subprocess (you can use either to accomplish the goal of passing arguments from shotgun to your actions):

execfile verse subprocess

what is argv

2 Likes

That’s helpful. I see how you passed the info. I’ve edited my scripts to match what you showed. I made a simple script to write the text file. It worked until I added in the lines you sent. So, I’m not sure what happened. Does this look correct?

import datetime
class shotgunActionScript():
    def __init__(self, selectedIds):
        self.main(selectedIds)
         
        def main(self, selectedIds):
            x = datetime.datetime.now()
            #run your action code here
            f= open("SG_output.txt","w+")
            #f=open("SG_output.txt","a+")
            f.write("Output from Shotgun")
            f.write("%s\n"%x)
            f.write(selectedIds)
            for i in range(10):
                f.write("This is line %d\r\n" % (i+1))
            f.close()   

if __name__== "__main__":
    main()
2 Likes

sorry,
I have been real busy at work… I dont know if you solved this but it looks like i accidentally have an extra tab on the “main function” and you have mirrored that in your code, that isn’t correct, my apologies still trying to figure out how to format code on these forums it seems :D.

It should be:

class shotgunActionScript():
    def __init__(self, selectedIds):
        self.mainfunction(selectedIds)
            
    def mainfunction(self, selectedIds):
        //run your action code here

like a normal python class/function relationship. Sorry about that format typo :(…

2 Likes

Thanks Ross. Does your plugin script run on it’s own? When I try run either the plugin script or the main AMI handler script nothing happens. I’m not getting any logs either.

1 Like

yes and no,
inside my plugins I do have a
if name == “main”:

so that if the plugin is run outside of a package as the main execute from the command-line, or other outside interpreter initiated from a direct call to “python” then it will do something, in my case it prompts the user in the terminal to input an ID… but that is not something you need to implement if you don’t need it.

both would need to be called with some sort of input from the user as arguments… the ami_handler would look for a valid URL… that it can parse… and the way I setup my plugins they would look for IDs that would need to exist inside the shotgun database…

Hope that helps…
I did all my debugging using the logs from the ami_handler, when testing full functionality, but yes i tested all my plugins before hand using direct calls and supplying dummy IDs that existed in my testing project within shotgun.

PS: you would need to call the shotgun_ami_handler as you would a normal python class…

import shotgun_ami_handler as SAH
SAH.ShotgunAction(VALID_URL)

also remember that class is not setup to return anything… it writes to a log file, so if u were to test like that, maybe you would want to write a handler to return some information that you were debugging? So you see some instant response in the terminal
-Ross

2 Likes