Launching applications using custom browser protocols

A very practical version of an Action Menu Item (AMI) is a variant that will run an application or a script on your local computer. For this to work you need to set up a connection between your browser and the script or application you wish to run. This link is called a custom browser protocol.

You may want to set up a type of link where if a user clicks on it, it will launch the [foo] application. Instead of having ‘http’ as the prefix, you need to designate a custom protocol, such as ‘foo’. Ideally you want a link that looks like:
foo://some/info/here.

The operating system has to be informed how to handle protocols. By default, all of the current operating systems know that ‘http’ should be handled by the default web browser, and ‘mailto’ should be handled by the default mail client. Sometimes when applications are installed, they register with the OS and tell it to launch the applications for a specific protocol.

As an example, if you install RV, the application registers rvlink:// with the OS and tells it that RV will handle all rvlink:// protocol requests to show an image or sequence in RV. So when a user clicks on a link that starts with rvlink://, as you can do in Shotgun, the operating system will know to launch RV with the link and the application will parse the link and know how to handle it.

See the RV User Manual for more information about how RV can act as a protocol handler for URLs and the “rvlink” protocol.

Registering a protocol

Registering a protocol on Windows

On Windows, registering protocol handlers involves modifying the Windows Registry. Here is a generic example of what you want the registry key to look like:

HKEY_CLASSES_ROOT
foo
(Default) = "URL:foo Protocol"
URL Protocol = ""
shell
open
command (Default) = "foo_path" "%1"

The target URL would look like:

foo://host/path...
Note: For more information, please see http://msdn.microsoft.com/en-us/library/aa767914(VS.85).aspx.

Windows QT/QSetting example

If the application you are developing is written using the QT (or PyQT / PySide) framework, you can leverage the QSetting object to manage the creation of the registry keys for you.

This is what the code looks like to automatically have the application set up the registry keys:

// cmdLine points to the foo path.
//Add foo to the Os protocols and set foobar to handle the protocol
QSettings fooKey("HKEY_CLASSES_ROOT\\foo", QSettings::NativeFormat);
mxKey.setValue(".", "URL:foo Protocol");
mxKey.setValue("URL Protocol", "");
QSettings fooOpenKey("HKEY_CLASSES_ROOT\\foo\\shell\\open\\command", QSettings::NativeFormat);
mxOpenKey.setValue(".", cmdLine);

Windows example that starts a Python script via a Shotgun AMI

A lot of AMIs that run locally may opt to start a simple Python script via the Python interpreter. This allows you to run simple scripts or even apps with GUIs (PyQT, PySide or your GUI framework of choice). Let’s look at a practical example that should get you started in this direction.

Step 1: Set up the custom “shotgun” protocol

Using Windows Registry Editor:

[HKEY_CLASSES_ROOT\shotgun]
@="URL:shotgun Protocol"
"URL Protocol"=""
[HKEY_CLASSES_ROOT\shotgun\shell]
[HKEY_CLASSES_ROOT\shotgun\shell\open]
[HKEY_CLASSES_ROOT\shotgun\shell\open\command]
@="\"python\" \"sgTriggerScript.py\" \"%1\""

This setup will register the shotgun:// protocol to launch the python interpreter with the first argument being the script sgTriggerScript.py and the second argument being %1. It is important to understand that %1 will be replaced by the URL that was clicked in the browser or the URL of the AMI that was invoked. This will become the first argument to your Python script.

Note: You may need to have full paths to your Python interpreter and your Python script. Please adjust accordingly.

Step 2: Parse the incoming URL in your Python script

In your script you will take the first argument that was provided, the URL, and parse it down to its components in order to understand the context in which the AMI was invoked. We’ve provided some simple scaffolding that shows how to do this in the following code.

Python script

import sys
import urlparse
import pprint


def main(args):
    # Make sure we have only one arg, the URL
    if len(args) != 1:
        return 1

    # Parse the URL:
    protocol, fullPath = args[0].split(":", 1)
    path, fullArgs = fullPath.split("?", 1)
    action = path.strip("/")
    args = fullArgs.split("&")
    params = urlparse.parse_qs(fullArgs)

    # This is where you can do something productive based on the params and the
    # action value in the URL. For now we'll just print out the contents of the
    # parsed URL.
    fh = open('output.txt', 'w')
    fh.write(pprint.pformat((action, params)))
    fh.close()



if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))

Step 3: Connect the Shotgun interface with your custom protocol and ultimately, your script

Finally, create an AMI in Shotgun whose URL value will be shotgun://processVersion. You can assign this AMI to any entity type you wish, but this example uses the Version entity.

Go to a Version page, right-click on a version and select your AMI from the menu. This should make your browser open a shotgun:// URL which will be redirected to your script via the registered custom protocol.

In the output.txt file in the same directory as your script you should now see something like this:

('processVersion',
 {'cols': ['code',
           'image',
           'entity',
           'sg_status_list',
           'user',
           'description',
           'created_at'],
  'column_display_names': ['Version Name',
                           'Thumbnail',
                           'Link',
                           'Status',
                           'Artist',
                           'Description',
                           'Date Created'],
  'entity_type': ['Version'],
  'ids': ['6933,6934,6935'],
  'page_id': ['4606'],
  'project_id': ['86'],
  'project_name': ['Test'],
  'referrer_path': ['/detail/HumanUser/24'],
  'selected_ids': ['6934'],
  'server_hostname': ['patrick.shotgunstudio.com'],
  'session_uuid': ['9676a296-7e16-11e7-8758-0242ac110004'],
  'sort_column': ['created_at'],
  'sort_direction': ['asc'],
  'user_id': ['24'],
  'user_login': ['shotgun_admin'],
  'view': ['Default']})

Possible variants

By varying the keyword after the // part of the URL in your AMI, you can change the contents of the action variable in your script, all the while keeping the same shotgun:// protocol and registering only a single custom protocol. Then, based on the content of the action variable and the contents of the parameters, your script can understand what the intended behavior should be.

Using this methodology you could open applications, upload content via services like FTP, archive data, send email, or generate PDF reports.

Registering a protocol on OSX

To register a protocol on OSX you need to create a .app bundle that is configured to run your application or script.

Start by writing the following script in the AppleScript Script Editor:

on open location this_URL
    do shell script "sgTriggerScript.py '" & this_URL & "'"
end open location 
Pro tip: To ensure you are running Python from a specific shell, such as tcsh, you can change the do shell script for something like the following:
    do shell script "tcsh -c \"sgTriggerScript.py '" & this_URL & "'\""

In the Script Editor, save your short script as an “Application Bundle”.

Find the saved Application Bundle, and Open Contents. Then, open the info.plist file and add the following:

CFBundleIdentifier
com.mycompany.AppleScript.Shotgun
CFBundleURLTypes

 
   CFBundleURLName
   Shotgun
   CFBundleURLSchemes
   
     shotgun
   
 

You may want to change the following three strings:

com.mycompany.AppleScript.Shotgun
Shotgun
shotgun

The third string is the protocol handler; therefore a URL would be:
shotgun://something

Finally, move your .app bundle to the Applications folder of your Mac. The data flow looks like this: once you click the AMI in Shotgun, or click a URL that starts with shotgun://, the .app bundle will respond to it and pass the URL over to your Python script. At this point the same script that was used in the Windows example can be used and all the same possibilities apply.

Registering a protocol on Linux

Use the following code:

gconftool-2 -t string -s /desktop/gnome/url-handlers/foo/command 'foo "%s"'
gconftool-2 -s /desktop/gnome/url-handlers/foo/needs_terminal false -t bool
gconftool-2 -s /desktop/gnome/url-handlers/foo/enabled true -t bool

Then use the settings from your local GConf file in the global defaults in:
/etc/gconf/gconf.xml.defaults/%gconf-tree.xml

Even though the change is only in the GNOME settings, it also works for KDE. Firefox and GNU IceCat defer to gnome-open regardless of what window manager you are running when it encounters a prefix it doesn’t understand (such as foo://). So, other browsers, like Konqueror in KDE, won’t work under this scenario.

See http://askubuntu.com/questions/527166/how-to-set-subl-protocol-handler-with-unity for more information on setting up protocol handlers for Action Menu Items in Ubuntu.

Follow

21 Comments

  • 0
    Avatar
    Hugh Macdonald

    That looks pretty cool - will have to have a play with this one!

     

    Any chance of some notes on how to register a new protocol under OSX?

  • 0
    Avatar
    Don Parker

    Note for all, Hugh got this working on OS X and created a forum post with details here:  https://support.shotgunsoftware.com/entries/127152

    Thanks Hugh!

  • 0
    Avatar
    Scott Lowe

    I'm new to both Python and Shotgun. I understand that on the last line of the windows reg key:

    @="\"python\" \"sgTriggerScript.py\" \"%1\""

    ...the "sgTriggerScript.py" is the doc that Python will attempt to run when Python is opened. Is there a specific location that the sgTriggerScript doc needs to be? I'm not very good at setting enviornmental variables, etc in Python yet.

     

    Python does open momentarily, but gives me this error:

    "Python.exe: can't open file 'sgTriggerScript.py': [Errno 2] No such file or directory"

     

    Any hints would help a lot! Thanks.

  • 0
    Avatar
    Hugh Macdonald

    I would suggest that you probably want to define the full path to sgTriggerScript.py in the reg key...

     

    Something like:

     

    @="\"python\" \"C:\\tech\\python\\sgTriggerScript.py\" \"%1\""

     

    And this is one of those situations where I really hate Windows.... That string will be parsed a couple of times... Each time, '\' will be replaced with '\', which is why we need 4 \s in there to end up with just one.

  • 0
    Avatar
    Scott Lowe

    Thanks for the quick reply. I'm not getting any more errors. But something is still not working...

    I wonder if this is really a question for the Python forms, but maybe it will help someone else like me.

     

    I wrote a script that I was hoping would dump a log file, but nothing seems to happen. Here's the sgTriggerScript I'm using:

     

    def main(script, plate):

        import logging

        LOG_FILENAME = 'mylog.out'

        logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG)

        logging.debug(script, plate)

    if __name__ == '__main__':

            main(*sys.argv)

    _ def main(script, plate):_

    import logging

    LOG_FILENAME = 'mylog.out'

    logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG)

    logging.debug(script, plate)

    _

    if __name__ == '__main__': _

    main(*sys.argv)

     

    Again, any hints would be very helpful. =)

  • 0
    Avatar
    Scott Lowe

    Ah! Excuse me. Please ignore the italicized code in my previous post.

  • 0
    Avatar
    Hugh Macdonald

    If you run from a command prompt:

        python sgTriggerScript.py

    Does it  do the right thing and write something out to the log file?

     

    Oh, you might want to specify the full path to the log file too - otherwise it'll end up wherever the script is run from, which could be anywhere.

  • 0
    Avatar
    Scott Lowe

    Success! I was able to get a log file to generate from both the command line AND a custom menu item.

    Here is the command:

    C:\Python26>python C:\pyTest\sgTriggerScript.py

     

    Here is the reg key I used for handling the custom protocal:

    [HKEY_CLASSES_ROOT\shotgun]

    @="URL:shotgun Protocol"

    "URL Protocol"=""

    [HKEY_CLASSES_ROOT\shotgun\shell]

    [HKEY_CLASSES_ROOT\shotgun\shell\open]

    [HKEY_CLASSES_ROOT\shotgun\shell\open\command]

    @="\"C:\\Python26\\python\" \"C:\\pyTest\\sgTriggerScript.py\" \"%1\""

     

    And, here is the Python script:

     

    import sys

    def main(script):

        import logging

        LOG_FILENAME = 'C:\Python26\mylog.out'

        logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG)

        logging.debug(script)

    if __name__ == '__main__':

            main(sys.argv[0])

    import sys

    def main(script):

    import logging

    LOG_FILENAME = 'C:\pyTest\mylog.out'

    logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG)

    logging.debug(script)

    if __name__ == '__main__':

    main(sys.argv[0])

     

    Unfortunately, I still don't think I've grasped how the action menu item is actually passing data to sys.argv. The log file doesn't seem to generate when I use "main(*sys.argv)". It only works with "main(sys,argv[0])". @Huge Thanks for walking me through this, BTW. It is a huge help.

  • 0
    Avatar
    Scott Lowe

    (I'm sorry, I don't know why it the code I copy into the comment gets posted twice.)

  • 0
    Avatar
    Prabu Kasireddy

    Has anybody faced problems when the number of rows in the entity exceeds a certain limit?

    We have an entity that has around 250 records.

    The custom protocol was working fine (executes a python script) until 70 records (atleast from what I remember)

    Now with 250+ records, when we try to select the Gear menu option, nothing shows up.

    However, when I apply a filter and have less records, the script executes.

     

    My guess is that the custom protocol (URL) is getting too lengthy.

     

    Also, I have tried changing the option in the status bar to show only 25 records per page and it still does not work, until I apply a filter.

    Any solutions or workarounds are appreciated.

  • 0
    Avatar
    Rob Blau

    I've seen that as well.  I think it is because the ids of ALL matching entities gets passed on through.  We use the full ID list in the case when selected ids is empty, but that is the only time.  It would be nice if the setup behaved better when custom menu items are used on a page where a LOT of entities match the filter.

    -r

  • 0
    Avatar
    Isaac Reuben

    What browser/OS are you using when it fails with a lot of records?  There is no official limit to the length of a url, according to the spec, though various implementations have different limits (Firefox, Safari, and Chrome all support at least 40,000 characters, but Internet Explorer is limited to 2083 chars).  The various points that url is passing through (OS when it handles the protocol, and then the script when it receives it) might have length issues as well.

    As Rob mentions, currently when using an action menu item we are sending *both* the list of selected ids, and the list of all ids that match the query.  If we made this a pref on the ActionMenuItem (only send selected ids), that could at least mitigate the problem if you just want to act on the selected entities anyway.  But I'd also like to see if we can make longer urls work in setups where they are failing now!

  • 0
    Avatar
    Rob Blau

    I've seen it in Firefox/Safari on OSX (10.5 and 10.6) and in Firefox on Linux.  Also with large result sets, I've seen Firefox bring up that little unresponsive script dialog (firebug seems to say it happens while iterating through the list of rows, or something like that in the js).  Even when it works it does slow the browser way down during the launch through the protocol handler.

    -r

  • 0
    Avatar
    Isaac Reuben

    Would having it only send the selected ids be a solution for your usage?  I'm imagining a pref that greys out the menu option unless you have something selected (like how "Edit Selected..." works). 

    I'm sure we can make sending very large result sets work reliably, but might require restructing how the data is passed around (send just the query info instead of the full list of ids, or stash the list of id's on server and pass a reference to that to script, like url shortening, which then gets the full data through a normal api request).

    If the ActionMenuItem is calling another web server (instead of the custom protocol), we're sending the values through in a POST request instead of GET, so not tacked onto the url and doesn't have these length restrictions.

    Thanks for the feedback!

  • 0
    Avatar
    Rob Blau

    For our case (not sure if this works in general) I'd LOVE the following:

    If something is selected, then those ids are sent through

    If nothing is selected, then all ids are sent through

    If more than X (configurable?) are being sent through, there is an alert saying something like "You are selecting more than X <things>, this could be slow" with continue or cancel.

    The only time we use all ids is when there is no selection (we've run into issues with wanting to run menu items on things that span multiple pages, and this is our workaround when cranking up the # of entities displayed on the page would just be too slow).

    -r

  • 0
    Avatar
    Hugh Macdonald

    Hi Isaac,

     

    I do like the idea of AMIs having a flag to say whether they can only work when items are selected. There are quite a few that shouldn't work without something selected, and to have them greyed out would be fantastic.

  • 0
    Avatar
    Prabu Kasireddy

    Hi Isaac,

    We are using Firefox on Windows XP 64 bit machines.

    Having an option to disable sending all the IDs sounds like a good idea.

    We currently have scenarios where we need to do some operations on a large number of records and we have built an python utility that will get all the information from Shotgun and then work on that result.

    We are expecting a lot of entities getting filled with more than 200 records per project and would like to have some kind of a setting that would make the URL short.

  • 0
    Avatar
    Isaac Reuben

    OK, just did some tests.  I'm not seeing any limit on the length of urls on the Mac side (50k chars works fine).  Works for Firefox 3.6, Safari 4, and Chrome 4.

    On the Windows side (XP 32bit), Firefox 3.0, Firefox 3.6, and Chrome 4 all have a 2048 char limit (like Internet Explorer does for urls in general) when launching custom protocol handlers.  They *don't* have that limit in general for urls, so seems to be related to how they are passing the value to the system.  I'm not sure if there is another way to setup a protocol handler that more directly launches the script. The reg setting we've been using adds an entry like:

    [HKEY_CLASSES_ROOT\shotguntest\shell\open\command]@="<exe\_to\_run>"

    but possibly there is another way to open the script directly instead of passing a command through the shell?  Seems to be suggesting that on these links:

    http://www.advancedinstaller.com/forums/viewtopic.php?f=2&t=9910#p26249

    http://blogs.msdn.com/oldnewthing/archive/2003/12/10/56028.aspx

    But looks like we need to do something about not making urls over 2048 chars, so first step will be adding option to only send selected ids (nice to have that option in any case!), and then we'll figure out another way to pass all the ids (either pass query info, or create a token that the script can pass back through api to get the full list).

  • 0
    Avatar
    Dumay Nicolas

    Hi Isaac,

    Any update on this side? We are facing the same 2048 char issue here :/

     

    Thanks

    Nicolas

  • 0
    Avatar
    Frank Rueter

    Is this thread still maintained? I am having trouble getting a custom protocol handler to work under linux (Kubuntu 12.10). I ran gconftool as  outlined above but when clicking one of my custom links I gets the same old "The address wasn't understood" error. I did get rvlink to work and remember there was a bit of pain involved, but can't remember the details.

    I also tried setting "network.protocol-handler.expose.shotgunpy;true" in Firefox' about:config to no avail.

    Any help would be greatly appreciated!

    frank

  • 0
    Avatar
Please sign in to leave a comment.