This article was written by Bastiaan Boertien and only posted by ACB, Bastiaan is the only author. Queries should be directed to DJ Bazzie Wazzie (see next post to topic).
The Problem:
When you download a file from with Safari you see a progress indicator with the option to cancel your download. In applescript this is not normally possible. For example, when you have a panel with a progress indicator and a cancel button to close this panel, the ‘on clicked theObject’ handler will not be called before your current code is completed. When batch processing, the whole batch must completed before any other user input will be handled. If you do so the script will finish the code first and when finished the next user input will be handled. This is because applescript has only one thread named main thread.
The Concept:
We need to detach the code from the main thread of the application. In the C family of languages, this is very easy. You can fork the application or make a thread. But we don’t have this functionality in Applescript so we need to do something else. Applescript has an commandline interpreter called osacompile to compile and osascript to run the code. Because an applescript application runs completely on events we don’t need to be in the application itself to manage the application. With this advantage we’ll start a script from the command line in the background of the system (fork code). When the main thread of the application has finished this command to start the osascript, which is almost instantly, the main thread of your application is ready for the next user input while the fork is still running its code in the background.
First Example of Backgrounding:
We’ll start with a very simple example. We’ll want to have two dialogs in the Finder at the same time. But now we’re using osascript to run this for us.
set myScriptAsString to "tell application \"Finder\" to display dialog \"hello world!\""
do shell script "osascript -e " & quoted form of myScriptAsString
do shell script "osascript -e " & quoted form of myScriptAsString --is still waiting for command above to be finished
What we did here was to run a shell command. We’ve used the command line utility osascript which allowed us to run a single line of code with the option -e. The only big difference between running from the command line osascript, script editor or as a standalone application is the focus. On the command line we can’t send events because osascript is an application below OS X. This means in this system level there are no events available. Thats why we tell the application finder to display a dialog. When you’re in script editor the events will be send to script editor itself. So without telling any application to display a dialog, script editor displays the dialog to you. You must be aware of this focus difference when programming with osascript.
We still haven’t achieved what we wanted. We wanted two dialogs at the same time. The second dialog only appears when the first is finished even if we detached the code from our own code because the shell command still waits till the command is finished. Do shell script is a process but normally takes a very short time. Unix allows us to run processes in the background to start servers or services like agents. What’s needed is to run the code just like a server or agent. This means we’ll start the command but won’t wait until it has finished running.
set myScriptAsString to "tell application \"Finder\" to display dialog \"hello world! -- Dismiss this dialog to see another.\""
do shell script "osascript -e " & quoted form of myScriptAsString & " > /dev/null 2> /dev/null & " --will be disabled by the finder because of the next line of code
do shell script "osascript -e " & quoted form of myScriptAsString & " > /dev/null 2> /dev/null & " --will be displayed above the previous dialog (move this to see other dialog)
Now we have two dialogs at the same time as we wanted. The first dialog is disabled because the Finder allows us to have only 1 dialog active at the same time but we did manage to continue our code without waiting for the previous command. We’ll also needed to add some other features. Because we don’t want to wait for the code to be finished we need to tell the command that it sends to its stdout (standard output) and stderr (standard error) to another place than the standard locations. Normally stdout is what you’ll get as a result from a do shell script and stderr is what wou’ll see in a error dialog. Because it’s running in the background we can’t retreive it so we must send its values to /dev/null, in other words we don’t want to store the data at all. The first redirection “>” is saying where the stdout goes and “2>” is saying where the stderr goes.
In many other languages you’ve got callback possibilities. Applescript and Applescript-Studio allows this as well in a way. Inside a script object we can set a property to a function and call this function later (passing functions).
on helloWorld()
display dialog "hello world!"
end helloWorld
on goodbyeWorld()
display dialog "goodbye world!"
end goodbyeWorld
on loadObject()
script prototype
property __callBack : null
on setCallBack(callBack)
set __callBack to callBack
end setCallBack
on runCallBack()
__callBack()
end runCallBack
end script
return prototype
end loadObject
set a to loadObject()
setCallBack(helloWorld) of a
set b to loadObject()
setCallBack(goodbyeWorld) of b
runCallBack() of a
runCallBack() of b
We set a and b to script objects. This script object has a property __callback (I prefer private variables with a prefix __) where we store our function. With the function setCallBack the property __callBack will be set. With runCallBack we run the function that’s set in the __callBack. This is not really a callback; it’s just passing functions to another object. But this feature is very usefull for keeping your code clean. A callback function is normally a function that is called after a process is finished in a separate thread. The best known callbacks, for example, are AJAX calls. You execute an HTTP request to the server and don’t wait until it’s completed. Instead, you send a function (named callback) to the requesting function and this callback function will be handled when the initial request is completed.
If we combine the backgrounding with callback in AppleScript we can do some advanced programming with Applescript. In the next example we want to display a dialog.
on cancelDialog()
display dialog "Process is canceled"
end cancelDialog
on backgroundScript()
script prototype
property __forkID : missing value
property __callBack : missing value
property __applescriptCode : missing value
on __construct()
set __forkID to null
set __callBack to null
set __applescriptCode to null
end __construct
on setApplescriptCode(applescriptCode)
set __applescriptCode to applescriptCode
end setApplescriptCode
on setCallBack(callBack)
set __callBack to callBack
end setCallBack
on startFork()
set theCommand to "osascript -e" & quoted form of __applescriptCode & " > /dev/null 2> /dev/null &
echo $!"
set __forkID to (do shell script theCommand)
end startFork
on stopFork()
do shell script ("kill " & __forkID & " > /dev/null 2> /dev/null" as string)
__callBack()
end stopFork
end script
__construct() of prototype
return prototype
end backgroundScript
set a to backgroundScript()
setApplescriptCode("repeat
tell application \"finder\" to display dialog \"Hello World!\"
end repeat") of a
setCallBack(cancelDialog) of a
startFork() of a
delay 5
stopFork() of a
What we have here is a object that runs code from a string as a separate process. After five seconds we stop the process and a callback function is handled. We also added a 'echo $!" in the startfork in the same shell command to retrieve the process ID that the separate process is given by the system. With stopfork we use this process ID to stop the process we started earlier.
We’ve only used a string to specify code so far but osascript can also handle .scpt files to be executed. I prefer this approach because it’s easier to program and handle in xocode projects. Another big advantage of running a script file instead of running a string is that you can pass arguments over the command line to your script.
do shell script "osascript -s s " & __pathToScript & " " & __arguments & " > /dev/null 2> /dev/null &
I’ve added -s s and a path to the file instead of -e and a string and at the end, I added arguments. The -s s parameter is (as the manual says) given so the arguments can be coerced back to their original class. I’ve haven’t tested all possible classes at this point but the standard applescript classes are workable. The path to script is just a posix path to the script to be run. Arguments is an interesting feature because it behaves like a normal command line utility. Every argument is separated with spaces. When passing text you’ll need to use quoted forms (text contains spaces as well). Remember that these arguments are visible in your process list when using ps. This means that if someone logs to this machine in via ssh he can see these. When the process must be secure, use a file with a key and send only the key to the application. Keep the cipher in your calling program so only it can decrypt any data sent by the application or fork and no other connected user will have the cipher.
--contents of your scpt file
on run argv
--do your thing here
end run
you’ll see a variable argv (can be any name). This variable is a list of strings that you’ve passed with with the osascript command above. Integers, numbers etc… are all strings so here you’ll need to coerce them back. I prefer to use the same method as in normal command line utilities. The order of the arguments is not important anymore and you can also include optional parameters. For example, when item 1 of argv is -u then item 2 of argv is to be the username, but If no -u is included then username is just guest.
AppleScript Studio
Now we’re going from applescript to applescript-studio to make a tiny applescript application with a panel that’s attached to the main window when the background is running. Because we have this process running in the background we’re able to add a cancel button to the panel in order to quit the background process.
First I start with a new project and have a window “main” and a panel “progress”. In window “main” I have only a button labeled “start”. In panel “progress” I have a progress indicator named “progress” and a button labeled “Cancel”.
property currentProcess : missing value
on quitProcess()
close panel window “progress”
end quitProcess
on backgroundScript()
script prototype
property __forkID : missing value
property __pathToScript : missing value
property __arguments : missing value
property __callBack : missing value
on __construct()
set __forkID to null
set __callBack to null
set __arguments to ""
end __construct
on attachScript(pathToScript)
set __pathToScript to pathToScript
end attachScript
on setArguments(arguments)
set AppleScript's text item delimiters to space
set __arguments to arguments as string
set AppleScript's text item delimiters to ""
end setArguments
on setCallBack(callBack)
set __callBack to callBack
end setCallBack
on startFork()
set theCommand to "osascript -s s " & __pathToScript & " " & __arguments & " > /dev/null 2> /dev/null &
echo $!"
set __forkID to (do shell script theCommand)
end startFork
on stopFork()
do shell script ("kill " & __forkID & " > /dev/null 2> /dev/null" as string)
__callBack()
end stopFork
end script
__construct() of prototype
return prototype
end backgroundScript
on clicked theObject
if the name of theObject is equal to “Start” then
set currentProcess to backgroundScript()
attachScript((POSIX path of (((path to me) & “Contents:Resources:Scripts:myprocess.scpt”) as string))) of currentProcess
setCallBack(quitProcess) of currentProcess
setArguments({0, 10, 1, 1}) of currentProcess
startFork() of currentProcess
else if the name of theObject is equal to “Cancel” then
stopFork() of currentProcess
end if
end clicked
The fork itself:
on run argv
set theApp to “backgrounder”
tell application theApp
tell progress indicator “progress” of window “progress”
set minimum value to (item 1 of argv as integer)
set maximum value to (item 2 of argv as integer)
set contents to 0
end tell
display window “progress” attached to window “main”
repeat with x from (item 1 of argv as integer) to (item 2 of argv as integer) by (item 3 of argv as integer)
set contents of progress indicator "progress" of window "progress" to x
delay (item 4 of argv as number)
end repeat
close panel window "progress"
end tell
end run
when building and running this you’ll see that, when you press the start button, a progress panel appears with a cancel button. Before the progress indicator has run to the end (when the panel will close by itself anyway) you can close the panel early by clicking the cancel button.
In this example we have some overkill to use a callback because the close panel command could also be used in the click handler. I uses a single process panel like this one for many different processes in my applications. They all have a button to close the panel but some of them need also different code to run after cancellation. For example, when you’re updating a table view by rows (not appending datasource) you’ll set in every new row update views to false, you need to set it back to true and delete the last row when canceling. This kind of code is what you put in a callback function.
Well I’ve made some simple and easy examples here but there are no security checks or what so ever in these examples. Also I haven’t spoke about multiple forks at the same time but I think that speaks for itself (same as building a window controller).
I hope you enjoyed this.