diff --git a/com.shotgunsoftware.basic.adobecc.version b/com.shotgunsoftware.basic.adobecc.version new file mode 100644 index 0000000..95e94cd --- /dev/null +++ b/com.shotgunsoftware.basic.adobecc.version @@ -0,0 +1 @@ +v0.0.1 \ No newline at end of file diff --git a/com.shotgunsoftware.basic.adobecc.zxp b/com.shotgunsoftware.basic.adobecc.zxp new file mode 100644 index 0000000..c0a9a45 Binary files /dev/null and b/com.shotgunsoftware.basic.adobecc.zxp differ diff --git a/developer/build_extension.py b/developer/build_extension.py new file mode 100755 index 0000000..54fb4e0 --- /dev/null +++ b/developer/build_extension.py @@ -0,0 +1,417 @@ +#!/usr/bin/env python + +# Copyright (c) 2016 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import argparse +import os +import re +import shlex +import shutil +import subprocess +import sys + +# global placeholder for when we import sgtk +sgtk = None +logger = None + +# bundle cache dir name within the built plugin +BUNDLE_CACHE_DIR = "bundle_cache" + +# the build script located in the core repo +CORE_BUILD_SCRIPT = os.path.join("developer", "build_plugin.py") + +# timestamp url for signing the extension. +# see: http://www.davidebarranca.com/2014/05/html-panels-tips-10-packaging-zxp-installers/ +TIMESTAMP_URL = "https://timestamp.geotrust.com/tsa" + + +def main(): + """ + Wraps all the steps to build and sign the extension + """ + + # parse and validate the command line args + args = _validate_args(_parse_args()) + + # first step is to build the plugin + _build_plugin(args) + + # remove the bundle cache unless specified + if not args["bundle_cache"]: + _remove_bundle_cache(args) + + # add a version file to the built plugin + _write_version_file(args) + + # sign the newly built plugin and create the .ZXP file + if args["sign"]: + _sign_plugin(args) + else: + logger.warning( + "Not signing the built plugin. This build can not be released!" + ) + + logger.info("Build successful.") + + +def _build_plugin(args): + """ + First step is to build the plugin itself. + + Adds the extension output dir to the args dict. + """ + + # construct the full extension output directory + plugin_build_dir = os.path.abspath( + os.path.join( + args["output_dir"], + args["extension_name"] + ) + ) + + # the full plugin build script command + command = "{py} {build_script} '{plugin_dir}' '{plugin_build_dir}'".format( + py=sys.executable, + build_script=os.path.join(args["core"], CORE_BUILD_SCRIPT), + plugin_dir=args["plugin_dir"], + plugin_build_dir=plugin_build_dir, + ) + + # execute the build script + logger.info("Plugin build command: %s" % (command,)) + logger.info("Executing plugin build command...") + status = subprocess.call(shlex.split(command)) + + # check the return status + if status: + logger.error("Error building the plugin.") + raise Exception("There was a problem building the plugin.") + + # add the full ext dir path to the args dict + args["plugin_build_dir"] = plugin_build_dir + logger.info("Built plugin: %s" % (args["plugin_build_dir"],)) + + +def _parse_args(): + """ + Define the parser and return the parsed args. + """ + + parser = argparse.ArgumentParser( + description="Build and package an Adobe extension for the " + + "engine. This includes signing the extension with the " + + "supplied certificate. The extension will be built " + + "in the engine repo unless an output directory is " + + "specified." + ) + + parser.add_argument( + "--core", "-c", + metavar="/path/to/tk-core", + help="The path to tk-core to use when building the toolkit plugin.", + required=True, + ) + + parser.add_argument( + "--plugin_name", "-p", + metavar="name", + help="The name of the engine plugin to build. Ex: 'basic'.", + required=True, + ) + + parser.add_argument( + "--extension_name", "-e", + metavar="name", + help="The name of the output extension. Ex: " + + "'com.shotgunsoftware.basic.photoshopcc'", + required=True, + ) + + parser.add_argument( + "--sign", "-s", + nargs=3, + metavar=("/path/to/ZXPSignCmd", "/path/to/certificate", "password"), + help="If supplied, sign the build extension. Requires 3 arguments: " + + "the path to the 'ZXPSignCmd', the certificate and the password." + + "Note, the ZXPSignCmd can be downloaded here: " + + "http://labs.adobe.com/downloads/extensionbuilder3.html", + ) + + parser.add_argument( + "--bundle_cache", "-b", + action="store_true", + help="If supplied, include the 'bundle_cache' directory in the build " + + "plugin. If not, it is removed after the build.", + ) + + parser.add_argument( + "--version", "-v", + metavar="v#.#.#", + help="The version to attached to the built plugin. If not specified, " + + "the version will be set to 'dev' and will override any version " + + "of the extension at launch/install time. The current version " + + "can be found in the .version file that lives next to the " + + "existing .zxp file." + ) + + parser.add_argument( + "--output_dir", "-o", + metavar="/path/to/output/extension", + help="If supplied, output the built extension here. If not supplied, " + + "the extension will be built in the engine directory at the top " + + "level.", + ) + + return parser.parse_args() + +def _remove_bundle_cache(args): + """ + Remove the built extensions bundle cache. + """ + + logger.info("Removing bundle cache from built extension...") + bundle_cache_dir = os.path.join( + args["plugin_build_dir"], + BUNDLE_CACHE_DIR + ) + try: + shutil.rmtree(bundle_cache_dir) + except Exception, e: + logger.warning("Failed to remove bundle cache from extension.") + + +def _sign_plugin(args): + """ + Sign the built plugin (creates .zxp) and cleanup the build directory. + """ + + # extension_path is same as the plugin build dir with the .zxp extension + extension_path = "%s.zxp" % (args["plugin_build_dir"],) + + # remove the existing build file + if os.path.exists(extension_path): + from sgtk.util.filesystem import safe_delete_file + safe_delete_file(extension_path) + + (sign_command, certificate_path, certificate_pwd) = args["sign"] + + cmd_str = "{zxp_sign} -sign {plugin_build_dir} {extension_path} {cert} " + \ + "{cert_pwd} -tsa {tsa}" + command = cmd_str.format( + zxp_sign=sign_command, + plugin_build_dir=args["plugin_build_dir"], + extension_path=extension_path, + cert=certificate_path, + cert_pwd=certificate_pwd, + tsa=TIMESTAMP_URL, + ) + + # execute the build script + logger.info("Signing extension command: %s" % (command,)) + logger.info("Signing the extension...") + status = subprocess.call(shlex.split(command)) + + # check the return status + if status: + logger.error("Error signing the extension.") + raise Exception("There was a problem signing the extension.") + + # clean up the plugin build directory + try: + shutil.rmtree(args["plugin_build_dir"]) + except Exception, e: + logger.warning("Failed to remove plugin build dir.") + + # add the signed extension path to the args + args["extension_path"] = extension_path + logger.info("Signed extension: %s" % (args["extension_path"],)) + +def _validate_args(args): + """ + Validate the parsed args. Will raise if there are errors. + + Sets up the logger if core can be imported. + + Adds some additional values based on supplied args including the engine + directory, plugin path, etc. + + Returns a dictionary of the parsed arguments of the following form: + + { + 'core': '/path/to/tk-core', + 'extension_name': 'extesion.name.here', + 'bundle_cache': True, + 'plugin_name': 'plugin_name', + 'sign': ['/path/to/ZXPSignCmd', '/path/to/cert', 'cert_password'], + 'version': 'v1.0.0', + 'output_dir': '/path/to/output/dir', + 'engine_dir': '/path/to/the/engine/repo', + 'plugin_dir': '/path/to/the/engine/plugin', + } + """ + + # convert the args namespace to a dict + args = vars(args) + + # ensure core path exists and build script is there + if not os.path.exists(args["core"]): + raise Exception( + "Supplied core path does not exist: %s" % (args["core"],) + ) + + # make sure we can import core + try: + sgtk_dir = os.path.join(args["core"], "python") + sys.path.insert(0, sgtk_dir) # make sure this one is found first + import sgtk as imported_sgtk + global sgtk + sgtk = imported_sgtk + except Exception, e: + raise Exception("Error import supplied core: %s" % (e,)) + + # setup the logger for use from here on out + try: + # set up std toolkit logging to file + sgtk.LogManager().initialize_base_file_handler("build_plugin") + + # set up output of all sgtk log messages to stdout + sgtk.LogManager().initialize_custom_handler() + + global logger + logger = sgtk.LogManager.get_logger("build_extension") + + except Exception, e: + raise Exception("Error creating toolkit logger: %s" % (e,)) + + logger.info("Validating command line arguments...") + + # ensure the core plugin build script exists + logger.info("Finding plugin build script...") + build_script = os.path.join(args["core"], CORE_BUILD_SCRIPT) + if not os.path.exists(build_script): + raise Exception( + "Could not find plugin build script in supplied core: %s" % + (build_script,) + ) + + # ensure the extension name is valid + logger.info("Ensuring valid plugin & extension build names...") + from sgtk.util.filesystem import create_valid_filename + args["extension_name"] = create_valid_filename(args["extension_name"]) + logger.info("Extension name: %s" % (args["extension_name"])) + + # make sure version is valid + logger.info("Verifying supplied version...") + if args["version"]: + if not re.match(r"^v\d+\.\d+\.\d+$", args["version"]): + raise Exception( + "Supplied version doesn't match the format 'v#.#.#'. Supplied: %s" % + (args["version"],) + ) + else: + args["version"] = "dev" + + # if signing requested, validate those args + if args["sign"]: + + logger.info("Verifying 'ZXPSignCmd` path...") + if not os.path.exists(args["sign"][0]): + raise Exception( + "The supplied 'ZXPSignCmd' does not exist. Supplied path: %s " % + (args["sign"][0],) + ) + + logger.info("Verifying certificate path...") + if not os.path.exists(args["sign"][1]): + raise Exception( + "The supplied certificate does not exist. Supplied path: %s " % + (args["sign"][1],) + ) + + # get the full path to the engine repo + logger.info("Populating the engine directory...") + args["engine_dir"] = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + os.pardir + ) + ) + + # ensure the plugin can be found in the engine + logger.info("Validating plugin name...") + plugin_dir = os.path.join( + args["engine_dir"], + "plugins", + args["plugin_name"] + ) + if not os.path.exists(plugin_dir): + raise Exception( + "Could not find plugin '%s' in engine." % (args["plugin_name"],) + ) + args["plugin_dir"] = plugin_dir + + # if output dir defined, ensure it exists. populate args with engine dir + # if not. + logger.info("Determining output directory...") + if args["output_dir"]: + if not os.path.exists(args["output_dir"]): + from sgtk.util.filesystem import ensure_folder_exists + ensure_folder_exists(args["output_dir"]) + else: + args["output_dir"] = args["engine_dir"] + + # return the validate args + logger.info("Command line arguments validated.") + return args + + +def _write_version_file(args): + """ + Write a file to the built plugin directory containing the specified version. + + Also write the file to the top-level of the engine repo to make it possible + to easily compare during install. + """ + + # the file to create with the specified version + bundle_version_file_path = os.path.join( + args["plugin_build_dir"], + "%s.%s" % (args["extension_name"], "version") + ) + + # write the file + logger.info("Writing build version info file...") + with open(bundle_version_file_path, "w") as bundle_version_file: + bundle_version_file.write(args["version"]) + + engine_file_path = os.path.join( + args["engine_dir"], + "%s.%s" % (args["extension_name"], "version"), + ) + + # write the file + logger.info("Writing build version info file...") + with open(engine_file_path, "w") as engine_version_file: + engine_version_file.write(args["version"]) + + +if __name__ == "__main__": + + exit_code = 1 + try: + exit_code = main() + except Exception, e: + print "ERROR: %s" % (e,) + else: + logger.info("Extension successfully built!") + + sys.exit(exit_code) + diff --git a/engine.py b/engine.py index 476cd89..ff77e03 100644 --- a/engine.py +++ b/engine.py @@ -287,9 +287,9 @@ def _handle_active_document_change(self, active_document_path): return if active_document_path: - self.log_debug("New active document is %s" % active_document_path) + self.logger.debug("New active document is %s" % active_document_path) else: - self.log_debug( + self.logger.debug( "New active document check failed. This is likely due to the " "new active document being in an unsaved state." ) @@ -304,15 +304,18 @@ def _handle_active_document_change(self, active_document_path): previous_context=self.context, ) except Exception: - # TODO: We need to set the panel to a disabled state the way - # we do the Shotgun menu in Nuke Studio. - self.log_debug( + self.logger.debug( "Unable to determine context from path. Not changing context." ) + # clear the context finding task ids so that any tasks that + # finish won't send data to js. + self.__context_find_uid = None + self.__context_thumb_uid = None + self.adobe.send_unknown_context() return if not context.project: - self.log_debug( + self.logger.debug( "New context doesn't have a Project entity. Not changing " "context." ) @@ -321,6 +324,7 @@ def _handle_active_document_change(self, active_document_path): self._CONTEXT_CACHE[active_document_path] = context if context != self.context: + self.adobe.context_about_to_change() sgtk.platform.change_context(context) def _handle_command(self, uid): diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..30dd70d --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,261 @@ +# Extension Development + +This section is designed for developers and covers how to setup, build, and test +the Shotgun Photoshop CC extension. + +For more information about Adobe CEP extensions, here are some useful resources: + +* [CEP Resources (Github)](https://github.com/Adobe-CEP/CEP-Resources) +* [David Berranca's Blog](http://www.davidebarranca.com/) +* [A Short Guide to HTML5 Extensions (Adobe)](http://www.adobe.com/devnet/creativesuite/articles/a-short-guide-to-HTML5-extensions.html) + +## Building & Testing + +Building the extension requires a little bit of setup. The following sections +cover the prerequisites and various scenarios for building and testing the +extension. + +### OS-specific CEP extensions install directories: + +All Adobe extensions live in a single directory. The directory is specific to +the current OS. Here are the directories for Windows and OS X. It is important +to be able to access these directories in order to monitor and manually clean up +extensions during development should something go awry. + +```shell +# Windows +> C:\Users\[user name]\AppData\Roaming\Adobe\CEP\extensions\ + +# OS X +> ~/Library/Application Support/Adobe/CEP/extensions/ +``` + +### Build Script + +Because the SG-Adobe integration is built via the **Toolkit as a Plugin** framework, +you'll need to rebuild the SG plugin into an Adobe extension as you make changes +to the code. + +In order to build the extension, you'll need to run the included +`developer/build_extension.py` script. This is a wrapper around the +`build_plugin.py` from core (`v0.18.27` or later). + +Building the extension requires running the `build_extension.py` script with a +local copy of tk-core, supplying it with options about where and how to build +the output extension. + +The arguments for the build script look like this: + +```bash +> ./build_extension.py --help +usage: build_extension.py [-h] --core /path/to/tk-core --plugin_name name + --extension_name name + [--sign /path/to/ZXPSignCmd /path/to/certificate password] + [--bundle_cache] + [--version v#.#.#] + [--output_dir /path/to/output/extension] + +Build and package an Adobe extension for the engine. This includes +signing the extension with the supplied certificate. The extension will be +built in the engine repo unless an output directory is specified. + +optional arguments: + -h, --help show this help message and exit + --core /path/to/tk-core, -c /path/to/tk-core + The path to tk-core to use when building the toolkit + plugin. + --plugin_name name, -p name + The name of the engine plugin to build. Ex: 'basic'. + --extension_name name, -e name + The name of the output extension. Ex: + 'com.shotgunsoftware.basic.adobecc' + --sign /path/to/ZXPSignCmd /path/to/certificate password, -s /path/to/ZXPSignCmd /path/to/certificate password + If supplied, sign the build extension. Requires 3 + arguments: the path to the 'ZXPSignCmd', the + certificate and the password.Note, the ZXPSignCmd can + be downloaded here: + http://labs.adobe.com/downloads/extensionbuilder3.html + --bundle_cache, -b If supplied, include the 'bundle_cache' directory in + the build plugin. If not, it is removed after the + build. + --version v#.#.#, -v v#.#.# + The version to attached to the built plugin. If not + specified, the version will be set to 'dev' and will + override any version of the extension at + launch/install time. The current version can be found + in the .version file that lives next to the existing + .zxp file. + --output_dir /path/to/output/extension, -o /path/to/output/extension + If supplied, output the built extension here. If not + supplied, the extension will be built in the engine + directory at the top level. +``` + +There are quite a few options for the script. The sections below will outline +how to use the build script for different testing scenarios. + +### PlayerDebugMode + +If you plan to do local development and testing with an unsigned/uncertified +extensions, you will need to set some OS-specific preferences: + +As per the Adobe docs: + +> Applications will normally not load an extension unless it is +> cryptographically signed. However, during development we want to be able to +> quickly test an extension without having to sign it. To turn on debug mode: +> +> * On Mac, open the file `~/Library/Preferences/com.adobe.CSXS.7.plist` and add a +> row with key `PlayerDebugMode`, of type `String`, and value `1`. +> * On Windows, open the registry key `HKEY_CURRENT_USER/Software/Adobe/CSXS.7` +> and add a key named `PlayerDebugMode`, of type `String`, and value `1` +> +> You should only need to do this once. + +### Local testing without signing + +It is possible to build and test the extension without bundling it into a `.zxp` +file (what Adobe wants for its Add-ons site). This scenario will simply run the +underlying SG plugin build into a directory of your choosing. + +To test with this setup, set an environment variable: +`SHOTGUN_ADOBECC_DISABLE_AUTO_INSTALL`. In the classic toolkit startup (web, +Desktop, tk-shell) this will prevent the `.zxp` packaged with the engine from +being installed automatically. + +Now you can build the extension directly to the CEP extensions directory. Here's +an example of the command run from the top level of the engine repo: + +``` +python developer/build_extension.py -c ../tk-core -p basic -e com.shotgunsoftware.basic.adobecc -o "/Users/josh/Library/Application Support/Adobe/CEP/extensions/" +``` + +Argument breakdown: + +* `-c /path/to/core`: The local path to the `tk-core` to use for building the plugin +* `-p plugin_name': The engine plugin to build. Plugins found in the engine's `plugins` directory. +* `-e extension_name`: The output name of the adobe extension to build. This should match the +`ExtensionBundleId` found in the extensions manifest (`CSXS/manifest.xml`). +* `-o output_dir`: The output directory to write the built extension. Here we're +writing directly to the installed CEP extensions directory. + +Repeat this command as you make changes to the plugin code to update the installed +extension. NOTE: Older versions of the core `build_plugin.py` script will leave a +backup directory in the CEP extensions directory, next to the build. You will likely +want to make sure these are cleaned up after each build to avoid confusion and prevent +issues inside the Adobe product. + +### Local testing with signing + +You can also build a signed extension that mimics the final build. This workflow +involves building a `.zxp` file that can be auto-installed and updated as you +launch toolkit (via web, Desktop, or tk-shell). + +First, make sure you do **NOT** have the `SHOTGUN_ADOBECC_DISABLE_AUTO_INSTALL` +set in your environment. We will build the `.zxp` and allow the auto install to +run. This env variable prevents that. + +Next, clean out any previous direct builds (see previous section) from the CEP +install directory. The first toolkit startup will install the `.zxp` automatically. + +Finally, download the `ZXPSignCmd` tool to sign the built extension. You can find +it [here](http://labs.adobe.com/downloads/extensionbuilder3.html). + +Now you can build the `.zxp` file with the extension build script. Here's an +example command run from the top level of the engine repo: + +``` +python developer/build_extension.py -c ../tk-core -p basic -e com.shotgunsoftware.basic.adobecc -s ../ZXPSignCmd ~/Documents/certs/my_certificate.p12 my_cert_password +``` + +The first few arguments are identical to the previous example. The additional +arguments are: + +* `-s ../ZXPSignCmd`: The path to the `ZXPSignCmd` tool that you previously +downloaded +* `~/Documents/certs/my_certificate.p12`: The second argument to the `-s` (sign) +option is the path to certificate to use to sign the extension. +* `my_cert_password`: The third argument to the `-s` option is the password for +the certificate. + +Since the `-o` (output directory) option was not supplied, the `.zxp` file will +be build and overwrite the `.zxp` file in the engine. The built extension file +will look like this: + +``` +com.shotgunsoftware.basic.adobecc.version +com.shotgunsoftware.basic.adobecc.zxp +``` + +Notice the `.version` file. This file is used by the toolkit startup logic to +know when to auto install the extension. When the `-v` (version) flag is not +specified, this file will contain the string `dev` which tells the engine +startup code to always install/update this extension. This makes the developer +round trip from extension build to engine startup straight forward. A copy of +this `.version` file is also included in the `.zxp` bundle so that when it is +unpacked within the CEP extensions directory, it can be easily compared against +what is in the engine for the non `dev` case. + +If you're worried about overwriting the `.zxp` file bundled with the engine, +remember you can always use `git checkout` to discard your changes. + +### Building for Release + +Building for release is almost identical to the previous example with the +exception of specifying an actual version. To do that, you simple add an additional +argument to the build command: + +``` +python developer/build_extension.py -c ../tk-core -p basic -e com.shotgunsoftware.basic.adobecc -s ../ZXPSignCmd ~/Documents/certs/my_certificate.p12 my_cert_password -v v0.0.1 +``` + +The results of the command will look like this: + +``` +com.shotgunsoftware.basic.adobecc.version +com.shotgunsoftware.basic.adobecc.zxp +``` + +Unlike the previous section, the `.version` file will now include the string +`v0.0.1`. As bug fixes and features are added to the extension, and it is rebuilt +with higher version numbers, the engine startup code will use this file to +compare and determine if the user's installed version requires an update. + +### Testing + +The `basic` plugin/extension has a flyout menu with options useful for testing +and debugging. To enable these, set the environment variable `SHOTGUN_ADOBECC_DEVELOPER`. + + +* **Chrome Console...** - Requires Chrome as the default browser. Opens a Chrome +console connected to the Adobe extensions. +* **Reload Shotgun Extension** - Reloads the extension, including restarting the +external python process. + +### Configuration repo + +If you're doing development on the `tk-photoshop` engine and launching it +via the Adobe CC launcher (Toolkit as a plugin), you'll need to clone the config +repo so that you can change it to point to your development repo(s). The config +used by the engine is the [tk-config-basic](https://github.com/shotgunsoftware/tk-config-basic) +repo. Obviously, you'll also need to clone the repos for the bundle's you're +doing development on as well. + +The base configuration is defined in the extension's `info.yml`. You will need +to change the descriptor arguments to point to your development repository. Here +is an example: + +```yaml +base_configuration: + #type: app_store + #name: tk-config-pluginbasic + + type: dev + path: /path/to/my/dev/config/repo +``` + +Once you're pointing to your configuration development repo, you can change the +bundle descriptors in the configuration's environment files to point to the +development repos for the bundles you're working on. + + diff --git a/plugins/basic/README.md b/plugins/basic/README.md deleted file mode 100644 index 4516b93..0000000 --- a/plugins/basic/README.md +++ /dev/null @@ -1,134 +0,0 @@ -# Extension Development - -This section is designed for developers and covers how to setup, build, and test -the Adobe CC extension. - -The entire extension source lives here in the `tk-photoshopcc` engine in the -`extensions/basic` folder. This includes all of the pieces required by Adobe to -run as a CEP extension as well as the configuration and logic specific to -bootstrapping and running Shotgun Toolkit in a python process. - -For more information about Adobe CEP extensions, here are some useful resources: - -* [CEP Resources (Github)](https://github.com/Adobe-CEP/CEP-Resources) -* [David Berranca's Blog](http://www.davidebarranca.com/) -* [A Short Guide to HTML5 Extensions (Adobe)](http://www.adobe.com/devnet/creativesuite/articles/a-short-guide-to-HTML5-extensions.html) - -## Setup - -Before you can build and test the extension locally, you need to setup your -environment to test. - -### PlayerDebugMode - -As per the Adobe docs: - -> Applications will normally not load an extension unless it is -> cryptographically signed. However, during development we want to be able to -> quickly test an extension without having to sign it. To turn on debug mode: -> -> * On Mac, open the file `~/Library/Preferences/com.adobe.CSXS.7.plist` and add a -> row with key `PlayerDebugMode`, of type `String`, and value `1`. -> * On Windows, open the registry key `HKEY_CURRENT_USER/Software/Adobe/CSXS.7` -> and add a key named `PlayerDebugMode`, of type `String`, and value `1` -> -> You should only need to do this once. - -### Development Repositories - -If you're doing development on the `tk-photoshopcc` engine itself, or an app or -framework being loaded by the engine, you'll need to clone the config repo so -that you can make changes to it. The config used by the engine is the -[tk-config-pluginbasic](https://github.com/shotgunsoftware/tk-config-pluginbasic) -repo. Obviously, you'll also need to clone the repos for the bundle's you're -doing development on. - -The base configuration is defined in the extension's `info.yml`. You will need -to change the descriptor arguments to point to your development repository. Here -is an example: - -```yaml -base_configuration: - #type: app_store - #name: tk-config-pluginbasic - - type: dev - path: /path/to/my/dev/config/repo -``` - -Once you're pointing to your configuration development repo, you can change the -bundle descriptors in the configuration's environment files to point to the -development repos for the bundles you're working on. - -### Core Development and Testing - -In order to bootstrap into a specific core, you'll need to alter the descriptor -in the extension's `config/core/core_api.yml` file. Simply change the descriptor -arguments in that file to point to a local clone of `tk-core` or to a specific -branch or tag in github. - -## Building - -In order to build the extension, you'll need to have a local copy of core that -has the `developer/build_plugin.py` script available (`v0.18.27` or later -preferably). - -Building the extension requires running the `build_plugin.py` script from your -local core, supplying it with a source path (the path to the extension itself -in your development repo) and a destination path (the path where you want to -write the extension). - -Here is an example of building the extension: - -```shell -> cd ~/dev -> python tk-core/developer/build_plugin.py tk-photoshopcc/extensions/basic my_extension -``` - -The above command will write the built extension to the `~/dev/my_extension` -directory. In order for the extension to be picked up, it will need to live in -the CEP extensions directory: - -```shell -# windows -> C:\Users\[user name]\AppData\Roaming\Adobe\CEP\extensions\ - -# osx -> ~/Library/Application Support/Adobe/CEP/extensions/ -``` - -You can have the `build_plugin.py` script write directly to the extensions -directory, just be aware that the script will create a backup copy in the -same location if the destination folder already exists. Alternatively, you can -create a symlink in the extensions directory back to the build destination path -in your development area. - -## Testing - -Once you've followed the steps above, the extension should be available the -next time you startup Photoshop. If you do not see it, double check the steps -above. - -The extension has a flyout menu with 2 options useful for testing and debugging: - -* **Debug Console...** - Opens the default browser and displays -information about the extension and a clickable link to debug it. -* **Reload** - Reloads the extension, including restarting the -external python process. - -As you make changes in your code, you will need to rebuild the extension using -the steps above. You can use the **Reload** menu item to reload the extension -after it has been rebuilt. - -# Building the Plugin for Release - -Coming soon... - -## Clean up dev descriptors - -# TODO: -* consider the case where you have the officially released extension installed -system wide, but you also want to do local development. Revisit once we've done -the initial release for beta testing. -* consider all the side effects of reloading the extension. does it actually -handle reconnecting properly. do we get a completely fresh environment? diff --git a/plugins/basic/css/style.css b/plugins/basic/css/style.css index 80dedae..a79ab5c 100644 --- a/plugins/basic/css/style.css +++ b/plugins/basic/css/style.css @@ -61,10 +61,12 @@ a:link.sg_value_link:hover { } /* indicator for external links in context header */ +/* a:link.sg_value_link:after { content: url(../images/external_link.png); margin-left: 4px; } +*/ /* white on hover */ a:hover { @@ -109,13 +111,12 @@ a:active { /* the first image that shows up when panel is loading */ #loading_img { - position: absolute; - top: 50%; - left: 50%; - width: 270px; - height: 193px; - margin-top: -96px; - margin-left: -135px; + position: fixed; + margin: auto; + left: 0; + right: 0; + top: 0; + bottom: 0; } /* error display */ @@ -176,26 +177,26 @@ div.sg_error { /* grayscale images in favorites until hovered */ .sg_panel_command_img { - -webkit-filter: grayscale(1); - width: 26px; - height: 26px; + -webkit-filter: grayscale(1) brightness(.75); + width: 19px; + height: 19px; } .sg_panel_command_other_img { - width: 26px; - height: 26px; + width: 19px; + height: 19px; } .sg_panel_command_img:hover { - -webkit-filter: grayscale(0); + -webkit-filter: grayscale(0) brightness(1); } /* regular command button */ .sg_command_button { display: inline-block; border-radius: 3px; - margin: 4px; - padding: 2px; + margin: 1px; + padding: 4px; border: 1px solid rgba(0, 0, 0, 0); } @@ -218,10 +219,11 @@ div.sg_error { /* a single command div */ .sg_panel_command { border-bottom: 1px solid #373737; - height: 34px; - padding-left: 18px; + height: 32px; + padding-left: 16px; padding-right: 18px; - -webkit-filter: grayscale(1); + padding-top: 5px; + -webkit-filter: grayscale(1) brightness(.75); color: #D7D7D7; } @@ -236,7 +238,7 @@ div.sg_error { .sg_panel_command:hover { background-color: #6B6B6B; color: white; - -webkit-filter: grayscale(0); + -webkit-filter: grayscale(0) brightness(1); } .sg_panel_command:hover a:link { @@ -370,6 +372,13 @@ div.sg_error { display: none; } +#sg_panel_console_tag { + display: inline; + color: #373737; + border-right: 1px solid #373737; + padding-right: 3px; +} + #sg_panel_console_header { position: fixed; top: 0px; @@ -396,20 +405,57 @@ div.sg_error { #sg_log_message { color: #D7D7D7; display: inline; + padding-left: 3px; } #sg_log_message_debug { color: #18A7E3; display: inline; + padding-left: 3px; } #sg_log_message_warn { color: rgb(226, 159, 74); display: inline; + padding-left: 3px; } #sg_log_message_error { color: rgb(226, 74, 74); display: inline; + padding-left: 3px; +} + +#sg_unknown_context_table { + width: 100%; + padding: 10px; + vertical-align: top; +} + +#sg_unknown_context_thumbnail { + display: table-cell; + vertical-align: top; + text-align: center; } +#sg_unknown_context_title { + text-align: left; + vertical-align: top; + color: #D7D7D7; + padding-left: 8px; + padding-top: 0; + width: 100%; + flex: 1; + display: inline-block; +} + +#sg_unknown_context_details{ + text-align: left; + vertical-align: top; + color: #777777; + padding-left: 8px; + padding-top: 0px; + width: 100%; + flex: 1; + display: inline-block; +} diff --git a/plugins/basic/images/sg_logo.png b/plugins/basic/images/sg_logo.png new file mode 100644 index 0000000..2a4fefb Binary files /dev/null and b/plugins/basic/images/sg_logo.png differ diff --git a/plugins/basic/images/sg_logo_loading.png b/plugins/basic/images/sg_logo_loading.png deleted file mode 100644 index 306c3fc..0000000 Binary files a/plugins/basic/images/sg_logo_loading.png and /dev/null differ diff --git a/plugins/basic/images/sg_logo_with_text.png b/plugins/basic/images/sg_logo_with_text.png new file mode 100644 index 0000000..d2e0d18 Binary files /dev/null and b/plugins/basic/images/sg_logo_with_text.png differ diff --git a/plugins/basic/js/shotgun/constants.js b/plugins/basic/js/shotgun/constants.js index 2e89123..8a7815a 100644 --- a/plugins/basic/js/shotgun/constants.js +++ b/plugins/basic/js/shotgun/constants.js @@ -36,12 +36,14 @@ sg_constants.product_info = { // Photoshop PHSP: { + display_name: "Photoshop", tk_engine_name: "tk-photoshop", debug_url: "http://localhost:45216" }, // Photoshop alt PHXS: { + display_name: "Photoshop", tk_engine_name: "tk-photoshop", debug_url: "http://localhost:45217" }, @@ -49,6 +51,7 @@ sg_constants.product_info = { // After Effects AEFT: { + display_name: "After Effects", tk_engine_name: "tk-aftereffects", debug_url: "http://localhost:45218" }, @@ -56,6 +59,7 @@ sg_constants.product_info = { // Premiere Pro PPRO: { + display_name: "Premiere Pro", tk_engine_name: "tk-premiere", debug_url: "http://localhost:45219" } @@ -84,7 +88,8 @@ sg_constants.panel_div_ids = { sg_constants.python_error_codes = { EXIT_STATUS_CLEAN: 0, EXIT_STATUS_ERROR: 100, - EXIT_STATUS_NO_PYSIDE: 101 + EXIT_STATUS_NO_PYSIDE: 101, + EXIT_STATUS_PYTHON_FAIL: 102 }; // External link to pyside docs diff --git a/plugins/basic/js/shotgun/manager.js b/plugins/basic/js/shotgun/manager.js index 5ed97fc..25015aa 100644 --- a/plugins/basic/js/shotgun/manager.js +++ b/plugins/basic/js/shotgun/manager.js @@ -112,8 +112,12 @@ sg_manager.Manager = new function() { ); } catch (error) { + + const error_lines = error.stack.split(/\r?\n/); + const stack_err_msg = error_lines[0]; + const message = "There was an unexpected error during startup of " + - "the Adobe Shotgun integration."; + "the Adobe Shotgun integration:

" + stack_err_msg; // log the error in the event that the panel has started and the // user can click the console @@ -144,26 +148,6 @@ sg_manager.Manager = new function() { self.shutdown(); }; - this.set_commands = function(commands) { - // emits the current commands update for listeners to respond to - sg_manager.UPDATE_COMMANDS.emit(commands); - }; - - this.set_context_fields = function(context_fields) { - // emits the current context fields update for listeners to respond to - sg_manager.UPDATE_CONTEXT_FIELDS.emit(context_fields); - }; - - this.set_context_thumbnail = function(context_thumbnail) { - // emits the current context thumbnail update for listeners to respond to - sg_manager.UPDATE_CONTEXT_THUMBNAIL.emit(context_thumbnail); - }; - - this.context_about_to_change = function() { - // emits the context about to change signal for listeners to respond to - sg_manager.CONTEXT_ABOUT_TO_CHANGE.emit(); - }; - this.shutdown = function() { // Ensure all the manager's components are shutdown properly // @@ -287,8 +271,7 @@ sg_manager.Manager = new function() { sg_logging.debug("Spawning child process... "); sg_logging.debug("Python executable: " + python_exe_path); sg_logging.debug("Current working directory: " + plugin_python_path); - sg_logging.debug("Executing command: "); - sg_logging.debug(" " + + sg_logging.debug("Executing command: " + [ python_exe_path, plugin_bootstrap_py, @@ -322,15 +305,17 @@ sg_manager.Manager = new function() { throw error; } - sg_logging.debug("Child process spawned! PID: " + self.python_process.pid) + self.python_process.on("error", function(error) { + sg_logging.error("Python process error: " + error); + }); // log stdout from python process - self.python_process.stdout.on("data", function (data) { + self.python_process.stdout.on("data", function(data) { sg_logging.python(data.toString()); }); // log stderr from python process - self.python_process.stderr.on("data", function (data) { + self.python_process.stderr.on("data", function(data) { sg_logging.python(data.toString()); }); @@ -367,9 +352,6 @@ sg_manager.Manager = new function() { const _get_open_port = function(port_found_callback) { // Find an open port and send it to the supplied callback - // TODO: allow specification of an explicit port to use for debugging - // perhaps something that is exposed during the build process? - // https://nodejs.org/api/http.html#http_class_http_server const http = require('http'); @@ -519,6 +501,8 @@ sg_manager.Manager = new function() { // setup listeners for any events that need to be processed by the // manager + sg_logging.debug("Setting up event listeners..."); + // ---- Events from the panel sg_panel.REQUEST_MANAGER_RELOAD.connect(_reload); @@ -556,6 +540,8 @@ sg_manager.Manager = new function() { // Keep an eye on the active document. setInterval(_active_document_check, active_document_interval); + + sg_logging.debug("Event listeners created."); }; const _emit_python_critical_error = function(error) { diff --git a/plugins/basic/js/shotgun/manager_events.js b/plugins/basic/js/shotgun/manager_events.js index 3e72966..afad54d 100644 --- a/plugins/basic/js/shotgun/manager_events.js +++ b/plugins/basic/js/shotgun/manager_events.js @@ -22,6 +22,9 @@ sg_event.create_event(sg_manager, "UPDATE_CONTEXT_DISPLAY"); // typically as an async response to a REQUEST_STATE event from the panel sg_event.create_event(sg_manager, "UPDATE_CONTEXT_THUMBNAIL"); +// sent when the python side cannot determine a context for the current document +sg_event.create_event(sg_manager, "UNKNOWN_CONTEXT"); + // sent just before a context change from python sg_event.create_event(sg_manager, "CONTEXT_ABOUT_TO_CHANGE"); diff --git a/plugins/basic/js/shotgun/panel.js b/plugins/basic/js/shotgun/panel.js index 56b6f05..34bbae9 100644 --- a/plugins/basic/js/shotgun/panel.js +++ b/plugins/basic/js/shotgun/panel.js @@ -52,6 +52,8 @@ sg_panel.Panel = new function() { // clears any state for the current context. _context_thumbnail_data = undefined; + _build_flyout_menu([]); + }; this.set_panel_loading_state = function() { @@ -63,7 +65,7 @@ sg_panel.Panel = new function() { _show_header(false); _set_contents( - ""); + ""); _show_info(true); _set_info( @@ -71,6 +73,47 @@ sg_panel.Panel = new function() { ); }; + this.set_unknown_context_state = function() { + // Clears the panel's contents and displays a message that it is disabled + + this.clear(); + + _set_bg_color("#222222"); + + _show_header(false); + _clear_messages(); + + var app_name = _cs_interface.getHostEnvironment().appName; + const app_display_name = sg_constants.product_info[app_name].display_name; + + _set_contents( + "" + + "" + + "" + + "" + + "" + + "
" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
" + + "Integration Disabled" + + "
" + + "The currently active file can't be associated with a " + + "Shotgun context. Try switching to another file or " + + "restarting " + app_display_name + "." + + "
" + + "
" + ); + + }; + this.set_context_loading_state = function() { // clears the current context and displays it as loading a new one. @@ -195,6 +238,15 @@ sg_panel.Panel = new function() { // TODO: display error in the panel } + // Here we send the "AppOnline" event strictly in the event of a manual + // reload/restart of the SG extension. For initial launch of PS, this + // will be a no-op since the manager will already be running. For the + // reload scenario, this is the jumpstart that the manager requires to + // start up. + const event_type = "com.adobe.csxs.events.AppOnline"; + var event = new CSEvent(event_type, "APPLICATION"); + event.extensionId = _cs_interface.getExtensionID(); + _cs_interface.dispatchEvent(event); }; this.on_unload = function() { @@ -351,22 +403,18 @@ sg_panel.Panel = new function() { commands_html += "
" + - "" + - "" + - "" + - "" + - "" + + "
" + "" + - "" + - "
" + + "" + "" + "" + "" + "" + + "" + "" + @@ -413,7 +461,7 @@ sg_panel.Panel = new function() { this.trigger_command = function(command_id, command_display) { // Emits the signal to launch the supplied command id. - // Also shows a tmp message in the footer to confirm user click + // Also shows a tmp message to confirm user click // show the progress message temporarily _set_info("Launching: " + command_display); @@ -458,11 +506,16 @@ sg_panel.Panel = new function() { Enabled="true" \ Checked="false"/>'; - if ( process.env.SHOTGUN_ADOBE_NETWORK_DEBUG || process.env.SHOTGUN_ADOBE_TESTS_ROOT || process.env.TK_DEBUG ) { + if (process.env.SHOTGUN_ADOBE_NETWORK_DEBUG || + process.env.SHOTGUN_ADOBE_TESTS_ROOT || + process.env.TK_DEBUG || + process.env.SHOTGUN_ADOBE_DEVELOP) { + flyout_xml += ''; + flyout_xml += '" + - "Shotgun Support" + + "support@shotgunsoftware.com" + "." + "

" + "
" + @@ -591,10 +644,10 @@ sg_panel.Panel = new function() { } else { contents_html += "
If you encounter this problem consistently or have any " + - "other questions, please send the steps to reproduce to: " + + "other questions, please send the steps to reproduce to " + "" + - "Shotgun Support" + + "support@shotgunsoftware.com" + "."; } @@ -624,9 +677,14 @@ sg_panel.Panel = new function() { _show_header(false); + var python_display = "system python"; + if (process.env.SHOTGUN_ADOBE_PYTHON) { + python_display = "" + process.env.SHOTGUN_ADOBE_PYTHON + ""; + } + var contents_html = "
" + "The Shotgun integration failed to load because PySide" + - " is not installed." + + " is not installed (Running " + python_display + ")." + "
"; contents_html += @@ -647,14 +705,17 @@ sg_panel.Panel = new function() { "*** Please enter your questions here... ***\n\n" ); + var app_name = _cs_interface.getHostEnvironment().appName; + const app_display_name = sg_constants.product_info[app_name].display_name; + contents_html += - "
Once you have PySide installed, restart this " + - "application to load the Shotgun integration.

" + + "
Once you have PySide installed, restart " + + app_display_name + " to load the Shotgun integration.

" + "If you believe the error is incorrect or you have any further " + - "questions, please contact: " + + "questions, please contact " + "" + - "Shotgun Support" + + "support@shotgunsoftware.com" + "."; contents_html = "
" + contents_html + "
"; @@ -815,21 +876,25 @@ sg_panel.Panel = new function() { div_id = "sg_log_message_error" } - // just a little indicator so that we know if the log message came from - // (javascript or python) when looking in the panel console. - if (log_source == "js") { - msg = " > " + msg; - } else { - msg = ">> " + msg; - } + // append the
 element to the log div
+        const log = document.getElementById("sg_panel_console_log");
 
         // create a 
 element and insert the msg
         const node = document.createElement("pre");
         node.setAttribute("id", div_id);
         node.appendChild(document.createTextNode(msg));
 
-        // append the 
 element to the log div
-        const log = document.getElementById("sg_panel_console_log");
+        // just a little indicator so that we know if the log message came from
+        // (javascript or python) when looking in the panel console.
+        const tag = document.createElement("pre");
+        tag.setAttribute("id", "sg_panel_console_tag");
+        if (log_source == "js") {
+            tag.appendChild(document.createTextNode("js"))
+        } else {
+            tag.appendChild(document.createTextNode("py"))
+        }
+
+        log.appendChild(tag);
         log.appendChild(node);
         log.appendChild(document.createElement("br"));
 
@@ -1014,6 +1079,13 @@ sg_panel.Panel = new function() {
             }
         );
 
+        // Sets the panel into a state where the context is not known
+        sg_manager.UNKNOWN_CONTEXT.connect(
+            function(event) {
+                self.set_unknown_context_state();
+            }
+        );
+
         // Clears the current context
         sg_manager.CONTEXT_ABOUT_TO_CHANGE.connect(
             function(event) {
diff --git a/plugins/basic/js/shotgun/socket_io_manager.js b/plugins/basic/js/shotgun/socket_io_manager.js
index 1d95b6f..07cda7a 100644
--- a/plugins/basic/js/shotgun/socket_io_manager.js
+++ b/plugins/basic/js/shotgun/socket_io_manager.js
@@ -399,6 +399,12 @@ sg_socket_io.SocketManager = new function() {
                 sg_manager.UPDATE_CONTEXT_THUMBNAIL.emit(context_thumbnail);
             });
 
+            socket.on("set_unknown_context", function() {
+                // The context is unknown
+                sg_logging.debug("Sending unknown context signal from the client.");
+                sg_manager.UNKNOWN_CONTEXT.emit();
+            });
+
             socket.on("context_about_to_change", function() {
                 // The context is about to change
                 sg_logging.debug("Sending context about to change from client.");
diff --git a/plugins/basic/python/plugin_bootstrap.py b/plugins/basic/python/plugin_bootstrap.py
index 324c563..ce77a0b 100644
--- a/plugins/basic/python/plugin_bootstrap.py
+++ b/plugins/basic/python/plugin_bootstrap.py
@@ -70,6 +70,7 @@ def get_sgtk_logger(sgtk):
 
     # initializes the file where logging output will go
     sgtk.LogManager().initialize_base_file_handler(engine_name)
+    sgtk_logger.debug("Log dir: %s" % (sgtk.LogManager().log_folder))
 
     return sgtk_logger, bootstrap_log_handler
 
diff --git a/python/startup/bootstrap.py b/python/startup/bootstrap.py
index f4f7139..f4d632d 100644
--- a/python/startup/bootstrap.py
+++ b/python/startup/bootstrap.py
@@ -8,9 +8,23 @@
 # agreement to the Shotgun Pipeline Toolkit Source Code License. All rights 
 # not expressly granted therein are reserved by Shotgun Software Inc.
 
-import sys
+import glob
 import os
+import re
+import shutil
+import sys
+import tempfile
+import zipfile
+
 import sgtk
+from sgtk.util.filesystem import (
+    backup_folder,
+    ensure_folder_exists,
+    move_folder,
+)
+
+logger = sgtk.LogManager.get_logger(__name__)
+
 
 def bootstrap(engine_name, context, app_path, app_args, **kwargs):
     """
@@ -38,5 +52,206 @@ def bootstrap(engine_name, context, app_path, app_args, **kwargs):
         os.pathsep.join(sys.path),
     )
 
+    # the basic plugin needs to be installed in order to launch the adobe
+    # engine. we need to make sure the plugin is installed and up-to-date.
+    # will only run if SHOTGUN_ADOBECC_DISABLE_AUTO_INSTALL is not set.
+    if not "SHOTGUN_ADOBECC_DISABLE_AUTO_INSTALL" in os.environ:
+        logger.debug("Ensuring adobe extension is up-to-date...")
+        try:
+            _ensure_extension_up_to_date(context)
+        except Exception, e:
+            import traceback
+            exc = traceback.format_exc()
+            raise Exception(
+                "There was a problem ensuring the Adobe integration extension "
+                "was up-to-date with your toolkit engine. If this is a "
+                "recurring issue please contact support@shotgunsoftware.com. "
+                "The specific error message encountered was:\n'%s'." % (exc,)
+            )
+
     return (app_path, app_args)
 
+
+def _ensure_extension_up_to_date(context):
+    """
+    Ensure the basic adobe extension is installed in the OS-specific location
+    and that it matches the extension bundled with the installed engine.
+
+    :param context:  The context to use when bootstrapping.
+    """
+
+    extension_name = "com.shotgunsoftware.basic.adobecc"
+
+    # the CEP install directory is OS-specific
+    if sys.platform == "win32":
+        app_data = os.getenv("APPDATA")
+    elif sys.platform == "darwin":
+        app_data = os.path.expanduser("~/Library/Application Support")
+    else:
+        raise Exception("This engine only runs on OSX & Windows.")
+
+    # the adobe CEP install directory. This is where the extension is stored.
+    adobe_cep_dir = os.path.join(app_data, "Adobe", "CEP", "extensions")
+    logger.debug("Adobe CEP extension dir: %s" % (adobe_cep_dir,))
+
+    # make sure the directory exists. create it if not.
+    if not os.path.exists(adobe_cep_dir):
+        logger.debug("Extension folder does not exist. Creating it.")
+        ensure_folder_exists(adobe_cep_dir)
+
+    # get the path to the installed engine's .zxp file. the extension_name file i
+    # is 3 levels up from this file.
+    bundled_ext_path = os.path.abspath(
+        os.path.join(
+            os.path.dirname(__file__),
+            os.pardir,
+            os.pardir,
+            "%s.zxp" % (extension_name,)
+        )
+    )
+
+    if not os.path.exists(bundled_ext_path):
+        raise Exception(
+            "Could not find bundled extension. Expected: '%s'" %
+            (bundled_ext_path,)
+        )
+
+    # now get the version of the bundled extension
+    version_file = "%s.version" % (extension_name,)
+
+    bundled_version_file_path = os.path.abspath(
+        os.path.join(
+            os.path.dirname(__file__),
+            os.pardir,
+            os.pardir,
+            version_file
+        )
+    )
+
+    if not os.path.exists(bundled_version_file_path):
+        raise Exception(
+            "Could not find bundled version file. Expected: '%s'" %
+            (bundled_version_file_path,)
+        )
+
+    # get the bundled version from the version file
+    with open(bundled_version_file_path, "r") as bundled_version_file:
+        bundled_version = bundled_version_file.read().strip()
+
+    # check to see if the extension is installed in the CEP extensions directory
+    installed_ext_dir = os.path.join(adobe_cep_dir, extension_name)
+
+    # if not installed, install it
+    if not os.path.exists(installed_ext_dir):
+        logger.debug("Extension not installed. Installing it!")
+        _install_extension(bundled_ext_path, installed_ext_dir)
+        return
+
+    # ---- already installed, check for udpate
+
+    logger.debug("Bundled extension's version is: %s" % (bundled_version,))
+
+    # get the version from the installed extension's build_version.txt file
+    installed_version_file_path = os.path.join(installed_ext_dir, version_file)
+
+    logger.debug(
+        "The installed version file path is: %s" %
+        (installed_version_file_path,)
+    )
+
+    if not os.path.exists(installed_version_file_path):
+        raise Exception(
+            "Could not find installed version file '%s'" %
+            (installed_version_file_path,)
+        )
+
+    # the version of the installed extension
+    installed_version = None
+
+    # get the installed version from the installed version info file
+    with open(installed_version_file_path, "r") as installed_version_file:
+        logger.debug("Extracting the version from the installed extension.")
+        installed_version = installed_version_file.read().strip()
+
+    if installed_version is None:
+        raise Exception(
+            "Could not determine version for the installed extension.")
+
+    logger.debug("Installed extension's version is: %s" % (installed_version,))
+
+    from sgtk.util.version import is_version_older
+    if bundled_version != "dev" and installed_version != "dev":
+        if (bundled_version == installed_version or
+           is_version_older(bundled_version, installed_version)):
+
+            # the bundled version is the same or older. or it is a 'dev' build
+            # which means always install that one.
+            logger.debug(
+                "Installed extension is equal to or newer than the bundled "
+                "build. Nothing to do!"
+            )
+            return
+
+    # ---- extension in engine is newer. update!
+
+    if bundled_version == "dev":
+        logger.debug("Installing the bundled 'dev' version of the extension.")
+    else:
+        logger.debug(
+            "Bundled extension build is newer than the installed extension " +
+            "build! Updating..."
+        )
+
+    # move the installed extension to the backup directory
+    backup_ext_dir = tempfile.mkdtemp()
+    logger.debug("Backing up the installed extension to: %s" % (backup_ext_dir,))
+    try:
+        backup_folder(installed_ext_dir, backup_ext_dir)
+    except Exception:
+        shutil.rmtree(backup_ext_dir)
+        raise Exception("Unable to create backup during extension update.")
+
+    # now remove the installed extension
+    logger.debug("Removing the installed extension directory...")
+    try:
+        shutil.rmtree(installed_ext_dir)
+    except Exception:
+        # try to restore the backup
+        move_folder(backup_ext_dir, installed_ext_dir)
+        raise Exception("Unable to remove the old extension during update.")
+
+    # install the bundled .zxp file
+    _install_extension(bundled_ext_path, installed_ext_dir)
+
+    # if we're here, the install was successful. remove the backup
+    try:
+        logger.debug("Install success. Removing the backed up extension.")
+        shutil.rmtree(backup_ext_dir)
+    except Exception:
+        # can't remove temp dir. no biggie.
+        pass
+
+def _install_extension(ext_path, dest_dir):
+    """
+    Installs the supplied extension path by unzipping it directly into the
+    supplied destination directory.
+
+    :param ext_path: The path to the .zxp extension.
+    :param dest_dir: The CEP extension's destination
+    :return:
+    """
+
+    logger.debug(
+        "Installing bundled extension: '%s' to '%s'" % (ext_path, dest_dir))
+
+    # make sure the bundled extension exists
+    if not os.path.exists(ext_path):
+        raise Exception(
+            "Expected CEP extension does not exist. Looking for %s" %
+            (ext_path,)
+        )
+
+    # extract the .zxp file into the destination dir
+    with zipfile.ZipFile(ext_path, 'r') as ext_zxp:
+        ext_zxp.extractall(dest_dir)
+
diff --git a/python/tk_adobecc/adobe_bridge.py b/python/tk_adobecc/adobe_bridge.py
index a055807..b33ae05 100644
--- a/python/tk_adobecc/adobe_bridge.py
+++ b/python/tk_adobecc/adobe_bridge.py
@@ -216,7 +216,17 @@ def send_context_thumbnail(self, context_thumbnail):
         self.logger.debug("Sending context thumb path: %s" % json_context_thumbnail)
         self._io.emit("set_context_thumbnail", json_context_thumbnail)
 
+    def send_unknown_context(self):
+        """
+        Sent when a context can not be determined for the current file.
+        """
+        self.logger.debug("Alerting js that there is no context")
+        self._io.emit("set_unknown_context")
+
     def context_about_to_change(self):
+        """
+        Sent just before the context is about to change.
+        """
         self.logger.debug("Sending context about to change message.")
         self._io.emit("context_about_to_change")