ConnectDrive v0.97 20180715 Copyright © 2018 Alex Micek. All rights reserved. ConnectDrive is a macOS application that listens for the following events: + Application launch + Application termination + Disk image mount + Disk image unmount + Home network connect + Away network connect + Sleep + Wake In response to any of the above events, ConnectDrive can: + Launch applications + Terminate applications + Mount disk images + Unmount disk images + Run AppleScripts + Run shell scripts In addition, ConnectDrive is designed to ensure access to automounted folders by patching a bug in the macOS implementation of automount / automountd / autofs. REQUIREMENTS ============ OS X Yosemite (10.10) and later ConnectDrive is not designed to establish connections to network folders. There are nice ways to do this: + Connect your network folder(s) and add them to your login items + Use an app like Pixeleyes' Automounter: https://www.pixeleyes.co.nz/automounter/ + Go the Unix way with automountd (/etc/auto_master on macOS) You can do as little as patch the automount bug and as much as build up an entire chain of system events and actions, as detailed below. TABLE OF CONTENTS ================= I Just Want to Fix the Bug Configuration Examples Understanding Location Known Issues I JUST WANT TO FIX THE BUG ========================== If you're just here to fix your automount bug, first update to latest macOS as 10.13+ fixed the bug. If that is not possible, here's all you need to do: + Download ConnectDrive: https://tumbledry.org/projects/ConnectDrive.dmg + Install & Run + Open the following file in your favorite text editor: ~/Library/Application Support/ConnectDrive/Settings.plist + Edit the key in the Settings dictionary, replacing the text between the string tags with the full path to your broken automount directory: PatchAutomount /path/to/your/automount directory/ CONFIGURATION ============= In order to be helpful, ConnectDrive should always be running. You can do this one of two ways: + Add ConnectDrive to your login items + Or, copy the launchd job included in the install file into ~/Library/LaunchAgents/ and then execute the command: launchctl load ~/Library/LaunchAgents/com.micek.ConnectDrive.plist To ensure ConnectDrive is always running. ConnectDrive is controlled by editing its plist. The remainder of this document describes the concepts and organization of that plist. When first run, ConnectDrive will create the file: ~/Library/Application Support/ConnectDrive/Settings.plist Use Xcode, your favorite stand-alone plist editor or, of course, a text editor to make changes. It is best to edit the plist while ConnectDrive is not running, and then start it to pick up the changes. Search Console for "ConnectDrive" to follow along with what ConnectDrive is doing. After you've finished configuring it, setup ConnectDrive to always run using one of the two options in the above section. The root level of Settings.plist contains: Blocks Settings We'll cover them in that order. BLOCKS ------ Blocks are the basic building block of ConnectDrive. They are a way to say "when THIS happens, I'd like THAT to happen in response." Blocks are dictionaries (associative arrays) in Settings.plist, located here: ~/Library/Application Support/ConnectDrive/Settings.plist If you are unfamiliar with XML or .plist files, a plist editor will really help to visualize the hierarchy I'm describing. Let's first take a look at the format of a key for the block. The @ sign is immediately followed by an event verb. This is a key for a block when your computer sleeps: @sleep This is a key for a block when any device unmounts: @unmount Some verbs also can be paired with nouns. If a noun is to be specified, the verb is followed by a single space and then the noun. Here's a key for a block when volume "alexmusic" mounts: @mount /Volumes/alexmusic And this describes actions to be taken when Photos closes: @terminate com.apple.Photos Or when ANY application closes: @terminate Or when your computer joins a WiFi network titled GiraffeNet: @ssid GiraffeNet Or the first time your computer returns home: @firstHome Here is the complete table of supported block keys and nouns: @launch com.bundle.id @terminate com.bundle.id @mount /Volumes/nameOfVolume @unmount /Volumes/nameOfVolume @mountfail /Volumes/nameOfVolume @ssid WiFiNetworkName @home @firstHome @away @firstAway @sleep @wake • com.bundle.id is a bundle identifier of an application. If you wanted to find the bundle ID for Mail, you'd search for the key CFBundleIdentifier in the file: /Applications/Mail.app/Contents/Info.plist • /Volumes/nameOfVolume is a mountpoint of a device. • WiFiNetworkName is an SSID of a wireless network. All of the above actions are valid with or without their associated nouns. So, to run a script when any device mounts, create a block whose key is: @mount You can still target a particular device by using a different block key, e.g.: @mount /Volumes/usbStickyThing Here's how an (empty) block looks in XML in the actual Settings.plist file: @sleep ACTIONS ------- Within each block, you can add dictionaries called actions: @block mount unmount scripts launch terminate hide show These describe what will be done when a block event occurs. There is one special action: @sleep terminateForSleep It is only used in an @sleep block. When sleep occurs, terminateForSleep takes a list of apps, saves which apps are open, closes them, and then reopens them on @wake. This is useful if an application is getting its data from a mounted disk image on a network, and you want to guarantee* that the application closes and saves its data to the disk image before sleep. * Actions executed at sleep have a 15-30s time limit imposed by macOS, so as long as termination of the app and unmounting are reasonably quick, data will be preserved. Here's how an empty action in a block looks in XML in the actual Settings.plist file: @sleep terminate PARAMETERS ---------- Parameters describe actions. There is one required parameter per action. For application actions, the required parameter is an array of bundleIDs: launch|terminate|terminateForSleep|hide|show bundleIDs array For scripts, the required parameter is an array of paths: scripts paths array Paths should be absolute, but if script files are placed in the Application Support directory (~/Library/Application Support/ConnectDrive/), then their path can simply be their filename. If you write AppleScripts that do things like click buttons in others apps, then you'll want to add ConnectDrive as an "app allowed to control your computer" in: System Preferences > Privacy > Accessibility For device mounting, the required parameter is an array of dictionaries, each having two keys (image and volume): mount|unmount paths - image /Users/alex/Synology/music/alexmusic.sparsebundle volume /Volume/music [fsck] always|home|away|never If mounting is slow (>10s), you can defer or turn off automatic file system checking using the optional fsck key in the paths dictionary. It determines when or if a file system check is run on drive mount. Therefore, it has no meaning when present in an unmount action. "home" will only run a file system check when the machine is at home, "away" will only run it when away. See the "Understanding Location" section to learn more about how location is determined. If the fsck key is not set, the system default is used. There are two optional parameters valid in any action: mount|unmount|scripts|launch|terminate|terminateForSleep|hide|show [retries] int [notify] success|failure|always If retries is not specified, it defaults to 5, with 5 seconds in between tries. If notify is not specified, no notifications will be sent to macOS Notification Center, but will still appear on Console.app. So, parameters for a launch action could be: launch bundleIDs com.apple.TextEdit com.apple.iTunes retries 2 notify failure In the above example, launch is a dictionary with 3 keys (parameters): bundleIDs, retries, notify. bundleIDs is an array with two strings. Retries is an integer and notify is a string that can be success, failure, or always. If notify is omitted, no notification is given on success or failure. Parameters for a mount action could be: mount paths - image /mnt/Synology/music.sparsebundle volume /Volume/music - image /mnt/Synology/docs/work.sparsebundle volume /Volume/workDocs retries 10 notify always In this example, mount is a dictionary with 3 keys: paths, retries, notify. Retries and notify are the same as before, but paths is slightly different. Paths is an array, just like bundleIDs was above, but each entry in the array is a dictionary with two keys: image and volume. Here's how the above example would look in a complete, functional @block in the actual Settings.plist file: @launch com.apple.iTunes mount paths image /mnt/Synology/music.sparsebundle volume /Volumes/music image /mnt/Synology/docs/work.sparsebundle volume /Volumes/workDocs retries 10 notify always You can see why having a plist editor is a nice way to ensure you generate a valid file. STRUCTURE --------- The full structure of Settings.plist is outlined below. For brevity, actions and their parameters are listed just once, but actions are available in any block. [Brackets] indicate optional nouns or parameters. Blocks @launch|terminate [com.bundle.id] mount|unmount paths array of dictionaries [retries] int [notify] success|failure|always scripts paths array [retries] int [notify] success|failure|always launch|terminate bundleIDs array [retries] int [notify] success|failure|always hide|show bundleIDs array [retries] int [notify] success|failure|always @mount|unmount [/Volumes/nameOfVolume] @mountfail [/Volumes/nameOfVolume] @ssid [WiFiNetworkName] @home|@firstHome @away|@firstAway @wake @sleep terminateForSleep Settings SSIDHome array PatchAutomount array ExamineOnStart array runningApplications mounts location previousSSID_state string • One exception to actions being valid in any @block is: terminateForSleep. It only has meaning within the @sleep block. • Any key ending in _state is not meant to be edited. SETTINGS -------- Tell ConnectDrive your definition of your home network by entering your home network SSIDs in the Settings dictionary: SSIDHome HomeNet HomeNet2.4 • Do not edit any key ending in _state. • PatchAutomount is set by following the instructions at the beginning of this document. • ExamineOnStart tells ConnectDrive to check for mounts or launched apps or WiFi location when it is first started. The following strings may be added to its array: runningApplications Run through all "@launch com.bundle.id" blocks If any of those bundle IDs are already running, execute that block mounts Run through all "@mount /Volumes/nameOfVolume" blocks If any of those drives are already mounted, execute that block location Examine the current WiFi SSID and determine location Run any @home|@firstHome|@away|@firstAway blocks if they apply ExamineOnStart is useful if you are testing your configuration of ConnectDrive or if you find that ConnectDrive is missing an event on boot. EXAMPLES ======== EXAMPLE 1: CONNECTING AN APP So let's say you want to mount a drive when you open iTunes and unmount it when you close iTunes. Here's the block structure: @launch com.apple.iTunes mount paths - image /mnt/music/music.sparsebundle volume /Volumes/music notify success @terminate com.apple.iTunes unmount paths - image /mnt/music/music.sparsebundle volume /Volumes/music notify always retries 10 If you also wanted to be extra-safe, you could have iTunes terminate on sleep, and resume on wake with this block: @sleep terminateForSleep bundleIDs com.apple.iTunes com.apple.Photos Note that special action terminateForSleep. If you'd simply like to terminate an app on @sleep, then you'd just use the normal action: terminate. Here's the XML for the above example: @launch com.apple.iTunes mount paths image /mnt/music/music.sparsebundle volume /Volumes/music notify success @terminate com.apple.iTunes unmount paths image /mnt/music/music.sparsebundle volume /Volumes/music notify always retries 10 @sleep terminateForSleep bundleIDs com.apple.iTunes com.apple.Photos EXAMPLE 2: CONTROLLING A VPN Perhaps you want a VPN to automatically connect when you are away from home. First, you'd find the Settings dictionary in ~/Library/Application Support/ConnectDrive/Settings.plist Then, you'd add your home WiFi SSIDs to the SSIDHome array, as described in the "Settings" section of this README. Now, ConnectDrive knows how you define home. Your blocks could then be something like this: @firstAway launch bundleIDs net.tunnelblick.tunnelblick scripts paths vpnAddRoutes.sh @firstHome terminate bundleIDs net.tunnelblick.tunnelblick Where net.tunnelblick.tunnelblick is the bundleID of your VPN. These blocks will only be run once per location change. So, once you are away, your Tunnelblick VPN will remain open as long as your location remains away. It is important to know that ConnectDrive defines your home location only by wireless SSID. Wired connection changes are not available as events at this time. Here's the XML for the above example: @firstAway launch bundleIDs net.tunnelblick.tunnelblick scripts paths photos.applescript @firstHome terminate bundleIDs net.tunnelblick.tunnelblick EXAMPLE 3: CHAINING EVENTS A nice way to describe more complex sequences of events is through chaining. All this means is that the _action_ specified within one @block corresponds to another @block. So, let's say you want to hide iTunes while the drive for it is being mounted. You'd do this: @launch com.apple.iTunes mount paths - image /mnt/music/music.sparsebundle volume /Volume/music hide bundleIDs com.apple.iTunes @mount /Volume/music show bundleIDs com.apple.iTunes In that case, the "@mount /Volume/music" block is chained to the "@launch com.apple.iTunes" block, because the "mount" action in @launch, if successful, will result in the @mount block executing ITS actions. Here's the XML for the above example: @launch com.apple.iTunes mount paths image /mnt/music/music.sparsebundle volume /Volumes/music hide bundleIDs com.apple.iTunes @mount /Volumes/music show bundleIDs com.apple.iTunes EXAMPLE 4: SPECIFIC AND NON-SPECIFIC EVENTS Let's say you have a script that copies photos from any USB stick you plug into your computer. You can set up ConnectDrive to run that script whenever a mount event occurs: @mount scripts paths photoCopier.sh You can also add a more specific block for @mount; let's say you want to launch Final Cut Pro when you attach a drive for video editing: @mount /Volumes/videoScratchDisk launch bundleIDs com.apple.FinalCut Keep in mind, your photoCopier.sh script would also run when you attached that video drive. UNDERSTANDING LOCATION ====================== ConnectDrive decides if you are on a home network or an away network every time the WiFi SSID changes. However, you may only want to execute actions when location changes from home to away or from away to home, not each time SSID changes. For example: The SSID can change and the location changes with it, e.g.: away -> home bminet -> HomeNet == location changed BUT the SSID can change, yet location STAYS THE SAME, e.g.: home -> home HomeNet -> HomeNet2.4 == location DID NOT change! OR, e.g.: away -> away bminet -> VirginAtlantic == location DID NOT change! In that last example, your physical location changed, but your location, as understood by ConnectDrive, remained the same (location remained "away"). You can specify actions to be executed only once, when location changes from away to home or from home to away, using: @firstHome @firstAway However, if you'd like to run a block for home or away on every update of the WiFi SSID (including reconnections to the same SSID), you may use: @home @away Location blocks are not run if there is no SSID available. Maybe if the pipe dream of cellular Macs ever materializes, we can geofence and get a more accurate, robust definition of "home" and "away". But the future remains uncertain. KNOWN ISSUES ============ • Photos relies on a lot of helper processes (e.g. photolibraryd) that will hold locks on your files. So, if you have an external drive that contains your Photos library, unmounting it after Photos is closed may fail. You can do something like: @terminate com.apple.Photos unmount retries 30 notify always paths - image /Users/alex/Synology/photos.sparsebundle volume /Volume/photos Then, if you don't get a notification that unmount was successful soon after you close Photos, you can hit Activity Monitor, terminate the helper photolibraryd, which should free the locks and allow ConnectDrive to succeed on its next retry. In my testing, it also appears helpful to keep a "system" Photos library on your Mac, even if it contains no images. Photos seems better at managing its daemons when the external library on your *.sparsebundle is not the "system" library. • The mechanism by which ConnectDrive patches Apple's automount bug may prevent sleep. You can increase the likelihood of sleep if your time-to-sleep (as set in the macOS "Energy Saver" preference pane) is LESS than the automount timeout, as specified by: AUTOMOUNT_TIMEOUT in the configuration file: /etc/autofs.conf Because I am unable to determine the complete etiology of this bug, that's where we have to leave it for now. Thankfully, this bug appears to have been fixed in macOS 10.13. ConnectDrive 0.97 15 Jul 2018