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.