Apple - How to start an application when a specific disk is mounted

I didn't downvote wch1zpink's answer, but it's very much not the way that I would solve the problem.

Having an app run an AppleScript every 5 seconds is a very inefficient way to handle this situation, especially since Mac OS already has a built-in feature to do this, namely, launchd.

Save this as ~/Library/LaunchAgents/com.tjluoma.itunes-on-mount.plist (you can change the "com.tjluoma.itunes-on-mount" part to whatever you want):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.tjluoma.itunes-on-mount</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>-c</string>
        <string>test -d /Volumes/Media &amp;&amp; open -j -g -a iTunes</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>WatchPaths</key>
    <array>
        <string>/Volumes</string>
    </array>
</dict>
</plist>

Then either reboot your Mac or do


launchctl load ~/Library/LaunchAgents/com.tjluoma.itunes-on-mount.plist

in Terminal. Then, anytime /Volumes/ changes, launchd will look to see if /Volumes/Media exists, and if it does, it will launch iTunes.


A stay-open application (Polling)

Although this question has already been marked as having a satisfactory solution, I'm compelled to provide an alternative. I completely agree with @TJ Luoma, who has provided, what I believe to be, by far the optimal solution, noting that a Stay Open application that polls for disk names every 5 seconds, and has to call out to an external application to do this, is not the way to approach this problem. It's inefficient, resource intensive, and entirely cumbersome to have an applet live permanently in the dock, or even just invisibly in the background, performing only one function in a terrible way, when macOS has a built-in means to monitor file system events, and a service manager to respond to these events and perform tasks in a scheduled and efficient manner.

launchd

TJ Luoma's method takes advantage of launchd to solve the problem in what is, as far as I am aware, the recommended way. It monitors for changes in the /Volumes folderpath, and acts accordingly. It's the method I would choose and know I didn't have to worry about what was going on behind the scenes.

Folder Actions

Some might not be aware that macOS's Folder Actions can also monitor the /Volumes folder, and if AppleScript feels more at home to someone than a .plist and a shell script, then this solution may be more appealing. As far as I'm aware, folder actions actually run as a process under launchd, so although they operate in overtly different ways to the user, the underlying mechanisms are possibly the same, or similar, and one is not likely to see any performance differences between the two.

Implementation

Setting up is very easy, even if you haven't done it before. It involves two steps:

  1. Save the following AppleScript, naming it anything you want, and saving it in the Folder Action Scripts folder located at ~/Library/Scripts/Folder Action Scripts. I saved mine as a plain .applescript text file using Script Editor, and named it New Volume Mounted.applescript:

    property diskName : "Media" -- whatever the disk is named
    property _objRef : a reference to reference
    
    
    script disks
        property path : missing value
        property kind : "public.volume"
        property list : missing value
    
        on _get(mountPoints)
            local mountPoints
    
            set my list to mountPoints
    
            tell application id "com.apple.systemevents" to ¬
                repeat with fURL in (a reference to my list)
                    if fURL's type identifier = my kind ¬
                        then set fURL's contents to ¬
                        the disk named fURL
                end repeat
    
            return every reference in my list
        end _get
    
        on _eval(key)
            if my list = missing value then return
    
            tell application id "com.apple.systemevents"
                tell {}
                    repeat with _disk in (a reference to my list)
                        set _objRef to _disk
                        set the end to contents of the key
                    end repeat
    
                    return it
                end tell
            end tell
        end _eval
    end script
    
    
    on adding folder items to mountedVolumes after receiving mountPoints
        local mountedVolumes, mountPoints
    
        disks's _get(mountPoints)
    
        set propertyKey to a reference to the name of my _objRef
        set disknames to disks's _eval(propertyKey)
    
        if the diskname is in the disknames then exec()
    end adding folder items to
    
    
    on removing folder items from mountedVolumes after losing diskNames
        local mountedVolumes, diskNames
    
        -- display alert "Disk removed: " & diskNames
    end removing folder items from
    
    
    on exec()
        -- display alert "Disk attached"
        run application id "com.apple.iTunes"
    end exec
    
  2. There is a conventional method to pair a folder action script with a watched folder, but by far the simplest is to copy and paste the script below into Script Editor and, if necessary, edit the third line the declares property script. This wants to be set to the same name (with file extension) as your script above. If you chose the same name as I did in the first step, you don't need to make any adjustments at all. Simply run the script and everything will be set up, taking immediate effect. There's no need to save this script, and no need to restart the computer.

    use sys : application id "com.apple.systemevents"
    
    property path : "/Volumes"
    property script : "New Volume Mounted.applescript"
    
    property name : a reference to the name of folder path
    property folder action : a reference to folder action named (my name)
    property folder : a reference to Folder Action scripts folder
    property file : a reference to the file named (my script) in my folder
    
    set folder actions enabled to true
    
    if not (my file exists) then return open my folder
    
    tell my folder action
        if (exists) then return its scripts
    
        make new folder action with properties my {name:name, path:path}
        make new script with properties my file's ¬
            {name:name, POSIX path:POSIX path}
    
        set [its enabled, its scripts's enabled] to [true, true]
    end tell
    

    This script has only been tested in High Sierra

Postscript

The script in step 1 above is one I wrote a while back as a generalised script that those who are comfortable with AppleScript could easily adapt and extend to perform whatever tasks one wishes, and already have available a list of all new mount points created in /Volumes in the form of a validated list of disk objects.

It is, however, actually more than is necessary for this task at hand. You are welcome to stick to it, and it ought to work just fine. But, if you would prefer a simpler script, then this one below performs exactly the same function as the one above, but with all the fat trimmed away:

property diskName : "Media" -- whatever the disk is named
property path : "/Volumes"
property diskVolumePath : [path, "/", diskName, "/"] as text


on adding folder items to mountedVolumes after receiving mountPaths
    local mountedVolumes, mountPoints

    repeat with mountPath in mountPaths
        set mountPath's contents to ¬
            mountPath's POSIX path
    end repeat

    if the mountPaths contains the diskVolumePath then exec()
end adding folder items to


on removing folder items from mountedVolumes after losing diskNames
    local mountedVolumes, diskNames

    -- display alert "Disk removed: " & diskNames
end removing folder items from


on exec()
    -- display alert "Disk attached"
    run application id "com.apple.iTunes"
end exec

This is a follow-up to TJ Luoma's answer, which I hope will fix the issues with the .plist definition that stops the job from being run in the correct manner.

From manlaunchd.plist:

WatchPaths <array of strings>

This optional key causes the job to be started if any one of the listed paths are modified.

IMPORTANT: Use of this key is highly discouraged, as filesystem event monitoring is highly race-prone, and it is entirely possible for modifications to be missed. When modifications are caught, there is no guarantee that the file will be in a consistent state when the job is launched.

StartOnMount <boolean>

This optional key causes the job to be started every time a filesystem is mounted.

Here is a revised .plist definition incorporating StartOnMount in favour of WatchPaths. It is currently labelled "local.startOnMount.iTunes", which, if unchanged, means the file should be saved at ~/Library/LaunchAgents/local.StartOnMount.iTunes.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>local.StartOnMount.iTunes</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/osascript</string>
        <string>-e</string>
        <string>if (list disks) contains "Media" then ¬
                run application id "com.apple.iTunes"</string>
    </array>
    <key>RunAtLoad</key>
    <false/>
    <key>StandardErrorPath</key>
    <string>/tmp/local.StartOnMount.iTunes.stderr</string>
    <key>StandardOutPath</key>
    <string>/tmp/local.StartOnMount.iTunes.stdout</string>
    <key>StartOnMount</key>
    <true/>
</dict>
</plist>

The ProgramArguments key can alternatively be more akin to TJ's original bash command:

<key>ProgramArguments</key>
<array>
    <string>/bin/bash</string>
    <string>-c</string>
    <string>[[ -d "/Volumes/Media" ]] &amp;&amp; open -jg -b "com.apple.iTunes"</string>
</array>

However, in testing, I found that, if iTunes is already running and is hidden, then callng the open command—even with the -j and/or -g option(s)—brings iTunes to the foreground. So I elected to do use osascript to perform the same actions, but in a more consistent manner.

This new .plist appears to resolve the issues, and only runs the job when a filesystem is mounted, which in turn only performs an action if it's the correct disk name. To replace:

cd ~/Library/LaunchAgents
launchctl unload com.tjluoma.itunes-on-mount.plist
launchctl load local.StartOnMount.iTunes.plist

However, if anything is not performing as expected, and it's not immediately obvious why, then look in the /tmp/ folder for file(s) named local.StartOnMount.iTunes.stderr and/or local.StartOnMount.iTunes.stdout:

cat local.StartOnMount.iTunes.stderr