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