Monday, November 24, 2014
  • Index
  •  » unScripted
  •  » AppleScript/Applescript-Studio: Background Code by B. Boertien

#1 2010-10-31 11:34:48 am

Adam Bell
Administrator
From: Nova Scotia, Canada
Registered: 2005-10-04
Posts: 4635

AppleScript/Applescript-Studio: Background Code by B. Boertien

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.

Applescript:

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.

Applescript:

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).

Applescript:

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.

Applescript:

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.

Applescript:

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.

Applescript:

--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.


iMac & MBP both running OS X 10.9.1

Offline

 

#2 2010-11-02 05:49:40 pm

DJ Bazzie Wazzie
Member
From: the Netherlands
Registered: 2004-10-20
Posts: 2005

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

Thanks Adam for the post


Kind regards

Offline

 

#3 2012-01-01 03:25:22 pm

jsherk
Member
Registered: 2011-12-24
Posts: 18
Website

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

Good post ... thanks!

Here is a forking problem question...

When I use the following osascript (in the do shell script) from Terminal command line, it returns immediately, so in theory the process should fork, but when I run this from AppleScript editor, it still only minimizes windows of one application at a time instead of forking to minimize windows of each application at the same time.

Applescript:

tell application "System Events"
   -- Get all the available applications that are visible
   repeat with appProc in (every application process whose visible is true)
       -- From the available applications above, click on the minimize
       -- button of every window that has a minimize button using
       -- do shell script to fork process
       do shell script "/usr/bin/osascript -e 'tell application \"System Events\" to click (first button of (every window of (application process \"" & (name of appProc) & "\")) whose role description is \"minimize button\")' &> /dev/null &"
   end repeat
end tell

For each application, I understand that it will minimize each window one at a time in that particular application, but in theory it should fork for each application and not wait for one application after another.

Thanks

Offline

 

#4 2012-01-02 04:34:48 am

DJ Bazzie Wazzie
Member
From: the Netherlands
Registered: 2004-10-20
Posts: 2005

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

Hi,

I've tried it myself and used the same code:

Applescript:


tell application "System Events"
   repeat with appProc in (every application process whose visible is true)
       set myScriptAsString to "tell application \"System Events\" to click (first button of (every window of (application process \"" & name of appProc & "\")) whose role description is \"minimalisatieknop\")"
       
       do shell script "osascript -e " & quoted form of myScriptAsString & " > /dev/null 2> /dev/null & "
   end repeat
end tell

My script is ready almost instantly and doesn't wait for system events to be ready. The problem is that system events has a queue which he simply executes one by one. The apple event listener inside System Events is single threaded so the system will wait when system events is ready to receive it's next event.


This kind of action(s) could be solved with ignoring application responses, this approach however doesn't solve the problem. The following script gives me the same behavior

Applescript:

tell application "System Events"
   set theProcesList to (name of every application process whose visible is true) as list
   repeat with thisProcess in theProcesList
       ignoring application responses
           click (first button of (every window of (application process thisProcess)) whose role description is "minimalisatieknop")
       end ignoring
   end repeat
end tell

EDIT: The role descriptions of buttons, as you can see in my code, are localized. So change them back into role description of your system.

Last edited by DJ Bazzie Wazzie (2012-01-02 04:35:57 am)


Kind regards

Offline

 

#5 2012-01-02 08:47:52 am

jsherk
Member
Registered: 2011-12-24
Posts: 18
Website

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

Looking more closely at my script, I can see that it is also ready almost instantly, so "do shell script" is returning instantly and forking properly as it should. And I see that the issue is definitely that System Events queues, so that is the reason for the delay.

Thanks DJ Bazzie Wazzie for those two examples though, they are very helpful in seeing a couple different ways to do the same thing! big_smile

I suppose the better solution then, for this particular issue (minimize all open windows) is to use the miniaturized property, but I see that System Events does not return/use this property ... I think I need to find all open windows of each application instead of application process.

Thanks

Offline

 

#6 2012-01-02 06:15:02 pm

jsherk
Member
Registered: 2011-12-24
Posts: 18
Website

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

A quick test of the following script, shows that using the miniaturize property does the same thing ... it gets queued up in System Events, so it is also not any faster.

Applescript:

--Pick a couple applications, open several windows in each, then run script and they will still be minimized one at a time
tell application "Thunderbird"
   set miniaturized of (every window whose ((miniaturizable is true) and (miniaturized is false) and (visible is true))) to true
end tell

tell application "Firefox"
   set miniaturized of (every window whose ((miniaturizable is true) and (miniaturized is false) and (visible is true))) to true
end tell

Offline

 

#7 2012-01-03 03:51:08 am

DJ Bazzie Wazzie
Member
From: the Netherlands
Registered: 2004-10-20
Posts: 2005

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

You bring me up to an idea..

All first windows of every application will be minimized simultaneous...

Applescript:


tell application "System Events" to set processList to (name of every process whose visible is true) as list
set blackList to {"Finder"} --The processes in this list will be skipped
repeat with processName in processList
   if contents of processName is not in blackList then
       tell application processName
           ignoring application responses
               set miniaturized of (every window whose ((miniaturizable is true) and (miniaturized is false) and (visible is true))) to true
           end ignoring
       end tell
   end if
end repeat


Kind regards

Offline

 

#8 2012-01-03 08:46:31 am

jsherk
Member
Registered: 2011-12-24
Posts: 18
Website

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

It does not work correctly for all applications because the application name and the application process name are sometimes different.

For example, application "Thunderbird" has application process "thunderbird-bin", so it gives an error when it tries to minimize window of thunderbird-bin since there is no application with that name.

But for the applications where the application name is the same as the application process name, then it does indeed minimize them all at the same time! smile

Thanks

Offline

 

#9 2012-01-03 09:32:42 am

DJ Bazzie Wazzie
Member
From: the Netherlands
Registered: 2004-10-20
Posts: 2005

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

If naming is a problem you can try on application ID.

Applescript:

tell application "System Events" to set processList to (bundle identifier of every process whose visible is true) as list
set blackList to {"com.apple.Finder"} --The processes in this list will be skipped
repeat with processName in processList
   if contents of processName is not in blackList then
       tell application id processName
           ignoring application responses
               set miniaturized of (every window whose ((miniaturizable is true) and (miniaturized is false) and (visible is true))) to true
           end ignoring
       end tell
   end if
end repeat


Kind regards

Offline

 

#10 2012-01-03 10:09:15 am

jsherk
Member
Registered: 2011-12-24
Posts: 18
Website

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

Your awesome!! big_smile

But now with application id, the problem is that it will not minmize Finder even with:
set blacklist to {}

I wonder if it is only Finder that has this issue or if there are other applications as well?

Always some problem!

Offline

 

#11 2012-01-03 10:28:32 am

DJ Bazzie Wazzie
Member
From: the Netherlands
Registered: 2004-10-20
Posts: 2005

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

jsherk wrote:

I wonder if it is only Finder that has this issue or if there are other applications as well?

Always some problem!

What the Finder does is IMO.... well lets say inconvenient. I'll hope the variable name for the list where the finder is in makes it clear what I think of it big_smile. You can add other applications with same behaviour as well (i didn't found one yet).

Applescript:

tell application "System Events" to set processList to (bundle identifier of every process whose visible is true) as list
set blackList to {} --The processes in this list will be skipped
set applicationIdsWithStupidCollapsePropertiesForWindowMiniaturizing to {"com.apple.Finder"}
repeat with processName in processList
   if contents of processName is not in blackList then
       tell application id processName
           ignoring application responses
               if processName is in applicationIdsWithStupidCollapsePropertiesForWindowMiniaturizing then
                   using terms from application "Finder"
                       set collapsed of (every window whose (collapsed is false) and (visible is true)) to true
                   end using terms from
               else
                   set miniaturized of (every window whose ((miniaturizable is true) and (miniaturized is false) and (visible is true))) to true
               end if
           end ignoring
       end tell
   end if
end repeat


Kind regards

Offline

 

#12 2012-01-03 10:42:35 am

jsherk
Member
Registered: 2011-12-24
Posts: 18
Website

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

Yes, I see what you think of Finder window lol

Again, you are awesome!

I did this (but I like yours better):

Applescript:

--set blackList to {"com.apple.Finder", "Finder"} --The processes in this list will be skipped.
set blackList to {}

--NOTE: This section will start minimizing windows of each application at the same time.
tell application "System Events" to set processList to (bundle identifier of every process whose visible is true) as list
repeat with processName in processList
   --display dialog processName --DEBUG only
   if contents of processName is not in blackList then
       tell application id processName
           ignoring application responses
               set miniaturized of (every window whose ((miniaturizable is true) and (miniaturized is false) and (visible is true))) to true
           end ignoring
       end tell
   end if
end repeat

--NOTE: This section is required in case there are applications (like Finder) that will not minimize using the Application ID method above
tell application "System Events"
   repeat with appProc in (every application process whose visible is true) -- Get all the available applications that are visible
       --display dialog (get name of appProc) --DEBUG only
       if (get name of appProc) is not in blackList then
           click (first button of every window of appProc whose role description is "minimize button") -- From the available applications above, click on the minimize button of every window that has a minimize button
       end if
   end repeat
end tell

Offline

 

#13 2012-01-03 11:32:42 am

jsherk
Member
Registered: 2011-12-24
Posts: 18
Website

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

Ok, more problems!! sad

First, your script does not work with Automator or Calculator .. I even tried adding them to the UsesStupidCollapseProperty, but they still stay open!

Second problem.. if I try to put both your script or my script into Automator (in the Run AppleScript action) it gives the following error:
SYNTAX ERROR: No result was returned from some part of this expression.
set miniaturized of (every window whose ((miniaturizable is true) and ((miniaturized) is false) and (visible is true))) to true

Thanks

Offline

 

#14 2012-03-21 12:17:18 am

jsherk
Member
Registered: 2011-12-24
Posts: 18
Website

Re: AppleScript/Applescript-Studio: Background Code by B. Boertien

Somebody finally figured out how to Show Desktop correctly:
http://www.everydaysoftware.net/showdesktop/index.html

Offline

 
  • Index
  •  » unScripted
  •  » AppleScript/Applescript-Studio: Background Code by B. Boertien

Board footer

Powered by FluxBB

[ Generated in 0.064 seconds, 11 queries executed ]

RSS (new topics) RSS (active topics)