Mac OS X Internals: A Systems Approach

9.15. Apple Events

Mac OS X includes a system-wide user-level IPC mechanism called Apple Events. An Apple Event is a message capable of encapsulating arbitrarily complex data and operations. The Apple Events mechanism provides the framework for transporting and dispatching these messages. Apple Events communication can be intraprocess or interprocess, including between processes on different networked computers. In general, one entity can request information or services from another entity by exchanging Apple Events.

AppleScript is the preferred scripting system on Mac OS X, providing direct control of applications and of many parts of the system. You can use the AppleScript scripting language to write programs (or AppleScripts) to automate operations and exchange data with or send commands to applications. Since AppleScript uses Apple Events to communicate with applications, the applications must be able to understand such messages and perform the requested operationsthat is, the applications must be scriptable. The Mac OS X Cocoa and Carbon frameworks provide support for creating scriptable applications.[16] Most GUI-based Mac OS X applications support at least some basic Apple Events, such as the ones used by the Finder for launching an application and providing a list of documents for it to open.

[16] Java applications, although typically not scriptable, can be made scriptable to some extent without including native code.

AppleScript has a syntax similar to natural language. Consider the example shown in Figure 952.

Figure 952. AppleScript program to speak the system version

-- osversion.scpt tell application "Finder" -- get "raw" version set the version_data to system attribute "sysv" -- get the 'r' in MN.m.r, where MN=major, m=minor, r=revision set the revision to ((version_data mod 16) as string) -- get the 'm' in MN.m.r set version_data to version_data div 16 set the minor to ((version_data mod 16) as string) -- get the 'N' in MN.m.r set version_data to version_data div 16 set the major to ((version_data mod 16) as string) -- get the 'M' in MN.m.r set version_data to version_data div 16 set major to ((version_data mod 16) as string) & major -- paste it all together set the os_version to major & "." & minor & "." & revision set the message to "This is Mac OSX " & os_version say message return os_version end tell

Mac OS X provides a standard, extensible mechanism called the Open Scripting Architecture (OSA), which can be used to implement and use Apple Eventsbased IPC in any language.[17] AppleScript is the only OSA language provided by Apple. The osalang command-line tool lists all installed OSA languages.

[17] There exist third-party implementations for other languages such as JavaScript.

$ osalang -l ascr appl cgxervdh AppleScript scpt appl cgxervdh Generic Scripting System

The first column in osalang's output is the component subtype, followed by the manufacturer, capability flags, and the language name. Each letter (unless it is the - character) in the capability flags string indicates whether a particular group of optional routines is supported. For example, c means that compilation of scripts is supported, whereas r means that recording scripts is supported. The Generic Scripting System entry is a pseudo-entry that transparently supports all installed OSA scripting systems.

You can use the osascript command-line tool to execute AppleScriptsor scripts written in any installed OSA language. Let us run our sample script from Figure 952 using osascript.

$ osascript osversion.scpt 10.4.6

This will result in several Apple Events being sent and received. The Finder will be instructed to retrieve the Mac OS X system version, which will be stored in the version_data variable. A human-friendly version string and a message announcing the version will be constructed, after which the Finder will run the say AppleScript command to convert the announcement string to speech.

If you wish to see internals of the Apple Events generated because of running your AppleScript, you can run it with one or more AppleScript debugging environment variables set. For example, setting each of the AEDebugVerbose, AEDebugReceives, and AEDebugSends environment variables to 1 will print an excruciatingly detailed trace of Apple Events being sent and received.

$ AEDebugVerbose=1 AEDebugSends=1 AEDebugReceives=1 osascript osversion.scpt AE2000 (2185): Sending an event: ------oo start of event oo------ { 1 } 'aevt': ascr/gdut (ppc ){ return id: 143196160 (0x8890000) transaction id: 0 (0x0) interaction level: 64 (0x40) reply required: 1 (0x1) remote: 0 (0x0) target: { 2 } 'psn ': 8 bytes { { 0x0, 0x2 } (osascript) } optional attributes: < empty record > event data: { 1 } 'aevt': - 0 items { } } ... { 1 } 'aevt': - 0 items { } } ------oo end of event oo------ 10.4.6

The AppleScript Studio application (included with Xcode) can be used to rapidly develop complex AppleScripts, including those with user-interface elements. You can also compile your textual AppleScripts into stand-alone application bundles.

Let us look at some more examples of using Apple Events, including how to generate and send Apple Events in C programs.

9.15.1. Tiling Application Windows Using Apple Events in AppleScript

This example is an AppleScript program called NTerminal.scpt, which communicates with the Mac OS X Terminal application (Terminal.app) and instructs it to open and tile a given number of windows. Running NTerminal.scpt will launch Terminal.app if it is not already running. Then, a given number of Terminal windowsas specified through the desiredWindowsTotal variablewill be opened. If there are already desiredWindowsTotal or more windows open, no further Terminal windows will be opened. Finally, NTerminal.scpt will tile desiredWindowsTotal windows in a grid, with desiredWindowsPerRow windows per row.

Note that the script is naïveit does not handle varying window sizes that would lead to complex arrangements. Moreover, its real-life utility is superfluous, since Terminal.app already supports saving window arrangements in .term files for subsequent restoration.

Figure 953 shows the NTerminal.scpt program. You can adjust the desiredWindowsTotal and desiredWindowsPerRow parameters based on the size of the display screen.

Figure 953. AppleScript program for opening and tiling Terminal application windows

-- NTerminal.scpt tell application "Terminal" launch -- Configurable parameters set desiredWindowsTotal to 4 set desiredWindowsPerRow to 2 -- Ensure we have N Terminal windows: open new ones if there aren't enough set i to (count windows) repeat if i >= desiredWindowsTotal exit repeat end if do script with command "echo Terminal " & i set i to i + 1 end repeat -- Adjust window positions set i to 1 set j to 0 set { x0, y0 } to { 0, 0 } set listOfWindows to windows repeat if i > desiredWindowsTotal then exit repeat end if tell item i of listOfWindows set { x1, y1, x2, y2 } to bounds set newBounds to { x0, y0, x0 + x2 - x1, y0 + y2 - y1 } set bounds to newBounds set j to j + 1 set { x1, y0, x0, y1 } to bounds if j = desiredWindowsPerRow then -- Move to the next row set x0 to 0 set y0 to y1 set j to 0 end if end tell set i to i + 1 end repeat end tell

9.15.2. Building and Sending an Apple Event in a C Program

In this example, we "manually" craft and send Apple Events to the Finder. We will send two types of events: one that will cause the Finder to open the given document using the preferred application for the document's type and another that will cause the Finder to reveal the given document in a Finder window.

We will use the AEBuild family of functions to construct in-memory Apple Event structures, which can be sent to other applications through the AESend() function. While constructing Apple Events using the AEBuild functions, we use an event description language that employs C-style formatting strings to describe events. The AEBuild functions parse the programmer-provided strings to yield event descriptors, simplifying an otherwise painful process wherein we would have to construct such event descriptors incrementally.

An event descriptor record is an arbitrarily ordered group of name-value pairs, where each name is a four-letter type code, and the corresponding value is a valid descriptor. The name and value within a name-value pair are separated by a colon, whereas multiple name-value pairs are separated by commas.

Let us compile and run the program shown in Figure 954.

Figure 954. Sending Apple Events to the Finder from a C program

// AEFinderEvents.c #include <Carbon/Carbon.h> OSStatus AEFinderEventBuildAndSend(const char *path, AEEventClass eventClass, AEEventID eventID) { OSStatus err = noErr; FSRef fsRef; AliasHandle fsAlias; AppleEvent eventToSend = { typeNull, nil }; AppleEvent eventReply = { typeNull, nil }; AEBuildError eventBuildError; const OSType finderSignature = 'MACS'; if ((err = FSPathMakeRef((unsigned char *)path, &fsRef, NULL)) != noErr) { fprintf(stderr, "Failed to get FSRef from path (%s)\n", path); return err; } if ((err = FSNewAliasMinimal(&fsRef, &fsAlias)) != noErr) { fprintf(stderr, "Failed to create alias for path (%s)\n", path); return err; } err = AEBuildAppleEvent( eventClass, // Event class for the resulting event eventID, // Event ID for the resulting event typeApplSignature, // Address type for next two parameters &finderSignature, // Finder signature (pointer to address) sizeof(OSType), // Size of Finder signature kAutoGenerateReturnID, // Return ID for the created event kAnyTransactionID, // Transaction ID for this event &eventToSend, // Pointer to location for storing result &eventBuildError, // Pointer to error structure "'----':alis(@@)", // AEBuild format string describing the // AppleEvent record to be created fsAlias ); if (err != noErr) { fprintf(stderr, "Failed to build Apple Event (error %d)\n", (int)err); return err; } err = AESend(&eventToSend, &eventReply, kAEWaitReply, // Send mode (wait for reply) kAENormalPriority, kNoTimeOut, nil, // No pointer to idle function nil); // No pointer to filter function if (err != noErr) fprintf(stderr, "Failed to send Apple Event (error %d)\n", (int)err); // Dispose of the send/reply descs AEDisposeDesc(&eventToSend); AEDisposeDesc(&eventReply); return err; } int main(int argc, char **argv) { switch (argc) { case 2: (void)AEFinderEventBuildAndSend(argv[1], kCoreEventClass, kAEOpenDocuments); break; case 3: (void)AEFinderEventBuildAndSend(argv[2], kAEMiscStandards, kAEMakeObjectsVisible); break; default: fprintf(stderr, "usage: %s [-r] <path>\n", argv[0]); exit(1); break; } exit(0); }

When run with only the pathname to a file or directory, the program will cause the Finder to open that file system objectsimilar to using the /usr/bin/open command. A file will be opened with the preferred application for that file type, whereas a directory will be opened by virtue of its contents being shown in a new Finder window. When the -r option is provided to the program, it will reveal the file system objectthat is, a file or directory will be shown in a Finder window with the corresponding icon selected. Note that a directory's contents will not be shown in this case.

$ gcc -Wall -o AEFinderEvents AEFinderEvents.c -framework Carbon $ ./AEFinderEvents -r /tmp/ ... $ echo hello > /tmp/file.txt $ ./AEFinderEvents /tmp/file.txt ...

The open command is built atop the AppKit framework's NSWorkspace class, which in turn uses the Launch Services framework.

9.15.3. Causing the System to Sleep by Sending an Apple Event

In this example, we will create and send a kAESleep Apple Event to the system process, causing the system to go to sleep. The loginwindow program is the system process, although we do not refer to the system process by name or process ID in our programwe use the special process serial number { 0, kSystemProcess } to specify the target of the Apple Event.

// sleeper.c #include <Carbon/Carbon.h> int main(void) { OSStatus osErr = noErr; AEAddressDesc target; ProcessSerialNumber systemProcessPSN; AppleEvent eventToSend, eventToReceive; // Initialize some data structures eventToSend.descriptorType = 0; eventToSend.dataHandle = NULL; eventToReceive.descriptorType = 0; eventToReceive.dataHandle = NULL; systemProcessPSN.highLongOfPSN = 0; systemProcessPSN.lowLongOfPSN = kSystemProcess; // Create a new descriptor record for target osErr = AECreateDesc(typeProcessSerialNumber,// descriptor type &systemProcessPSN, // data for new descriptor sizeof(systemProcessPSN), // length in bytes &target); // new descriptor returned if (osErr != noErr) { fprintf(stderr, "*** failed to create descriptor for target\n"); exit(osErr); } // Create a new Apple Event that we will send osErr = AECreateAppleEvent( kCoreEventClass, // class of Apple Event kAESleep, // event ID &target, // target for event kAutoGenerateReturnID, // use auto ID unique to current session kAnyTransactionID, // we are not doing an event sequence &eventToSend); // pointer for result if (osErr != noErr) { fprintf(stderr, "*** failed to create new Apple Event\n"); exit(osErr); } // Send the Apple Event osErr = AESend(&eventToSend, &eventToReceive, // reply kAENoReply, // send mode kAENormalPriority, // send priority kAEDefaultTimeout, // timeout in ticks NULL, // idle function pointer NULL); // filter function pointer if (osErr != noErr) { fprintf(stderr, "*** failed to send Apple Event\n"); exit(osErr); } // Deallocate memory used by the descriptor AEDisposeDesc(&eventToReceive); exit(0); } $ gcc -Wall -o sleeper sleeper.c -framework Carbon $ ./sleeper # make sure you mean to do this!

Категории