Opening a file with a custom application action?
2 2

16 posts in this topic

I built a little Blender launcher action for Connect so I can integrate ftrack with my current studio setup. I have a basic launcher working well, and can open Blender both from Connect and the ftrack interface.

How do I go a step further and tell it to open the component file associated with an asset? I've found some documentation pointing me in the right direction, but can't seem to make it to the finish line. Any pointers would be appreciated!

I assume I should be grabbing the file location from the asset and pushing it through either the event data's "command" or "options" variables (http://ftrack-connect.rtd.ftrack.com/en/stable/developing/hooks/application_launch.html#developing-hooks-application-launch). Because opening a blend file from the command line isn't a keyword argument (ie, "blender my_blend_file.blend") it should be under the command variable, but I can't figure out how to get it to work. Here is my launch function:

def launch(self, event):
        '''Callback method for Blender action.'''
        applicationIdentifier = (
            event['data']['applicationIdentifier']
        )

        self.logger.debug(
            pprint(event['data'])
        )

        context = event['data'].copy()
        
        # Tried opening a test file, but this crashes the action. Removing it opens default Blender file.
        #context['data']['command'].append('C:\\Users\\aaron\\Desktop\\test.blend')

        return self.launcher.launch(
            applicationIdentifier, context
        )

I've spent some time in the Maya action source code, but found little helpful for this particular problem. https://bitbucket.org/ftrack/ftrack-connect-maya/src/394b8d7a065bb67469386391caa9d883165e926a/resource/hook/ftrack_connect_maya_hook.py?at=master#ftrack_connect_maya_hook.py-74,89,117,126,131,134,137:138

Share this post


Link to post
Share on other sites

Hi @Aaron Powell have you tried using the 'launchArguments' rather than data.command ? 

you can see how it gets used in nuke (in this case from the discovery itself) https://bitbucket.org/ftrack/ftrack-connect-nuke/src/77c27edbab69316fc09a3b37f77852c9b8adadd7/resource/hook/ftrack_connect_nuke_hook.py#lines-276

and how gets interally used:
https://bitbucket.org/ftrack/ftrack-connect/src/e9c26b4007b03825f1fcf5b0346a4260c38ad45b/source/ftrack_connect/application.py#lines-416
 

hope it helps.
L.
 

Share this post


Link to post
Share on other sites

@Lorenzo Angeli These links are incredibly helpful. Adding the path with launchArguments works perfectly.

I can use the context['selection'] data to look up the file name I assume, right? I see I get an entityId and type, so I'll start playing around with the API and reference the links you sent me. Thanks again!

Share this post


Link to post
Share on other sites

Ok, so here's a little update!

If I run the action against the component, I can pull the file path with:

selection = event['data'].get('selection', [])
component = self.session.get('Component', selection[0]['entityId'])
if component is not None:
  # this is an internal function that prints to a file on my desktop for quick debugging
  # returns 'u'https://ftrack-us-east-1.s3-accelerate.amazonaws.com/68d68ada-9f98-11ea-96a3-42010af0000d/storage/5/5/0/f/f586-a218-11ea-8422-72ed585b6b9d?Signature=iUglFLXGsXDm4pTY7RvA5gFxn%2BQ%3D&Expires=1590812357&AWSAccessKeyId=AKIAYKW36AUCL5BMAVWP'
  self.__debug(component['component_locations'][0]['url']['value'])

Is there a better way to do this or am I on the right track?

Edited by Aaron Powell
Found more information through testing and research

Share this post


Link to post
Share on other sites

Thanks Lorenzo, that works great.

All of a sudden though, ftrack doesn't want to launch Blender with the edited launchArguments parameter. I had it working at one point a couple of weeks ago, and left it alone to take another look at my Perforce issues. Now that I'm revisiting this, it's giving me grief.

The expected launchArguments are in the event['data'] object - this is what I get if I print out event['data'] just before passing it to launcher.launch() (file path changed to protect client):

{u'actionIdentifier': u'blender-launch-action',
 u'applicationIdentifier': u'blender_2.82',
 u'icon': u'http://icons.iconarchive.com/icons/dakirby309/simply-styled/256/Blender-icon.png',
 u'label': u'Blender 2.82',
 u'launchArguments': [u'c:\\Users\\aaron\\Perforce\\path\\to\\file.blend'],
 u'selection': [{u'entityId': u'cce25bb7-b9ee-41a5-8278-db5c47d21940',
                 u'entityType': u'Component'}]}

And here are my discover() and launch() functions for reference:

def discover(self, event):
  '''Return action based on *event*.'''

  launchArguments = []

  selection = event['data'].get('selection', [])

  if self.is_component(selection): # custom helper function
  	component = self.session.get('Component', selection[0]['entityId'])
            
    if component is not None:
    	location = self.session.pick_location()
        url = location.get_filesystem_path(component)
        launchArguments.append(url)
                

    items = []
    applications = self.applicationStore.applications
    applications = sorted(
      	applications, key=lambda application: application['label']
    )

    for application in applications:
        applicationIdentifier = application['identifier']
        label = application['label']
        items.append({
            'actionIdentifier': self.identifier,
            'label': label,
            'icon': 'http://icons.iconarchive.com/icons/dakirby309/simply-styled/256/Blender-icon.png',
            'applicationIdentifier': applicationIdentifier,
            'launchArguments': launchArguments
        })

	return {
        'items': items
    }

def launch(self, event):
    '''Callback method for Blender action.'''
    applicationIdentifier = (
        event['data']['applicationIdentifier']
    )

    context = event['data'].copy()

    self.__debug(context) # custom debug helper function

    return self.launcher.launch(
        applicationIdentifier, context
    )

Nothing unusual is showing up in the main ftrack log file, but I don't know if it would.

Share this post


Link to post
Share on other sites

in the ftrack_connect logs you should have the full command which gets executed by subprocess in a line such as :
 

Quote
2020-06-19 15:34:06,044 - ftrack_connect.application.ApplicationLauncher - DEBUG - Launching ['/usr/autodesk/maya2018/bin/maya'] with options {'close_fds': True, '.........}
 
try to manually run it to see if there's any error.
let us know how it goes.
L.

Share this post


Link to post
Share on other sites

@Lorenzo Angeli When I checked the log file there was no such line. So, I did some testing.

First, I removed both my Blender plugin and the Perforce plugin and tried to run Photoshop. That time, I did see a "launch" log line.

When I enabled only the Perforce plugin and tried to run Photoshop, I got the same results I did the first time: significantly smaller log file with no "launch" log.

Enabling Blender only, I was able to see a launch log line but it doesn't appear to be attaching the launchArguments to the command if I'm reading it right. I hard-coded a path to an existing file to test.

2020-06-20 11:55:34,707 - ftrack_connect.ui.widget.actions.Actions - DEBUG - Before action launched: {'selection': [], 'applicationIdentifier': 'blender_2.82', 'label': 'Blender 2.82', 'actionIdentifier': 'blender-launch-action', 'launchArguments': ['c:\\Users\\aaron\\Desktop\\lookdev_addon.blend'], 'icon': 'http://icons.iconarchive.com/icons/dakirby309/simply-styled/256/Blender-icon.png'}
2020-06-20 11:55:34,786 - ftrack_connect.application.ApplicationLauncher - DEBUG - Launching ['C:\\Program Files\\Blender Foundation\\Blender 2.82\\blender.exe'] with options {'close_fds': True, 'cwd': 'C:\\Program Files\\Blender Foundation\\Blender 2.82', 'env': {#environment variables here#}
2020-06-20 11:55:34,796 - ftrack_connect.application.ApplicationLauncher - DEBUG - blender_2.82 application started. (pid=14384)
2020-06-20 11:55:34,798 - ftrack_connect.ui.widget.action_item.ActionItem - DEBUG - Launched action with result: [{'message': 'Blender 2.82 application started.', 'success': True}]
2020-06-20 11:55:34,826 - ftrack_connect.ui.widget.actions.Actions - DEBUG - Action launched: {'selection': [], 'applicationIdentifier': 'blender_2.82', 'label': 'Blender 2.82', 'actionIdentifier': 'blender-launch-action', 'launchArguments': ['c:\\Users\\aaron\\Desktop\\lookdev_addon.blend'], 'icon': 'http://icons.iconarchive.com/icons/dakirby309/simply-styled/256/Blender-icon.png'}

 

Share this post


Link to post
Share on other sites

Hi @Aaron Powell, extra arguments seems to be passed fine  :
 

'launchArguments': ['c:\\Users\\aaron\\Desktop\\lookdev_addon.blend'], 

but then does not get populated when running the application

2020-06-20 11:55:34,786 - ftrack_connect.application.ApplicationLauncher - DEBUG - Launching ['C:\\Program Files\\Blender Foundation\\Blender 2.82\\blender.exe']

as by https://bitbucket.org/ftrack/ftrack-connect/src/57ece813d21bff72621c1a6a58feb958cd0557cd/source/ftrack_connect/application.py#lines-364

your Launching command should contain your extra arguments.

in order to further debug you can overwrite 
_getApplicationLaunchCommand to print the command before being returned to the actual popen function.

Hope it helps.
L.

 

Share this post


Link to post
Share on other sites
22 hours ago, Lorenzo Angeli said:

 


'launchArguments': ['c:\\Users\\aaron\\Desktop\\lookdev_addon.blend'], 

but then does not get populated when running the application


2020-06-20 11:55:34,786 - ftrack_connect.application.ApplicationLauncher - DEBUG - Launching ['C:\\Program Files\\Blender Foundation\\Blender 2.82\\blender.exe']

as by https://bitbucket.org/ftrack/ftrack-connect/src/57ece813d21bff72621c1a6a58feb958cd0557cd/source/ftrack_connect/application.py#lines-364

your Launching command should contain your extra arguments.

in order to further debug you can overwrite 
_getApplicationLaunchCommand to print the command before being returned to the actual popen function.

Hope it helps.
L.

 

Hi Lorenzo,

i am currently looking for the same solution and my launchArgument gets not populated when running my application too.

can you please give more details on how to overwrite the "_getApplicationLaunchCommand" ?
this function is not included in my hook, and adding this function from your mentioned site will miss some inputs i cannot work out to handle.

this is my first time using ftrack.
thanks in advance.
M.

Share this post


Link to post
Share on other sites
import logging

import ftrack_api

import sys
import pprint

import ftrack_connect.application

class ApplicationStore(ftrack_connect.application.ApplicationStore):
    '''Store used to find and keep track of available applications.'''

    def _discoverApplications(self):
        '''Return a list of applications that can be launched from this host.'''
        applications = []

        if sys.platform == 'darwin':
            prefix = ['/', 'Applications']

            applications.extend(self._searchFilesystem(
                expression=prefix + [
                    'Blender*', 'Blender.app'
                ],
                label='Blender {version}',
                applicationIdentifier='blender_{version}'
            ))

        elif sys.platform == 'win32':
            prefix = ['C:\\', 'Program Files.*']

            applications.extend(self._searchFilesystem(
                expression=(
                    prefix +
                    ['Blender Foundation', 'Blender*', 'blender.exe']
                ),
                label='Blender {version}',
                applicationIdentifier='blender_{version}'
            ))

        self.logger.debug(
            'Discovered applications:\n{0}'.format(
                pprint.pformat(applications)
            )
        )

        return applications
    



class BlenderAction(object):
    '''Launch Blender action.'''

    # Unique action identifier.
    identifier = 'blender-launch-action'

    def __debug(self, message):
        f = open("C:\\Users\\aaron\\Desktop\\output.txt", "a")
        f.write(pprint.pformat(message))
        f.close()

    def __init__(self, applicationStore, launcher):
        '''Initialise action with *applicationStore*.'''
        super(BlenderAction, self).__init__()

        self.logger = logging.getLogger(
            __name__ + '.' + self.__class__.__name__
        )

        self.applicationStore = applicationStore
        self.launcher = launcher

        if self.identifier is None:
            raise ValueError('The action must be given an identifier.')

    def register(self, session):
        '''Register action.'''
        session.event_hub.subscribe(
            'topic=ftrack.action.discover',
            self.discover
        )

        session.event_hub.subscribe(
            'topic=ftrack.action.launch and data.actionIdentifier={0}'.format(
                self.identifier
            ),
            self.launch
        )

        self.session = session

    def is_component(self, selection):
        if (
            len(selection) != 1 or
            selection[0]['entityType'] != 'Component'
        ):
            return False

        return True
        
    def discover(self, event):
        '''Return action based on *event*.'''

        launchArguments = []

        selection = event['data'].get('selection', [])

        if self.is_component(selection):
            component = self.session.get('Component', selection[0]['entityId'])
            
            if component is not None:
                location = self.session.pick_location()
                url = location.get_filesystem_path(component)
                launchArguments.append(url)
                

        items = []
        applications = self.applicationStore.applications
        applications = sorted(
            applications, key=lambda application: application['label']
        )

        for application in applications:
            applicationIdentifier = application['identifier']
            label = application['label']
            items.append({
                'actionIdentifier': self.identifier,
                'label': label,
                'icon': 'http://icons.iconarchive.com/icons/dakirby309/simply-styled/256/Blender-icon.png',
                'applicationIdentifier': applicationIdentifier,
                'launchArguments': ['c:\\Users\\aaron\\Desktop\\lookdev_addon.blend'] #launchArguments
            })

        return {
            'items': items
        }

    def launch(self, event):
        '''Callback method for Blender action.'''
        applicationIdentifier = (
            event['data']['applicationIdentifier']
        )

        context = event['data'].copy()

        return self.launcher.launch(
            applicationIdentifier, context
        )

class ApplicationLauncher(ftrack_connect.application.ApplicationLauncher):
    '''Custom launcher to modify environment before launch.'''

    def __debug(self, message):
        f = open("C:\\Users\\aaron\\Desktop\\output.txt", "a")
        f.write(pprint.pformat(message))
        f.close()
    
    def _getApplicationLaunchCommand(self, application, context=None):
        command = ftrack_connect.application.ApplicationLauncher._getApplicationLaunchCommand(self, application, context)
        self.__debug(command)
        return command

def register(session, **kw):
    '''Register action in Connect.'''

    # Validate that session is an instance of ftrack_api.Session. If not, assume
    # that register is being called from an old or incompatible API and return
    # without doing anything.
    if not isinstance(session, ftrack_api.Session):
        return

    applicationStore = ApplicationStore()

    launcher = ApplicationLauncher(
        applicationStore
    )

    action = BlenderAction(applicationStore, launcher)
    action.register(session)

Here's the full hook. To be honest, it's not too much different from the Houdini example in the docs at the moment, which is why I'm not sure what's going on. Let me know if something sticks out.

Share this post


Link to post
Share on other sites

Hi @Aaron Powell using the discovery to populate the available applications does not work as it will look into the application store to decide which one to start.
The best option at the time seems to be to override the _getApplicationLauncher as by this example to inject the launchArguments you are after.

long story short this is what you can simply do:
 

class ApplicationLauncher(ftrack_connect.application.ApplicationLauncher):
    '''Custom launcher to modify environment before launch.'''
    
    def _getApplicationLaunchCommand(self, application, context=None):
        command = super(ApplicationLauncher, self)._getApplicationLaunchCommand(application, context)
        command.extend(context.get('launchArguments'))
        return command

The issue is in the original _getApplicationLauncher, which extracts the extra launchArguments from the applications but does not take in account the current context.
good news is that we are in the process of reviewing how applications are launched, hence this will be fixed soon.

Cheers.
L.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
2 2