Skip to content

Forking Scripts

One of the precepts of FaceSpan programming is that your event handlers should run as quickly as possible to avoid blocking the FaceSpan user interface.

However, this is at odds with how AppleScript is commonly used to orchestrate the actions of other applications. One generally begins a long process that involves directing one or more applications to complete a task. Even if the AppleScript code is short, it may have to wait for another application to complete a lengthy operation.

The question becomes how best to accomplish this kind of AppleScript automation in FaceSpan. One answer is forking. This involves breaking the long-running portion of your project’s AppleScript code out into a separate script, and then running that script in its own Unix task. However, you want to do this in a way that keeps all the benefits of running AppleScript code within the FaceSpan environment (delegation, access to the UI, etc.).

The answer is to use AppleScript’s parent property to link the forked script back to the FaceSpan responder (e.g. a button) that forked it. Here’s a simple example of a long running AppleScript that uses this technique:

using terms from application "Forking" -- the name of the FaceSpan project
    property parent : application "Forking"'s pushMe
    --  From this point onward, all commands and property access is directed
    --  at the "pushMe" button.  Note that this includes scripting addition
    --  commands!

    on doSomethingTimeConsuming()
        delay 1
    end doSomethingTimeConsuming

    try
        --  Simulate a long-running AppleScript task
        repeat with i from 1 to 10
            reportProgress(i) -- tell the main app how far we have gotten
            doSomethingTimeConsuming()
        end repeat

        --  To send commands to the process running this script rather than
        --  the main FaceSpan application, you must do the following:
        tell current application to current date

        --  Report back to the main application that we are finished
        reportAllDone()

        --  Prove that this code can be written as if its running in FaceSpan:
        display alert "Done" ¬
            message "I've finished wasting some time" ¬
            buttons "OK" ¬
            over my window
    on error errMsg
        set responderName to get my name
        display alert "Runtime Error" ¬
            message "Error in " & responderName & "'s forked script: " & errMsg ¬
            buttons "OK" ¬
            over my window
    end try
end using terms from

This code is saved as a standard AppleScript compiled script using Script Editor/Script Debugger in a file within the FaceSpan project’s bundle.

Now you are ready to fork the script from FaceSpan:

on action theObject
    set enabled to false

    --  Fork off a process running an AppleScript that thinks its running in
    --  the context of this object (button)...
    set theCommand to "osascript " & ¬
        quoted form of (POSIX path of (path to resource "fork.scpt"))
    tell (make new task ¬
        with properties {command:theCommand, auto delete:true})
        start
    end tell
end action

--  Subroutines that the forked script can call to tell the main application
--  what's going on:
on reportProgress(theProgress)
    set title to "Progress: " & theProgress
end reportProgress

on reportAllDone()
    set title to "Push Me"
    set enabled to true
end reportAllDone

Notes:

  • since the forked script runs in a FaceSpan task, you could use the task’s ‘task did end’ event to discover when the forked script completes instead of the reportAllDone() subroutine.
  • if you need to abort the forked script for some reason (e.g. the user pressing a Cancel button), you can tell the FaceSpan task to stop.

This seems like such an important thing to be able to do that I’m working on integrating this capability directly into the FaceSpan runtime for a future build.

UPDATE: The FaceSpan 5.0d42 build provides this capability with the new fork command. The fork command accepts an AppleScript script object that is executed in a seperate process, thus allowing for long-running operations that don’t block the UI.