-
Notifications
You must be signed in to change notification settings - Fork 49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement Application discovery and launch in core #303
Comments
Here's a proposal from my end.
class Action(object):
"""Run an action within a context"""
name = None
label = None
icon = None
order = 0
def launch(self, **kwargs):
pass
def is_compatible(self, session):
"""Return whether this action can be triggered in the specific session"""
pass A Maya application could then run like: # pseudocode
class Maya2018(Action):
name = "maya2018"
label = "Maya 2018"
icon = "path/to/maya/icon.png"
def is_compatible(self, session):
required = ["AVALON_ASSET", "AVALON_SILO", "AVALON_TASK", "AVALON_PROJECT", "AVALON_PROJECTS", "AVALON_WORKDIR"]
missing = [x for x in required if x not in session]
if missing:
return False
return True
def process(self, **kwargs):
env = self.env.copy()
env["PYTHONPATH"] = "xx/yy"
if kwargs.get("launch", True):
return lib.launch("maya2018")
The Project manager could then run like: # pseudocode
class ProjectManagerAction(Action):
def is_compatible(self, session):
# This can run anywhere?
return True
def process(self, **kwargs):
import avalon.tools.projectmanager.app as app
app.show() Then to show the available actions in your current context: actions = api.discover(api.Action)
session = api.Session.copy()
available = api.actions_by_session(actions, session)
print available Or to check compatibility for a single one. # Note: this code assumes the discover return action classes - not instances.
# and it also assumes the "is_compatible" method is a classmethod or
# staticmethod so it can run prior to initialization of the action.
Action = next(api.discover(api.Action))
if Action.is_compatible(session):
action = Action(session)
action.process() |
Here's some code that functionally works similar to the current launcher. @lib.log
class Action(object):
"""A custom action available"""
name = None
label = None
icon = None
order = None
def is_compatible(self, session):
"""Return whether the class is compatible with the Session."""
return True
def process(self, session, **kwargs):
pass
class Application(Action):
"""Default application launcher
This is a convenience application Action that when "config" refers to a
loaded application `.toml` this can launch the application.
"""
config = None
def is_compatible(self, session):
required = ["AVALON_PROJECTS",
"AVALON_PROJECT",
"AVALON_SILO",
"AVALON_ASSET",
"AVALON_TASK"]
missing = [x for x in required if x not in session]
if missing:
self.log.debug("Missing keys: %s" % (missing,))
return False
return True
def process(self, session, **kwargs):
session = session.copy()
session["AVALON_APP"] = self.config["application_dir"]
session["AVALON_APP_NAME"] = self.name
# Compute work directory
project = io.find_one({"type": "project"})
template = project["config"]["template"]["work"]
workdir = _format_work_template(template, session)
session["AVALON_WORKDIR"] = workdir
# Build environment
env = os.environ.copy()
env.update(self.config.get("environment", {}))
env.update(session)
# Create working directory
workdir_existed = os.path.exists(workdir)
if not workdir_existed:
os.makedirs(workdir)
self.log.info("Creating working directory '%s'" % workdir)
# Create default directories from app configuration
default_dirs = self.config.get("default_dirs", [])
if default_dirs:
self.log.debug("Creating default directories..")
for dirname in default_dirs:
try:
os.makedirs(os.path.join(workdir, dirname))
self.log.debug(" - %s" % dirname)
except OSError as e:
# An already existing default directory is fine.
if e.errno == errno.EEXIST:
pass
else:
raise
# Perform application copy
for src, dst in self.config.get("copy", {}).items():
dst = os.path.join(workdir, dst)
try:
self.log.info("Copying %s -> %s" % (src, dst))
shutil.copy(src, dst)
except OSError as e:
self.log.error("Could not copy application file: %s" % e)
self.log.error(" - %s -> %s" % (src, dst))
# Launch
if kwargs.get("launch", True):
executable = lib.which(self.config["executable"])
return lib.launch(executable=executable,
args=self.config.get("args", []),
environment=env,
cwd=workdir) This is then how I'm currently generating the App actions based on a project configuration. def register_project_apps():
project = io.find_one({"type": "project"})
assert project is not None, "This is a bug"
# Register the applications by `.toml`
apps = project["config"]["apps"]
for app in apps:
try:
app_definition = lib.get_application(app['name'])
except Exception as exc:
print("Unable to load application: %s - %s" % (app['name'], exc))
continue
# Define the application class
class App(pipeline.Application):
name = app['name']
label = app.get("label", app["name"])
config = app_definition.copy()
api.register_plugin(pipeline.Action, App) Then to run a specific one: # List registered apps
for Action in api.discover(pipeline.Action):
if Action.name == "maya2018":
action = Action()
action.process(api.Session.copy()) |
Here's a prototype of the above implemented into the Launcher. This is the project manager action: class ProjectManagerAction(avalon.api.Action):
name = "projectmanager"
label = "Project Manager"
icon = "gear"
def is_compatible(self, session):
return "AVALON_PROJECT" in session
def process(self, session, **kwargs):
import avalon.lib as lib
return lib.launch(executable="python",
args=["-m", "avalon.tools.projectmanager",
session['AVALON_PROJECT']]) This will only show when "AVALON_PROJECT" is available in the session - meaning the action is compatible to be processed (or triggered, or launched, or whatever you'd call it). And this is how the def get_apps(project):
"""Define dynamic Application classes for project using `.toml` files"""
import avalon.lib
import avalon.api as api
apps = []
for app in project["config"]["apps"]:
try:
app_definition = avalon.lib.get_application(app['name'])
except Exception as exc:
print("Unable to load application: %s - %s" % (app['name'], exc))
continue
icon = app_definition.get("icon", app.get("icon", "folder-o"))
action = type("app_%s" % app["name"],
(api.Application,),
{
"name": app['name'],
"label": app.get("label", app['name']),
"icon": icon,
"config": app_definition.copy()
})
apps.append(action)
return apps With this change the Launcher practically knows nothing about "launching" an application, it can just trigger actions - of which some happen to launch applications. |
Without having touched Avalon yet, this looks pretty good.
Think separating out the launching code into dedicated methods would give a nicer structure, and less per method responsibilities. Adding to that I would maybe think about an Manipulating Action Launch One thing that has given Ftrack's application actions a lot more flexibility has been the ability to modify the environment and launch arguments; http://ftrack-connect.rtd.ftrack.com/en/latest/developing/hooks/application_launch.html |
Could you elaborate slightly more on this? Perhaps a small psuedocode sample?
Note that the Actions here currently allow |
class Application(Action):
"""Default application launcher
This is a convenience application Action that when "config" refers to a
loaded application `.toml` this can launch the application.
"""
config = None
def is_compatible(self, session):
required = ["AVALON_PROJECTS",
"AVALON_PROJECT",
"AVALON_SILO",
"AVALON_ASSET",
"AVALON_TASK"]
missing = [x for x in required if x not in session]
if missing:
self.log.debug("Missing keys: %s" % (missing,))
return False
return True
def environ(self, session):
env = {}
env["AVALON_APP"] = self.config["application_dir"]
env["AVALON_APP_NAME"] = self.name
# Compute work directory
project = io.find_one({"type": "project"})
template = project["config"]["template"]["work"]
workdir = _format_work_template(template, session)
env["AVALON_WORKDIR"] = workdir
return env
def initialize(self, session):
# Create working directory
workdir_existed = os.path.exists(workdir)
if not workdir_existed:
os.makedirs(workdir)
self.log.info("Creating working directory '%s'" % workdir)
# Create default directories from app configuration
default_dirs = self.config.get("default_dirs", [])
if default_dirs:
self.log.debug("Creating default directories..")
for dirname in default_dirs:
try:
os.makedirs(os.path.join(workdir, dirname))
self.log.debug(" - %s" % dirname)
except OSError as e:
# An already existing default directory is fine.
if e.errno == errno.EEXIST:
pass
else:
raise
# Perform application copy
for src, dst in self.config.get("copy", {}).items():
dst = os.path.join(workdir, dst)
try:
self.log.info("Copying %s -> %s" % (src, dst))
shutil.copy(src, dst)
except OSError as e:
self.log.error("Could not copy application file: %s" % e)
self.log.error(" - %s -> %s" % (src, dst))
def launch(self, session, **kwargs):
executable = lib.which(self.config["executable"])
return lib.launch(executable=executable,
args=self.config.get("args", []),
environment=self.environ(),
cwd=workdir)
If you are using someone else's Maya action (maybe this is not the intend, then just ignore this), and you want to append an extra flag to the launch without duplicate/modify the action, how would you do that? |
Ah, yes - purely for inheritance. I agree - this would help towards that design. 👍 Thanks. |
Just don't call the database in this object; treat it as a data container only. If it returns an environment, it should only be the environment it knows; which is the variables set in the config. They can be merged with |
Though if it's an "action" then it'd be ok. I think the concept of what we first discussed about an Application object representing the .toml files have vanished from here; which is ok, but better not call it just Application then. Also there is no reference to the Better call it an application launcher action or the like. Actually, how about calling it I still think we need a discovery for .toml files, to get a hold of environment variables, default directories and such. How are you getting those currently? |
I've thought about this - but I have no clue how that would work. The application being launched describes the application directory required to format the The thing you are right about though, is that it's not explicit enough that the passed
Actually, the def get_apps(project):
"""Define dynamic Application classes for project using `.toml` files"""
import avalon.lib
import avalon.api as api
apps = []
for app in project["config"]["apps"]:
try:
app_definition = avalon.lib.get_application(app['name'])
except Exception as exc:
print("Unable to load application: %s - %s" % (app['name'], exc))
continue
icon = app_definition.get("icon", app.get("icon", "folder-o"))
color = app_definition.get("color", app.get("color", None))
action = type("app_%s" % app["name"],
(api.Application,),
{
"name": app['name'],
"label": app.get("label", app['name']),
"icon": icon,
"color": color,
"config": app_definition.copy()
})
apps.append(action)
return apps And the The |
Implement actions getavalon#303
@BigRoy This looks like its implemented in your fork of the launcher? |
Yes, I believe so. |
Should we try to merge those changes? |
Yes, great. Could you test it out on your end first? Just to be sure. :) |
These should be merged with: 5c7f9d6 |
…_in_all_hosts Pype workfiles tool in all hosts
Issue
Currently the Launcher allows to launch applications within the context of a project with its completely contained logic - this should be moved to the Avalon core where applications should be able to be listed, initialized and launched.
The Launcher currently uses
.toml
files to describe applications as described here: https://getavalon.github.io/2.0/reference/#project-executable-apiThe api in core would return Application classes that can run or get triggered within a specific Session, e.g. like Maya.
A proposed api by @mottosso can be found here
Other references:
Use cases
Some of my use cases:
Eventually be able to list the currently available Applications (or Actions?) in a current context, similar to ftrack, shotgun or even the current launcher. Here's a visual from ftrack:
The text was updated successfully, but these errors were encountered: