Automating publishing of FLA files; calling Process.Start multiple times
Asked Answered
G

1

4

Problem Background

I've created a "One-Click" publication process for my Flash (*.fla) project files with a C# program that performs the following operations:

  • Updates a static "version" variable in two main project files (one for each FLA project)
  • Alters an embedded JSFL template file (automates the opening/publishing of a specific FLA file) and writes it to disk to be used in the next step
  • Calls Flash Professional via Process.Start, passing the path to flash.exe as well as the JSFL filename as an argument, so flash runs the JSFL file and publishes the project
  • Publishes project files to my web server (simple backup/file-copy via mapped shared drive over Win7<->WinServer2012 VPN tunnel)

The UI has three buttons, one to publish the first project, one to publish the 2nd project, and one to publish both projects. The second project depends on the first project, so if and when the "publish both" button is clicked, it should completely finishing publishing the first file before publishing the second.

For each project, it reads the main document class AS file into memory as a string and uses a Regex to update specific static "version" variables to have a current timestamp. It then rewrites the files with the updated version. The purpose of the "version" variable is to be displayed at runtime in browser so I'm certain I'm testing the most recently compiled version of the project.

Flash Professional accepts a JSFL file name as a command line argument, but does not allow further arguments to be passed to that JSFL file, so the program alters a JSFL template to include the proper parameters and passes the customized temporary JSFL file to Flash Professional via Process.Start.

Publication to web occurs via another generic features that lets me specify a list of source and target paths (with optional timestamped backup of each file), that automates the backup and copying of specific published files to my web server.


Problem

First of all, I should mention that everything works fine when I'm publishing only one project file, and the issue I'm trying to solve is one of timing or event signaling.

Flash is a single-instance-only application, so running Process.Start will either start Flash Professional if it's not already running and run the JSFL script, or it will run the JSFL script in the existing instance of Flash Professional.

The first problem is after calling Process.Start, I cannot call waitForExit for the task to complete, because Flash stays open. If Flash is already open, waitForExit actually returns pretty quickly, because the 2nd Flash.exe instance will close after forwarding the command to the primary instance. Single-instance-only applications don't actually prevent a 2nd process from starting, they just quickly kill the 2nd process when it detects one is already running and forwards the command to it. For that reason, I cannot simply wait on the process to exit, since Flash may or may not be open.

Assuming I don't wait at all, my application will rather quickly call Process.Start twice, once for each project, passing a unique JSFL script filename to run for each one. The problem with this is that the first call seems to be discarded. I'm not sure if this behavior is rooted in the Windows OS or in the Flash Professional application. This occurs when Flash is not already open. The initial Process.Start call and corresponding parameters should be what activates Flash, so I would have expected that to make it through, however as Flash is starting up, when it finally shows the main window, it's only the 2nd script that runs.

Interstingly, if Flash is already started, both scripts appear to run despite their rapid activation (I see both documents open in the IDE), but the simultaneous publication actually causes Flash to crash (the main window disappears and the process terminates abruptly without any error).

So I need a way to coordinate issuing these Process.Start commands. Thankfully, the JSFL "publish" method is synchronous and JSFL is capable of executing command line commands, so perhaps once the publish method returns I can just call some external EXE to function as a coordination mechanism to detect when each script has completed its work before executing the next one? Does anyone have any experience with this kind of inter-process communication who could help me out?

TL;DR

I need to know how to build a simple executable such that when called from the command line, it sends a message to a specific external process, indicating that an operation has completed. Basically, a JSFL script running in Flash Professional must call the exe via the "undocumented" FLfile.runCommandLine method once the file has finished publishing, and that exe must then notify my automation program so it knows Flash has finished publishing the file and is ready to run another JSFL script to publish the next file.

Garret answered 7/5, 2014 at 18:27 Comment(0)
G
4

I solved this problem with the following project: http://www.codeproject.com/Articles/17606/NET-Interprocess-Communication

It offers a simple, zero-configuration implementation of IPC as a class library.

I added it to my automation program, and allowed the executable to run with arguments indicating it should signal the main instance and shut down. The main logic just checks: Environment.GetCommandLineArgs() for flags indicating it should send an IPC message and close instead of actually displaying the main form.

Here is the full implementation of the main program's signaling system:

static class Program
{
    private static readonly string MUTEX_AND_CHANNEL_NAME = "FlashPublishingAutomation";
    private static bool acquired_app_lock = false;
    private static Mutex app_lock;

    private static XDListener listener;
    public static ManualResetEvent publishCompleteSignal = new ManualResetEvent( true );

    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        string[] args = Environment.GetCommandLineArgs();
        if ((args.Length > 1) && (args[1] == "-publishcomplete"))
        {
            XDBroadcast.SendToChannel( MUTEX_AND_CHANNEL_NAME, "publishcomplete" );
            Application.Exit();
            return;
        }
        else
        {
            bool createdNew = false;
            MutexSecurity security = new MutexSecurity();
            MutexAccessRule rule = new MutexAccessRule( "Users", MutexRights.Synchronize | MutexRights.Modify | MutexRights.ReadPermissions, AccessControlType.Allow );
            security.AddAccessRule( rule );
            app_lock = new Mutex( false, "Global\\" + MUTEX_AND_CHANNEL_NAME, out createdNew, security ); //Name must start with "Global\\" in order to be a system-wide mutex for all logged on usesr.
            acquired_app_lock = app_lock.WaitOne( TimeSpan.Zero, true );

            if (!acquired_app_lock)
            {
                MessageBox.Show( "An instance of FlashPublishingAutomation is already running.\r\nOnly one instance is allowed." );
            }
            else
            {
                listener = new XDListener();
                listener.RegisterChannel( MUTEX_AND_CHANNEL_NAME );
                listener.MessageReceived += listener_MessageReceived;
                Application.ApplicationExit += Application_ApplicationExit;
                Application.Run(new Form1());
            }

            if (acquired_app_lock)
                app_lock.ReleaseMutex();
            app_lock.Close();
        }
    }

    static void listener_MessageReceived(object sender, XDMessageEventArgs e)
    {
        switch (e.DataGram.Message)
        {
            case "publishcomplete":
                publishCompleteSignal.Set();
                break;
        }
    }

    static void Application_ApplicationExit(object sender, EventArgs e)
    {
        listener.MessageReceived -= listener_MessageReceived;
        listener.UnRegisterChannel( MUTEX_AND_CHANNEL_NAME );
    }
}

And the "publish" methods that are called when a button is clicked for a project (along with a "fillTemplate" method:

private static readonly string FLASH_PATH = @"C:\Program Files (x86)\Adobe\Adobe Flash CS6\Flash.exe";
public void publish( string fla_directory, string fla_filename, string jsfl_filename )
{
    Program.publishCompleteSignal.Reset();
    string template = fillTemplate( fla_directory, fla_filename );
    string curdir = Environment.CurrentDirectory;
    string tempJSFLfilepath = Path.Combine( curdir, jsfl_filename );
    File.WriteAllText( tempJSFLfilepath, template );
    Process p = Process.Start( FLASH_PATH, tempJSFLfilepath );
    Program.publishCompleteSignal.WaitOne( 30000 ); //wait for signal from JSFL runnCommandLine; timeout after 30 seconds; may want to increase this value if Flash needs time to startup or files take a long time to publish
}

private string fillTemplate( string fla_directory, string fla_filename )
{
    string fileuri = "file:///" + Path.Combine( fla_directory, fla_filename ).Replace( '\\','/' ); //convert path to file URI
    return EmbeddedResources.OpenAndPublishJSFLTemplate
        .Replace( "FLAFILEPATH", HttpUtility.JavaScriptStringEncode( fileuri ) )
        .Replace("FLAFILENAME", HttpUtility.JavaScriptStringEncode( fla_filename ) )
        .Replace("COMPLETECOMMAND", HttpUtility.JavaScriptStringEncode( "\"" + Application.ExecutablePath + "\"" + " -publishcomplete" ));
}

Also, here is the JSFL template that the automation program fills in before executing it in Flash. It is embedded as a string under EmbeddedResources.OpenAndPublishJSFLTemplate`. The C# app replaces the FLAFILENAME, FLAFILEPATH, and COMPLETECOMMAND strings with the target FLA filename, FLA uri (of the form file:///path_to_FLA), and finally the path to the C# app itself as implemented above (plus the "-publishcomplete" switch). The C# app obtains its own path via System.Windows.Forms.Application.ExecutablePath. Once this template is filled, it is written to disk as a JSFL file and passed as an argument to Flash Professional (flash.exe) via Process.Start. Once the JSFL file publishes the FLA, it executes a new instance of the automation program with the "-publishcomplete" flag, which signals the main instance of the automation program to trigger a manual reset event.

In summary, the automation program resets an event before calling Flash, then waits for the signal once Flash finishes publishing, before trying to publish the next file.

var myDocument = null;
var wasOpen = false;
var isOpen = false;
var openDocs = fl.documents;
var filename = "FLAFILENAME"; //template parameter: the filename (name only, without the path) of the FLA file to publish
var filepath = "FLAFILEPATH"; //template parameter: the URI (beginning with "file:///") of the FLA file to publish
for(var i=0;i < openDocs.length; i++)
{
    myDocument = openDocs[i];
    if (myDocument.name.toLowerCase() == filename.toLowerCase())
    {
        wasOpen = true;
        isOpen = true;
        break;
    }
}
if (!wasOpen)
{
    myDocument = null;
    fl.openDocument( filepath );
    openDocs = fl.documents;
    for(var i=0;i < openDocs.length; i++)
    {
        myDocument = openDocs[i];
        if (myDocument.name.toLowerCase() == filename.toLowerCase())
        {
            isOpen = true;
            break;
        }
    }
}
if (isOpen && (myDocument != null))
{
    //Publish the document
    myDocument.publish(); //this method is synchronous, so it won't return until the publish operation has fully completed

    //Signal the automation program that publishing has completed (COMPLETECOMMAND should be 
    FLfile.runCommandLine("COMPLETECOMMAND"); //tempate parameter: the automation program's executable path plus the "-publishcomplete" argument
}
else
    alert( "Publishing of " + filename + " failed.  File was not open and failed to open." );

I'm actually very impressed with what I've created here. It achieves end-to-end publication (versioning, compilation, backup, and deployment to web server) of two very large (tens of thousands of lines; hundreds of classes) FLA projects with a single click of a button, and it all completes in under 10 seconds.

This could probably run even faster if the JSFL template was simplified to just call the silent FLA publishing method which doesn't even open the file, and allows you to specify the publish profile to use: fl.publishDocument( flaURI [, publishProfile] ).

Garret answered 7/5, 2014 at 18:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.