In-Process Dynamic Commands

Introduction

The Dynamic Command created in the previous chapter always runs in a separate process from the Extension instance. In this chapter, In-process Dynamic Commands will be introduced, which run within the process of an associated Extension instance. As a result of running "in-process", the dynamic commands can impact the execution of the Extension instance. 

On this page, we will cover the following:

  1. Add Asynchronous In-Process Dynamic Command to the "UE Task" Universal Template.
  2. Add Synchronous In-Process Dynamic Command to the "UE Task" Universal Template.
  3. Add a new text field to the "UE Task" Universal Template.
  4. Add a backing implementation for both Asynchronous and Synchronous In-Process Dynamic Commands to the extension.py file in the ue-task Extension project.
  5. Build and Upload the modified Extension.
  6. Execute the Asynchronous Dynamic Command.
  7. Execute the Synchronous Dynamic Command.

The steps below assume Controller version is at least 7.1.0.0

Step 1 - Add Asynchronous In-Process Dynamic Command to the "UE Task" Universal Template

Go back to the "UE Task" Universal Template form.  

On the Commands tab, click the "New" button and create the following “Async Print Word” command.

Note that the “Supported Status(es)” is set to Running only. This is because In-Process Dynamic commands can only run while the Extension instance is running. For all other task statuses, the command will be greyed out.

Also note that the "Execution Option" is explicitly set to In Process, and the Asynchronous option is checked.

Step 2 - Add Synchronous In-Process Dynamic Command to the "UE Task" Universal Template

Go back to the "UE Task" Universal Template form.  

On the Commands tab, click the "New" button and create the following “Sync Print Word” command.

Note that the “Supported Status(es)” is set to Running only. This is because In-Process Dynamic commands can only run while the Extension instance is running. For all other task statuses, the command will be greyed out.

Also note that the "Execution Option" is explicitly set to In Process, and the Asynchronous option is NOT checked.

Step 3 - Add a new text field to the "UE Task" Universal Template

Go back to the "UE Task" Universal Template form.  

On the Fields tab, click the "New" button and create the following “Word" field.

Ensure that the default value is set to ABCD

Step 4 - Add a backing implementation for both Asynchronous and Synchronous In-Process Dynamic Commands to the extension.py file

Open file ~/dev/extensions/sample-task/src/extension.py in your editor of choice.

Add the implementation of the async_print_word() and sync_print_word() Dynamic Commands:

reset_environment Dynamic Command
from __future__ import (print_function)
import time
from universal_extension import UniversalExtension
from universal_extension import ExtensionResult
from universal_extension import logger
from universal_extension import ui
from universal_extension.deco import dynamic_choice_command
from universal_extension.deco import dynamic_command


class Extension(UniversalExtension):
    """Required class that serves as the entry point for the extension
    """

    def __init__(self):
        """Initializes an instance of the 'Extension' class
        """
        # Call the base class initializer
        super(Extension, self).__init__()

    @dynamic_choice_command("primary_choice_field")
    def primary_choice_command(self, fields):
        """Dynamic choice command implementation for
        primary_choice_field.

        Parameters
        ----------
        fields : dict
            populated with the values of the dependent fields
        """
        return ExtensionResult(
            rc=0,
            message="Values for choice field: 'primary_choice_field'",
            values=["Start", "Pause", "Stop", "Build", "Destroy"]
        )

    @dynamic_choice_command("secondary_choice_field")
    def secondary_choice_command(self, fields):
        """Dynamic choice command implementation for
        secondary_choice_field.

        Parameters
        ----------
        fields : dict
            populated with the values of the dependent fields
        """
        return ExtensionResult(
            rc=0,
            message="Values for choice field: 'secondary_choice_field'",
            values=["System", "Command", "Application", "Transfer", "Evidence"]
        )

    @dynamic_command("reset_environment")
    def reset_environment(self, fields):
        """Dynamic command implementation for reset_environment command.

        Parameters
        ----------
        fields : dict
            populated with the values of the dependent fields
        """

        # Reset the state of the Output Only 'step' fields.
        out_fields = {}
        out_fields["step_1"] = "Initial"
        out_fields["step_2"] = "Initial"
        ui.update_output_fields(out_fields)

        return ExtensionResult(
            message="Message: Hello from dynamic command 'reset_environment'!",
            output=True,
            output_data='The environment has been reset.',
            output_name='DYNAMIC_OUTPUT')

    @dynamic_command("async_print_word")
    def async_print_word(self, fields):
        """
        Adds each letter of self.WORD to self.async_queue

        If curr_index is odd, then the function will sleep
        for 5 seconds before adding self.WORD to self.async_queue
        """

        curr_index = self.async_index

        if curr_index % 2 != 0:
            self.async_index += 1
            time.sleep(5)
        else:
            self.async_index += 1

        self.async_queue.append(self.WORD[curr_index])

        return ExtensionResult(
            message="",
            output=True,
            output_data="async_print_word()",
            output_name='DYNAMIC_ASYNC_OUTPUT')

    @dynamic_command("sync_print_word")
    def sync_print_word(self, fields):
        """
        Adds each letter of self.WORD to self.sync_queue

        If curr_index is odd, then the function will sleep
        for 5 seconds before adding self.WORD to self.sync_queue
        """

        curr_index = self.sync_index

        if curr_index % 2 != 0:
            self.sync_index += 1
            time.sleep(5)
        else:
            self.sync_index += 1

        self.sync_queue.append(self.WORD[curr_index])

        return ExtensionResult(
            message="",
            output=True,
            output_data="sync_print_word()",
            output_name='DYNAMIC_SYNC_OUTPUT')

    def extension_start(self, fields):
        """Required method that serves as the starting point for work performed
        for a task instance.

        Parameters
        ----------
        fields : dict
            populated with field values from the associated task instance
            launched in the Controller

        Returns
        -------
        ExtensionResult
            once the work is done, an instance of ExtensionResult must be
            returned. See the documentation for a full list of parameters that
            can be passed to the ExtensionResult class constructor
        """

        self.WORD = fields['word']
        self.async_index = 0
        self.sync_index = 0

        self.async_queue = []
        self.sync_queue = []

        # Sleep for 40 seconds to keep the Extension instance running
        # while the In-Process Dynamic Commands run
        time.sleep(40)

        # Log the Async and Sync Queues
        logger.info('Async Queue: %s' % ' -> '.join(self.async_queue))
        logger.info('Sync Queue: %s' % ' -> '.join(self.sync_queue))

        # Get the value of the 'action' field
        action = fields.get('action', [""])[0]

        if action.lower() == 'print':
            # Print to standard output...
            print("Hello STDOUT!")
        else:
            # Log to standard error...
            logger.info('Hello STDERR!')

        # Return the result with a payload containing a Hello message...
        return ExtensionResult(
            unv_output='Hello Extension!'
        )

Line 75-98The complete implementation of async_print_word() is defined. In short, the method adds each letter of self.WORD to self.async_queue. If self.async_index is odd, the function sleeps for 5 seconds before adding it to self.async_queue.
Line 100-123The complete implementation of sync_print_word() is defined. The method does the exact same thing as async_print_word() except it works with self.sync_queue and self.sync_index
Lines 143-156The extension_start() method grabs the value of the word field and initializes the queues and index variables. It sleeps for 40 seconds to keep the process running as required by the In-Process Dynamic commands. At the end, it prints out the queues.

Step 5 - Build and Upload the Extension Zip Archive Using the UIP VS Code Extension

Save all changes to extension.py.

From the VS Code command palette, execute the UIP: Push command as shown below:

Recall that the UIP: Push command builds and uploads the Extension zip archive

 Click here to expand uip-cli details...

Step 5 Supplemental - Build and Upload the Extension Zip Archive Using the CLI

Save all changes to extension.py.

From the command line, cd to the ~/dev/extensions/sample-task directory and execute the push command as shown below:

Recall that the push command builds and uploads the Extension zip archive

Step 6 - Execute the Asynchronous Dynamic Command

In the Controller, open a "UE Task Tasks" tab.  If the tab is already open, it must be closed and reopened before the new In-Process Dynamic Commands are available on the task instances.

As mentioned in the previous chapter, Dynamic Commands are available from the context menu generated by right-clicking on the task instance form and from the context menu generated by right-clicking on the task instance in a list of task instances (for example, in the Activity Monitor and/or the Instances tab of a task definition form).


It is recommended to read the entire step before executing the dynamic command as it needs to be done in a timely manner to get the proper results


Launch a new task, switch to the Instances tab, right-click on the task-instance (should be in "Running" status), and in the "UE Task Task Instance Commands" dropdown, click "Async Print Word". 



This displays an additional dialog, which can be used to pull in dependent fields from the task form if needed. This is optional functionality that is defined in the Universal template and made use of by the backing implementation in the extension archive.



Submit the Dynamic Command.

The immediate result of Dynamic Command is a new window showing the output results of the command.


Close the "Output" dialog, and execute "Async Print Word" 4 more times one after the other. Because of the 5 second sleep period when self.async_qeueue is an odd number, the "Output" dialog will not appear immediately for the 2nd (self.async_queue == 1) and 4th (self.async_queue == 3) execution. Don't wait for the dialog to appear. Immediately execute the next Dynamic command after the previous one. In total, the Asynchronous Dynamic Command should be executed 5 times.

After all commands are executed, wait until the task goes to completion. It should end up in "Success" state. 


Once finished, right-click on the task instance and click "Retrieve Output" or use the VS Code Extension's "UIP: Task Output" command to retrieve the output as shown below:


Now, let's examine the output. It will show us what exactly "Asynchronous" means in the context of Dynamic commands.

Recall that the default value of the word field was ABCD, but the async queue that's logged in STDERR is: A → C → B → D. The and B are flipped because of how the Asynchronous Dynamic Command executes:

  • When "Async Print Word" is first clicked, self.async_index is 0 (even), so the method does not sleep for 5 seconds and immediately adds to self.async_queue
  • When "Async Print Word" is clicked for the second time, self.async_index is 1 (odd), so the method sleeps for 5 seconds before adding B to self.async_queue
  • While the second "Async Print Word" is sleeping, "Async Print Word" is clicked for the third time where self.async_index is 2 (even), so the method does not sleep for 5 seconds and immediately adds C to self.async_queue
  • After the third instance of "Async Print Word" is finished, the fourth instance of "Async Print Word" is launched where self.async_index is 3 (odd), so it will sleep for 5 seconds before adding D to self.async_queue
  • While the fourth instance is sleeping, the second instance will have finished which will result in adding B to self.async_index. At the same time, the fifth and last instance of "Async Print Word" is launched where self.async_index is 4 (even).
  • The fifth instance will end up with an internal failure since index 4 is out of bounds. However, this will NOT impact the main Extension instance or any of the other running Dynamic Command instances.
  • After a couple seconds, the fourth instance will finish and add D to self.async_queue

As noted above, any exceptions in a Dynamic Command instance will not affect the Extension instance or any other Dynamic Command instances. We can see this clearly by examining the timestamps of the Exception (09:20:19) vs. the log statements (09:20:38).

The exception occurred first, but that did not prevent the log statements from being printed in extension_start()

Step 7 - Execute the Synchronous Dynamic Command

In the Controller, open a "UE Task Tasks" tab.  If the tab is already open, it must be closed and reopened before the new In-Process Dynamic Commands are available on the task instances.

As mentioned in the previous chapter, Dynamic Commands are available from the context menu generated by right-clicking on the task instance form and from the context menu generated by right-clicking on the task instance in a list of task instances (for example, in the Activity Monitor and/or the Instances tab of a task definition form).


It is recommended to read the entire step before executing the dynamic command as it needs to be done in a timely manner to get the proper results


Launch a new task, switch to the Instances tab, right-click on the task-instance (should be in "Running" status), and in the "UE Task Task Instance Commands" dropdown, click "Sync Print Word". 



This displays an additional dialog, which can be used to pull in dependent fields from the task form if needed. This is optional functionality that is defined in the Universal template and made use of by the backing implementation in the extension archive.



Submit the Dynamic Command.

The immediate result of Dynamic Command is a new window showing the output results of the command.


Close the "Output" dialog, and execute "Sync Print Word" 4 more times one after the other. In total, the Synchronous Dynamic Command should be executed 5 times. Note that the commands must be executed immediately after the previous one.

After all commands are executed, wait until the task goes to completion. It should end up in "Success" state. 


Once finished, right-click on the task instance and click "Retrieve Output" or use the VS Code Extension's "UIP: Task Output" command to retrieve the output as shown below:


Notice that unlike the Asynchronous Dynamic Command, the Synchronous one logs the word in order: A → B → C → D. This is because a Synchronous Dynamic Command cannot start processing until all previously received Synchronous Dynamic Commands have completed. 

Similar to the Asynchronous Dynamic Command, the Exception does not impact the Extension instance or any other Dynamic Command instances.

Step 8 - Update the Local template.json

In steps 1 to 3, we modified the Universal Template by adding new Dynamic Commands. 

Recall that inside ~/dev/extensions/sample-task/src/templates, there is a template.json file. This file should correspond to the Universal Template in the Controller. 

Right now, they are not both the same. The Controller's version of template.json has the In-Process Dynamic Command changes. To grab those changes, use the pull command as shown below:

UIP VS Code Extension

 Click here to expand uip-cli details...

Step Supplemental - CLI


Now, both the local and Controller's version of the Universal Template are the same.

< Previous     Next >