Articles‎ > ‎

Running items at login

posted Dec 19, 2008, 2:55 PM by Philip Rinehart   [ updated May 17, 2009, 9:44 PM by Greg Neagle ]
Written by Greg Neagle   
Wednesday, 24 November 2004
A common need in a managed OS X environment is to run certain scripts every time someone logs in, or to open certain items (applications, background utilities, folders, documents). Apple has provided a method for each user to specify items to be opened at login, but it is not entirely obvious how to specify certain items to be opened or executed for all users of a given computer.
Fortunately, there is a simple way to do this.

As it turns out, the loginwindow.plist file, located at ~/Library/Preferences/loginwindow.plist, works the way you'd wish all preference files worked. This file contains the list of items to open at login. If you copy this file to /Library/Preferences/ and make sure it is readable by everyone (chmod o+r /Library/Preferences/loginwindow.plist), the items you've specified to open at login will now be opened for every user of that machine. What's even better is that if a user specifies his or her own items to be opened at login (using the "My Account" (Jaguar) or "Accounts" (Panther) preference pane), the items defined in/Library/Preferences/loginwindow.plist AND the items defined for the specific user at ~/Library/Preferences/loginwindow.plist will be opened. So you can define items to be opened for every user of a machine without interfering with the ability for a user to define their own items.

This technique can be further refined. I have defined a single item to be opened by every user of the machines I manage. It's an application I call "LoginLauncher". This application looks in a folder I've defined (/Library/FA/LoginItems/) and opens everything in it. It knows how to run AppleScripts; execute shell scripts, Perl scripts, and other UNIX executables; and open anything else the same way the Finder would. The advantage of this method is that you do not have to keep editing /Library/Preferences/loginwindow.plist - instead, simply add or remove items from /Library/FA/LoginItems/ to control what is open or executed at start up.

The solution detailed here demonstrates several useful techniques that can be used by Mac OS X administrators in a variety of situations.

Implementation

This solution has three parts:
  1. Login items directory: all items to be auto-launched at login go here.
  2. LoginLauncher.app: this application does the actual work of running/opening the items in the Login items directory. A zip archive, containing this file and it's icon can be downloaded here
  3. /Library/Preferences/loginwindow.plist file: this tells the OS to auto-launch LoginLauncher.app.

Login items directory

This is simply a folder in which you put items to be run/opened at login for every user. Mine is at /Library/FA/LoginItems. You can put yours anywhere you want.

LoginLauncher.app

The LoginLauncher application is actually an executable shell script wrapped into an application bundle. This allows us to specify it as an item to be opened at login, control its visibility in the dock, give it a custom icon, and make it look like a "regular" OS X application.

There are several techniques in use here that you might be able to apply to other situations.

We'll start with the shell script. I've highlighted a few interesting parts in red:

#!/bin/sh

echo "Running login items..."

# find the bundle contents dir
macosdir=`/usr/bin/dirname $0`
contentsdir=`/usr/bin/dirname $macosdir`

# use the defaults command to read in the LoginItemsDir from 
# $contentsdir/Resources/Defaults.plist 
loginItemsDir=`/usr/bin/defaults read "$contentsdir/Resources/Defaults" LoginItemsDir` 

for file in "$loginItemsDir"/*
do
   if [ -x "$file" -a ! -d "$file" ]; then
      # execute it
      echo "Executing: $file"
      "$file" &
   else
      macName=`osascript -e "get POSIX file "$file" as text"`
      if [ $? -eq 0 ]; then
         kind=`/usr/bin/osascript -e "tell application "Finder" to get kind of item "$macName""`
         if [ "$kind" == "Alias" ]; then
            kind=`/usr/bin/osascript -e "tell application "Finder" to get kind of original item of item "$macName""`
         fi
         if [ "$kind" == "Script" -o "$kind" == "script text" -o "$kind" == "compiled script" ]; then 
            # run the Applescript
            echo "Running Applescript: $file"
            /usr/bin/osascript -e "tell application "Finder" to run script file "$macName""
         else
            # just pass it to the open command, which will launch apps and open docs
            echo "Opening: $file"
            /usr/bin/open "$file"
         fi
      fi
   fi
done

echo "Completed running login items."

Parsing the script

I'll point out some interesting parts of the shell script:

Output that is sent via "echo" goes to the console log. You can view it using Console.app. This is a handy way to debug - just put in echo statements and check the Console to see that it is doing what you expect.

The script uses the dirname command and the special variable $0 to find the Contents directory of its own bundle. This works because $0 contains the path to the executable, which is at 
   LoginLauncher.app/Contents/MacOS/LoginLauncher
The first call to dirname returns
   /path/to/LoginLauncher.app/Contents/MacOS 
and the second returns
   /path/to/LoginLauncher.app/Contents 

Once we have the path to the Contents directory, we can find our Defaults.plist file, and use the defaults command to extract the path to the login items directory:

   loginItemsDir=`/usr/bin/defaults read "$contentsdir/Resources/Defaults" LoginItemsDir`

Once we have the path to the login items directory, we loop through every item in it. If a item is executable, we run it. If it's not, we ask the Finder (via osascript, which is a command-line interface to AppleScript) what kind of file it is. If it's an AppleScript, we ask the Finder to run it. (We could run the AppleScript directly, but if it asks for user interaction or displays a dialog, it gets messier. It's more reliable and safer to ask the Finder to run the AppleScript, since that replicates the environment you probably used to build and test the AppleScript.)
If the file is not an AppleScript, we pass it to the open command, which opens files and applications much the same way as if you had double-clicked them.

Net result: each item in the login items directory is run, opened, or launched.

Wrapping the script into a bundle

Many Mac OS X applications are really bundles, which is a special kind of directory. The simplest bundle looks like this:
MyBundle.app/
MyBundle.app/Contents/
MyBundle.app/Contents/MacOS/
MyBundle.app/Contents/MacOS/MyBundle
That is, a directory with a name ending in .app, containing a directory named Contents, containing a directory named MacOS, containing the actual executable file. You can convert any executable shell script into a double-clickable application by wrapping it up into a bundle in this way.
Note that in this simplest case, the bundle and the executable must have the exact same name - the executable minus the ".app" extension.

Stupid bundle tricks

You can make your bundle more Mac-like and control more aspects of its behavior by adding additional files and directories to your bundle. For LoginLauncher.app, I did not want it to appear in the Dock while it was running. It should do its work silently in the background. To achieve this behavior, you must add a "Info.plist" file to the bundle's Contents directory with the following contents:
<plist>
<dict>
   <key>LSBackgroundOnly</key>
   <string>1</string>
</dict>
</plist>
The Info.plist file can actually contain a good deal more. Indeed, later we will specify a custom icon in this file. But right now, it contains a single key/value pair: LSBackgroundOnly = 1. This tells Launch Services this is a background-only application that displays no windows, has no menubar, and needs no icon in the Dock.

Default preferences

I also wanted the path to the login items directory to be stored in a preferences-style file so others could edit it without needing to edit the script itself. This also demonstrates how to store simple data outside of your executable. In this example, I'm storing only a single key/value pair, but you could store many.
Traditionally, internal data an application needs is stored in the bundle's Contents/Resources directory. I created a "Resources" directory inside the Contents directory, and then created file called "Defaults.plist" inside the Resources directory:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <key>LoginItemsDir</key>
   <string>/Library/FA/LoginItems</string>
</dict>
</plist>
This looks far more complicated than it is. In fact, I just copied and pasted the beginning and end of another .plist file I found in my Preferences directory, and then added these two lines:

   <key>LoginItemsDir</key>
   <string>/Library/FA/LoginItems</string>

This allows us to use the defaults command to read this file. All sorts of preferences can be stored and read this way. This technique does not allow the end-user to modify defaults, but does provide a way to allow an admin to modify preferences for a script without editing the script itself.

In the script itself, this line reads the value we stored in Defaults.plist:
   loginItemsDir=`/usr/bin/defaults read "$contentsdir/Resources/Defaults" LoginItemsDir`

It uses the defaults command to read "$contentsdir/Resources/Defaults" (note no ".plist" at the end of the name) and return a value for the key "LoginItemsDir". Earlier in the script, "$contentsdir" was assigned the path to the application bundle's Contents directory.

Adding an icon

Finally, for that professional appearance, all self-respecting Mac applications need their own icon, though it's certainly not necessary for an application like this. I created a custom icon using Icon Composer, part of Apple's Xcode developer tools, free with Mac OS X. I then copied that .icns file to the bundle's Contents/Resources directory. Finally, we have to tell the bundle to use the .icns file by adding two lines to the Contents/Info.plist file:
<plist>
<dict>
   <key>LSBackgroundOnly</key>
   <string>1</string>
   <key>CFBundleIconFile</key>
   <string>LoginLauncher</string>

</dict>
</plist>
Note that the Contents/Resources directory is assumed, and the icon file name in the plist does NOT include the .icns extension.

Pulling it all together with /Library/Preferences/loginwindow.plist

So - now we have an application that will open or run every item in a directory of our choosing. But we need to ensure that this application will itself be run at every login. We do this by editing the /Library/Preferences/loginwindow.plist file, creating it if needed.
Here's mine, with the key parts in red:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <key>AutoLaunchedApplicationDictionary</key>
   <array>
      <dict>
         <key>Hide</key>
         <true/>
         <key>Path</key>
         <string>/Library/FA/LoginLauncher.app</string>
      </dict>
   </array>
   <key>BuildVersionStampAsNumber</key>
   <integer>12977088</integer>
   <key>BuildVersionStampAsString</key>
   <string>6G30</string>
   <key>SystemVersionStampAsNumber</key>
   <integer>167904000</integer>
   <key>SystemVersionStampAsString</key>
   <string>10.2.3</string>
</dict>
</plist>
Specifically, it tells the system to open "LoginLauncher.app", located at "/Library/FA/". You would need to modify the path to match your deployment.
Make sure the file is readable by everyone, but not changeable by anyone other than the owner (chmod 755 /Library/Preferences/loginwindow.plist).

Now, at every login, LoginLauncher.app will run and open or run every file in the directory you specified in LoginLauncher.app/Contents/Resources/Defaults.plist.

To add or remove items that open or run at login for every user, you simply add or remove items from the login items directory.

So now what?

The mechanism is in place - what sorts of things can we do with it?
Here are some examples I use:

Quota checks:
We use NFS-mounted home directories. At login, a script is launched that monitors the disk space being used and warns the user as they approach their quota.

Configuration scripts:
I have scripts that add and remove items from a user's Dock based on what is actually installed on a system, a script that offers to help a user configure their account on first login, one that sets up ColorSync profiles for all users of a given machine, and others.

Third-party apps:
There are several third-party apps that open background applications at login.
Some installers set this up for the user that was logged in when the app was installed, but other users do not get these apps auto-launched.
Or the installer does correctly modify the /Library/Preferences/loginwindow.plist file to auto-launch the helper apps for all users.
If different apps make different modifications to this file, it becomes very difficult to manage with tools like radmind. Therefore, I identify these background "helper" apps and add symlinks to the login items directory that point to these helper apps.
This way, there is a single mechanism that manages these, and I can examine one place (the login items directory) to see what is auto-launched. Some examples:
  • Kensington MouseWorks: MouseWorks Background.app
  • Norton AntiVirus: DiskMountNotify.app
  • Norton AntiVirus: ScanNotification.app
  • Wacom: TabletDriver.app
There are many more.

Where to go for more information

Apple Developer bundle documentation:
http://developer.apple.com/documentation/MacOSX/Conceptual/SystemOverview/Bundles/chapter_4_section_1.html

Property lists (.plist files):
http://developer.apple.com/documentation/Cocoa/Conceptual/PropertyLists/Concepts/XMLPListsConcept.html

plist keys for the bundle's Info.plist:
http://developer.apple.com/documentation/MacOSX/Conceptual/BPRuntimeConfig/Concepts/PListKeys.html

dirname, defaults, osascript, open commands:
Type "man [command name]" at a Terminal prompt.

Conclusion

This article details a solution to a specific problem facing Mac OS X administrators: running/opening specific items at logon for all users of a machine.
As part of this solution, several useful techniques for Mac OS X system administrators have been presented:
  • Running/opening items at login for every user of a machine
  • Simple shell scripting
  • Calling AppleScript from shell scripts
  • Opening applications and documents from shell scripts
  • Wrapping a shell script into an application bundle
  • Specifying an application as a "background-only" app
  • Using the defaults command to read application preferences
  • Giving an application bundle a custom icon
  • Taking advantage of Apple's preferences hierarchy
I hope you find some of these useful for your own environment.

__________
Greg Neagle
Last Updated ( Thursday, 07 April 2005 )
Comments