23.1. Targeting Scriptable ApplicationsTo target a scriptable application is to aim Apple events at it, like arrows. The principal linguistic device for targeting an application in AppleScript is the tell block containing an application specifier. Such a tell block actually has two purposes: it determines the target, if no other target is specified within the block, and (at compile time) it also causes a dictionary to be loaded, which may be used in the resolution of terminology. If the target of the tell block is expressed as a variable rather than a literal application specifier, no resolution of terminology is performed and the application is not sought for targeting until runtime when the code is actually encountered. Instead of a tell block, the of operator (or its equivalents) can be used to form a target; this does not cause any resolution of terminology either. Terminology can be resolved independently of any tell block by means of a terms block. A reference to an object belonging to an application can also be used to target that application. The terminology within the reference has already been resolved (otherwise the reference could not have been formed in the first place); any further terminology accompanying the reference when you actually use it will have to be resolved independently. (See "Missing External Referents" in Chapter 3; "Target" in Chapter 11; Chapter 12; "Application" in Chapter 13; "Tell" and "Using Terms From" in Chapter 19; and "Resolution of Terminology" in Chapter 20.) 23.1.1. Local ApplicationsA local application is an application on the same computer and under the same user as the script. The specifier for a local application may consist of a full pathname string (colon-delimited) or simply the name of the application; the name should usually be sufficient. Specifying an application's name can be trickier than you might suppose. Back in the pre-Mac OS X days, typing the name of an application could be a maddening and tedious exercise. BBEdit's name wasn't "BBEdit"; it was "BBEdit 6.5". Excel's name was "Microsoft Excel". Frontier's name was "UserLand Frontier?", and you'd better not omit that "?" (and good luck remembering how to type it). Fortunately, if you got the name wrong, AppleScript would put up a File Open dialog giving you a chance to locate the application, and the correct name would then be substituted in your script. I remember I often used to supply a false name deliberately, such as tell application "xxx", just to get this dialog, because locating the application through the dialog was faster than trying to type (let alone remember) its "real" name. With the coming of Mac OS X, things got even worse. The File Open dialog was replaced by a Choose Application dialog asking "Where is... ?" and listing all your applications; this dialog is slow to open and impossibly sluggish to navigate. For the programmer, the horror of this dialog becomes an incentive to enter the application's name correctly to begin with. At the same time, applications often contrive to disguise their real names. For example, Excel's name is "Excel" in both the Application menu and the Dock, but its real name is "Microsoft Excel". Before Tiger, that's what you had to type in order to specify it. In Tiger, though, you can specify an application by its short name, such as "Excel"; on compilation, provided the application is running, it will be found and the long name will be substituted. Script editor applications now generally provide shortcuts for entering application names. In Script Editor, if an application is listed in the Library palette, you can select it and click the New Script button (in the palette's toolbar) to create a new script containing a tell block targeting that application. Script Debugger helps even more: there's a Recent Applications palette similar to Script Editor's Library window; every dictionary window contains a Paste Tell button; you can drag an application's icon into a script window or onto Script Debugger's icon to get a tell block targeting it; and there's a Paste Tell menu item, which lists all currently running scriptable applications and all applications whose dictionary you've recently viewed. Thus there is usually no need to type an application's name manually in order to target it. Specifying a full pathname should not usually be necessary, but can sometimes be useful; for example, it could be a way to distinguish two versions of the same application on your machine . Unfortunately, this trick doesn't work if one version is already running. For example, I have FileMaker Pro version 7 and FileMaker Pro version 5.5. This script is meant to launch version 5.5, but it misbehaves if version 7 is already running (I regard this as a bug in AppleScript):
tell application "gromit:Users:matt2:Info Process:FileMaker 5.5:FileMaker Pro.app"
launch
get version -- "7.0v3"
end tell 23.1.2. Remote ApplicationsA remote application is an application running on a different computer from the script, or on the same computer but under a different user already logged in (through Fast User Switching ). Communication is performed over IP (not AppleTalk , as in the past); this has the advantage that it works over the Internet. Thanks to Bonjour (formerly called Rendezvous), a machine on the local network can be specified by name. On the target computer, Remote Apple Events must first be turned on; this can be done in the Sharing preference pane. To target a remote application, you have to specify the machine on which it is running. To do so, you use an eppc URL . You can learn a lot more about the format of this URLindeed, you can easily create such a URL, suitable for targeting a remote applicationthrough the choose remote application command (Chapter 21). For example, if I use choose remote application to select the Finder on my iBook in the next room, I get this result: application "Finder" of machine "eppc://duck.local/?uid=501&pid=179" The application specifier is followed by a machine specifier that uses an eppc URL with my iBook's Bonjour name as the first path element. Actual connection to this machine will require a username and password; you can supply these as part of the eppc URL using the format username:password@ just before the machine name. The question mark and the material that follows it is optional, but can be useful. The uid is the user number, and you're going to need this parameter if you want to target a particular user's applications when multiple users are logged in. The pid is the process number of this application, so it's redundant given that we are already targeting the process by name, and in any case this number can change if an application quits or the computer is restarted, so you will probably have no use for it (though you can certainly use it instead of the name if you like). You can also incorporate the machine specifier into the application specifier by putting the application name as a second path element after the machine name (if you do this with a literal specifier, decompilation will rewrite the specifier in canonical form). So, here are some ways of targeting the Finder on my iBook: tell application "Finder" of machine "eppc://duck.local" -- puts up the username/password dialog get name of every window end tell tell application "Finder" of machine "eppc://mattneub:teehee@duck.local" -- avoids the username/password dialog get name of every window end tell set s to "eppc://mattneub:teehee@duck.local/Finder" -- incorporates machine specifier into application specifier tell application s get name of window 1 end tell set s to "eppc://mattneub:teehee@192.168.0.4/Finder" -- uses IP number instead of Bonjour name tell application s get name of window 1 end tell set s to "eppc://mattneub:teehee@duck.local/?uid=501&pid=179" -- uses pid instead of application name tell application s get name of window 1 end tell If you use a variable rather than a literal to specify the target, a terms block referring to a local application may be needed to resolve terminology at compile time (see "Using Terms From" in Chapter 19). It wasn't needed in the previous examples, because name and window are defined in AppleScript itself (see "The 'aeut' Resource" in Chapter 20). If possible, it's probably a good idea to use a terms block in any case, because it is much faster to get the dictionary from a local copy of an application than to get it across a network, and besides, you probably don't want to bother forming the connection to the remote machine every time you compile the script. So, for example:
set s to "eppc://mattneub:teehee@duck.local/Finder?uid=501"
using terms from application "Finder"
tell application s
get name of every disk
-- {"OmniumGatherum", "Network", "SecretSharer", "Puma"}
end tell
end using terms from Some scripting addition commands you call while targeting a remote application are run on the remote machine. For example: tell application "Finder" of machine "eppc://duck.local" say "Watson, come here, I want you." end tell Starting in Tiger, however, scripting addition commands that put up a user interface are disabled from operating remotely; this makes sense because there might be no one at the remote machine to dismiss a dialog: tell application "Finder" of machine "eppc://duck.local" display dialog "Watson, come here, I want you." -- error: Finder got an error: No user interaction allowed end tell The scripting addition commands store script and run script are disabled, probably as a security measure (but load script works). Passing object references back and forth between machines can work, because the reference is usually endowed with a machine specifier as well as an application specifier. But aliases can't usually be handed between one machine and another, because they are resolved on the wrong machine; instead, try to obtain a pathname string. You cannot target a remote machine merely; you must target some application on that machine. This raises the question of how to launch a remote application in order to target it. You cannot simply target the application by name as a way of launching it, because a literal application specifier will be resolved on the local machine. A remote application must thus already be running before you target it. Accordingly, you must always start with some application you know is running; likely possibilities are Finder and Dock. Then you can worry about whether the application you want to target is running. Here's how to find out:
set s to "eppc://mattneub:teehee@duck.local/Finder?uid=501"
using terms from application "Finder"
tell application s
get name of every process
-- {"loginwindow", "Dock", "SystemUIServer", "Finder", "LaunchBar",
"UniversalAccess", "System Events"}
end tell
end using terms from In theory you shouldn't be able to talk like that, because the Finder no longer handles the process classSystem Events does (see "System Events," later in this chapter). However, the process class is grandfathered into the Finder, and as a bonus, it is implemented by routing the Apple event to System Events, so not only do you get the answer, you also cause System Events to launch, which is good because you might want to target it. This still doesn't answer the more general question of how to launch an application remotely. If you know its full pathname, then you can just tell the Finder to open it: tell application "Finder" of machine "eppc://duck.local" open item "OmniumGatherum:Applications:Utilities:Terminal.app" end tell Otherwise, the official solution is to ask the Finder to open the application file using its ID , which can be a four-letter creator code or the application's bundle identifier. Thus, this works even if BBEdit is not running: set m to "eppc://mattneub:teehee@duck.local" tell application "Finder" of machine m using terms from application "Finder" open application file id "com.barebones.BBEdit" end using terms from end tell tell application "BBEdit" of machine m using terms from application "BBEdit" set contents of document 1 to "Hello, world!" end using terms from end tell (It is also possible to launch an application remotely using do shell script and osascript, but I'm told that this is regarded as a security hole and may be closed.) If multiple users are logged into the remote computer simultaneously, you can target a specific user with the uid parameter of the machine specifier. Indeed, you really should do so, because otherwise you can't be certain which user you're talking to (AppleScript seems to decide this in a somewhat random manner; I regard this as a bug). Again, the choose remote application dialog can be helpful here, as it distinguishes applications running under different users and tells you the user's uid number. Another approach is trial and error: just throw uid numbers at the machine and try to communicate with a running application, and see if you get an error or not:
set L to {}
repeat with i from 501 to 520
set m to "eppc://mattneub:teehee@duck.local/?uid=" & (i as string)
using terms from application "Finder"
try
tell application "Finder" of machine m
get (desktop as string)
set end of L to i
end tell
end try
end using terms from
end repeat
L -- {501, 502} Once we know the uid numbers of the logged-in users, we can target the applications run by a specific user. Observe that the username and password is still the username and password for the machine as a whole, not the user: on test(m) tell application "Finder" of machine m set n to (get name of window 1) end tell display dialog n end test test("eppc://mattneub:teehee@duck.local/?uid=501") -- mattneub test("eppc://mattneub:teehee@duck.local/?uid=502") -- guest The identical syntax allows you to target a different user logged into the local machine. In other words, if a machine has multiple users logged in, you can run a script under one user and target an application under another user. You can describe the present machine as localhost. Don't forget to turn on Remote Apple Events on your own machine! tell application "Finder" to get name of window 1 -- mattneub set m to "eppc://mattneub:teehee@localhost/?uid=502" tell application "Finder" of machine m get name of window 1 -- mrclean end tell 23.1.3. XML-RPC and SOAPXML-RPC and SOAP are web services . This means they are ways for one computer (the client) to query or command another computer (the server) over the Internet. The server defines certain commands to which it is prepared to respond. The client somehow knows what these are, and issues one of the commands with appropriate parameters; it also knows what sort of reply to expect, so it can parse the reply that it receives and extract desired pieces of data from it. If this sounds a lot like what Apple events and AppleScript are all about, it should! AppleScript is a good fit with XML-RPC and SOAP, and it makes sense that the AppleScript language should be capable of functioning as a client. XML-RPC and SOAP do not themselves use Apple events for communication. They use the HTTP protocol. This is clever, because HTTP is the protocol you use in your browser when you type in a URL or click a link to ask for a web page. It's a common, well-understood, and widely implemented protocol. An XML-RPC or SOAP message can be sent anywhere that a web page request can be sentbecause it is a web page request. And any CGI-enabled web server can function as an XML-RPC or SOAP server; it treats the message from the client just as it would an ordinary web page requestbecause it is a web page request. A message from the client to server is actually XML, structured according to certain conventions; this XML is wrapped up in an HTTP post argument (a post argument is the same sort of thing that gets sent when you press the Submit button in a web page form in your browser). The server looks at the requested URL and passes the message along to the appropriate CGI application, which pulls the XML out of the post argument, parses it, does whatever it's supposed to do, and hands back the reply. The reply is structured as a web page consisting of XML, so the web server just sends it back in the same way that it would send an ordinary web page back to your browser. The web server here need not in fact be elsewhere on the Internet. It could be on your local network, even on the same machine. It isn't difficult to set up your own web server to function as an XML-RPC or SOAP server ; there are standard modules for doing this with Perl or PHP, for example. This means you can use AppleScript to communicate with a Perl or PHP script. I know someone who does this as a way of storing certain email messages in a MySQL database . PHP has a good interface to MySQL, so it makes a good intermediary. His email client is scriptable; when he receives an email message that he wants to store, he runs a script that captures the information from the email message and sends a SOAP message containing it to the web server running on the same machine; the web server processes the SOAP message through PHP, which stores the email message in the MySQL database. (For a similar architecture, with AppleScript speaking to Perl instead of PHP, see http://developer.apple.com/internet/applescript/applescripttoperl.html.) AppleScript targets XML-RPC and SOAP services through support built into the Apple Event Manager. Your AppleScript command is translated into an Apple event, which in turn is translated into XML. The XML is shoved into the post argument of an HTTP request, and the request is sent across the Internet. (Your script waits synchronously for a reply.) When the reply comes back, the XML is extracted from the reply and interpreted as AppleScript data (usually a record)and that's the result of your command. You specify the server in a tell block, but instead of saying the name of an application, you provide a URL string. Within the tell block, you use one of two commands: either the call xmlrpc command or the call soap command. (These terms are resolved only when the target is an http URL; otherwise, they are illegal.) The target can be expressed in this form: tell application "http://www.xxx.com/someApplication" or you can say this, which decompiles to the previous syntax: tell application "someApplication" of machine "http://www.xxx.com" The syntax for call xmlrpc is as follows: call xmlrpc {method name: methodName, parameters: parameterList} The syntax for call soap is as follows: call soap {method name: methodName, method namespace uri: uri, parameters: parameterRecord, SOAPAction: action} You can omit an item of the parameter record (such as method namespace uri or parameters) if it isn't applicable. Now let's test these commands. There's a copy of UserLand Frontier on the Internet that is intended for users to test with XML-RPC and SOAP requests. By default, we access this server's XML-RPC functionality through a URL whose path is /rpc2. The server includes some simple test verbs, one of which is examples.getStateName. We can call this verb using AppleScript, as follows:
tell application "http://superhonker.userland.com/rpc2"
call xmlrpc
{method name:"examples.getStateName",
parameters:30} -- "New Jersey"
end tell Frontier is also a SOAP server. We can call the SOAP equivalent of the same verb using AppleScript, as follows:
tell application "http://superhonker.userland.com"
call soap
{method name:"getStateName",
SOAPAction:"/examples",
parameters:{statenum:30}} -- "New Jersey"
end tell If you happen to have a copy of Frontier or Radio UserLand, you can test all this on your own machine, without using the Internet. These programs run the very same server on port 8080. So with Frontier or Radio UserLand running, you can substitute "http://localhost:8080/rpc2" and "http://localhost:8080" as the application URLs for the tell block. If you tell AppleScript to look for a dictionary in an application that is a web URL, AppleScript won't actually look there, but will assume that this application is a SOAP or XML-RPC server. We can use this as a trick to treat the target as a variable when doing a SOAP call over the Internet. This example, based on a script distributed by Apple, shows how to write a general SOAP-calling handler. The handler generalSOAP contains no hard-coded information at all, except the application URL named in the terms blockbut this URL is a fake, merely to satisfy the compiler that the call soap command is legal. In the last line of this script, we test our general SOAP-calling handler by handing it some parameters; in this case, we are fetching the current Apple stock price over the Internet:
on generalSOAP(u, m, s, a, p)
using terms from application "http://www.apple.com/placebo"
tell application u
return call soap
{method name:m,
method namespace uri:s,
parameters:p,
SOAPAction:a}
end tell
end using terms from
end generalSOAP
generalSOAP("http://services.xmethods.net:80/soap",
"getQuote", "urn:xmethods-delayed-quotes",
"", {Symbol:"AAPL"}) -- 74.93 For another example of call soap, see "Combining Specialties" in Chapter 1. In general, call xmlrpc and call soap are not difficult to use, but you'll have to study the documentation for the service you're trying to call, and it may take a little trial and error to get the parameters just right.
|