set done to false
repeat until done
display dialog "This applet can be used to make sure that a connected drive never goes to sleep (that is, that it never spins down). Normally this can be controlled using the Energy Saver preference pane in System Preferences, but some external drives do not respect these settings." & return & return & "This applet simply creates a launch agent that tells OS X to update a hidden file on the selected drive once every minute so that the drive stays active. Launch agents persist across reboots, etc., so you can run this once to set-it-and-forget-it." & return & return & "You can run Keep Drive Spinning again at any time to change your configuration." & return & return & "To get started, click on the approprite button below." buttons {"Quit", "Let a drive sleep...", "Keep a drive awake..."} default button "Keep a drive awake..." with icon note with title "Keep Drive Spinning"
if button returned of result is "Keep a drive awake..." then
try
set sleepless to {}
tell application "System Events" to set vols to name of every disk whose free space is not 0
if (count vols) = 0 then error "No eligible disks were found. Keep Drive Spinning only works for writable disks."
set selected to choose from list vols with prompt "Choose a drive to keep awake." & return & "Press Shift for multiple items." with title "Keep Drive Spinning" with multiple selections allowed
if selected is false then
error number -128
else
repeat with i from 1 to count selected
set sel to item i of selected
-- Get the path of sel
tell application "System Events" to set sel_path to (POSIX path of disk sel)
-- Save the current text item delimiters so we can restore them later
set ASTID to AppleScript's text item delimiters
-- Get a version of sel with no spaces
set AppleScript's text item delimiters to space
set sel_nospace to text items of sel
set AppleScript's text item delimiters to ""
set sel_nospace to sel_nospace as text
-- Escape XML reserved characters in sel_path and sel_nospace
set AppleScript's text item delimiters to "&"
set sel_path_xmlsafe to text items of sel_path
set sel_nospace_xmlsafe to text items of sel_nospace
set AppleScript's text item delimiters to "&"
set sel_path_xmlsafe to sel_path_xmlsafe as text
set sel_nospace_xmlsafe to sel_nospace_xmlsafe as text
set AppleScript's text item delimiters to ">"
set sel_path_xmlsafe to text items of sel_path_xmlsafe
set sel_nospace_xmlsafe to text items of sel_nospace_xmlsafe
set AppleScript's text item delimiters to ">"
set sel_path_xmlsafe to sel_path_xmlsafe as text
set sel_nospace_xmlsafe to sel_nospace_xmlsafe as text
set AppleScript's text item delimiters to "<"
set sel_path_xmlsafe to text items of sel_path_xmlsafe
set sel_nospace_xmlsafe to text items of sel_nospace_xmlsafe
set AppleScript's text item delimiters to "<"
set sel_path_xmlsafe to sel_path_xmlsafe as text
set sel_nospace_xmlsafe to sel_nospace_xmlsafe as text
-- Restore the text item delimiters to normal
set AppleScript's text item delimiters to ASTID
-- Ask for a time interval
repeat
set interval to text returned of (display dialog "How often (in seconds) should OS X prod " & sel & " to keep it awake?" default answer "60" with icon note with title "Keep Drive Spinning")
try
set interval to interval as number
if interval is less than 11 then
display dialog "OS X will not permit the job to run this frequently. The default value of 60 seconds is recommended." buttons {"OK"} default button "OK" with icon note with title "Keep Drive Spinning"
error
else if interval is less than 60 then
display dialog "An interval of less than 60 seconds is not recommended. It won't work any better to prod the drive that frequently, and it may negatively affect performance." & return & return & "Are you sure you want to use an interval of " & interval & " seconds?" buttons {"Yes", "No"} default button "No" with icon note with title "Keep Drive Spinning"
if button returned of result is "No" then error
end if
exit repeat
end try
end repeat
-- Set the name and path to our hidden file on the drive
set hiddenfile to ".keepThisDriveSpinning"
if last character of sel_path is not "/" then set hiddenfile to "/" & hiddenfile
-- Create the content of the launch agent
set plisttext to "<?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>Label</key>
<string>info.stovell.KeepDriveSpinning." & sel_nospace_xmlsafe & "</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>if [[ -w " & quoted form of sel_path_xmlsafe & hiddenfile & " ]]; then date '+%F %T %Z' > " & quoted form of sel_path_xmlsafe & hiddenfile & "; fi</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>" & interval & "</integer>
</dict>
</plist>"
-- Make sure plisttext is valid XML so we don't get a broken launch agent. Also make it pretty while we are at it.
try
set plisttext to (do shell script "echo " & quoted form of plisttext & " | xmllint --format -")
on error errMsg number errNum
error "Could not create a valid launch agent for " & sel number errNum
end try
-- If the hidden file already exists, make sure it isn't locked
try
tell application "Finder" to set locked of ((POSIX file (sel_path & hiddenfile)) as alias) to false
end try
-- Create the hidden file on the drive
try
do shell script "date '+%F %T %Z' > " & quoted form of sel_path & hiddenfile
on error errMsg number errNum
if errMsg contains "Permission denied" then
try
get (POSIX file (sel_path & hiddenfile)) as alias
set dialog_message to "You do not have permission to update the necessary file in " & sel & ". "
on error
set dialog_message to "You do not have permission to create the necessary file in " & sel & ". "
end try
display dialog dialog_message & "Retry with an administrator password?" & return & return & "You will only need to do this once." buttons {"Cancel", "Retry"} with icon caution with title "Keep Drive Spinning"
if button returned of result is "Retry" then
try
do shell script "date '+%F %T %Z' > " & quoted form of sel_path & hiddenfile with administrator privileges
on error errMsg number errNum
error "Could not create the necessary file in " & sel & ". The following error occurred:" & return & errMsg & return & return & "This is not an error in Keep Drive Spinning itself. Please correct the issue and then try again." number errNum
end try
end if
else if errMsg contains "Read-only file system" then
error sel & " is a read-only file system. Drives are kept awake by updating a hidden file on the drive once per minute. Files can’t be updated on read-only drives, so " & sel & " can’t be forced to stay awake." number errNum
else
error errMsg number errNum
end if
end try
-- Other users might also want to keep this drive awake, so let them
try
do shell script "chmod 666 " & quoted form of sel_path & hiddenfile
on error
do shell script "chmod 666 " & quoted form of sel_path & hiddenfile with administrator privileges
end try
-- If there is an old version of the launch agent already in memory, unload and delete it
do shell script "launchctl unload -w ~/Library/LaunchAgents/net.jonstovell.keep" & quoted form of sel_nospace & "Spinning.plist; launchctl unload -w ~/Library/LaunchAgents/info.stovell.KeepDriveSpinning." & quoted form of sel_nospace & ".plist; rm -f ~/Library/LaunchAgents/net.jonstovell.keep" & quoted form of sel_nospace & "Spinning.plist ~/Library/LaunchAgents/info.stovell.KeepDriveSpinning." & quoted form of sel_nospace & ".plist"
-- Create and load the launch agent
do shell script "mkdir -p ~/Library/LaunchAgents; echo " & quoted form of plisttext & " > ~/Library/LaunchAgents/info.stovell.KeepDriveSpinning." & quoted form of sel_nospace & ".plist; launchctl load -w ~/Library/LaunchAgents/info.stovell.KeepDriveSpinning." & quoted form of sel_nospace & ".plist"
-- Double check that it loaded properly
set agentloaded to false
repeat 4 times
if (do shell script "launchctl list") does not contain "info.stovell.KeepDriveSpinning." & sel_nospace then
delay 0.5
else
set agentloaded to true
exit repeat
end if
end repeat
if not agentloaded then
error "Could not load the launch agent. Make sure that the correct permissions are set on " & POSIX path of (((path to library folder from user domain) as text) & "LaunchAgents")
end if
set end of sleepless to sel
end repeat
set ASTID to AppleScript's text item delimiters
set AppleScript's text item delimiters to return
set sleepless to sleepless as text
set AppleScript's text item delimiters to ASTID
display dialog "The following drives will be kept awake:" & return & return & sleepless buttons {"More...", "Quit"} default button "Quit" with icon note with title "Keep Drive Spinning"
if button returned of result is "Quit" then set done to true
end if
on error errMsg number errNum
try
if errNum is not -128 then
beep
display dialog errMsg buttons {"Cancel"} default button "Cancel" with icon stop with title "Keep Drive Spinning"
end if
end try
end try
else if button returned of result is "Let a drive sleep..." then
try
set vols to {}
set existingAgents to every paragraph of (do shell script "find ~/Library/LaunchAgents -name 'net.jonstovell.keep*Spinning.plist' -or -name 'info.stovell.KeepDriveSpinning.*.plist'")
if (count existingAgents) = 0 then display dialog "There are no drives being kept awake." buttons {"Cancel"} default button "Cancel" with icon caution with title "Keep Drive Spinning"
repeat with thisAgent in existingAgents
tell application "System Events" to set vol to value of third property list item of property list item "ProgramArguments" of contents of property list file thisAgent
set ASTID to AppleScript's text item delimiters
set AppleScript's text item delimiters to {"[[ -e ", "[[ -w ", " ]]"}
set vol to second text item of vol
set AppleScript's text item delimiters to ASTID
set vol to (do shell script "dirname " & vol)
-- Do our best to ensure we show the user the drive's proper name. In some cases, it may be different than its POSIX path name.
try
-- If the drive is attached, get its proper name
tell application "System Events" to set vol to volume of ((vol as POSIX file) as alias)
on error
-- The drive isn't attached, so just use its POSIX path name
set vol to (do shell script "basename " & quoted form of vol)
end try
set end of vols to vol
end repeat
set selected to choose from list vols with prompt "Choose a drive to let sleep." & return & "Press Shift for multiple items." with title "Keep Drive Spinning" with multiple selections allowed
if selected is false then
error number -128
else
repeat with i from 1 to count selected
set agentToUnload to ""
repeat with j from 1 to count vols
if item i of selected is item j of vols then
set agentToUnload to item j of existingAgents
exit repeat
end if
end repeat
-- Unload and delete the launch agent
do shell script "launchctl unload -w " & quoted form of agentToUnload & "; rm -f " & quoted form of agentToUnload
end repeat
display dialog "The following drives are now allowed to sleep:" & return & return & selected buttons {"More...", "Quit"} default button "Quit" with icon note with title "Keep Drive Spinning"
if button returned of result is "Quit" then set done to true
end if
on error errMsg number errNum
try
if errNum is not -128 then
beep
display dialog errMsg buttons {"Cancel"} default button "Cancel" with icon stop with title "Keep Drive Spinning"
end if
end try
end try
else
set done to true
end if
end repeat
display dialog "This script will conduct a series of tests to figure out whether the Keep Drive Spinning launch agent is working correctly on a selected drive—and if not, why not. It will detect and report most common reasons for trouble." & return & return & "The diagnostic report will be saved in a file on your desktop and opened for you." with title "Keep Drive Spinning"
tell application "System Events" to set vols to name of every disk whose free space is not 0
set selected to choose from list vols with prompt "Select a drive." & return & "Press Shift for multiple items." with title "Keep Drive Spinning" with multiple selections allowed
if selected is false then
error number -128
else
set os_version to system version of (system info)
repeat with i from 1 to count selected
try
set report to {}
set sel to item i of selected
-- Gather basic info
tell application "System Events"
set sel_path to POSIX path of disk sel
set sel_format to format of disk sel
end tell
set report to report & {"OS version:", os_version, "", "Volume info:", "Name: " & sel, "Path: " & sel_path, "Format: " & sel_format}
set currentdate to do shell script "date '+%F %T %Z'"
set currentdate_seconds to do shell script "date -j -f '%F %T %Z' '+%s' " & quoted form of currentdate
set report to report & {"", "Current date:", currentdate}
set hiddenfile to do shell script "find " & quoted form of sel_path & " -maxdepth 1 -name '.keepThisDriveSpinning'"
if hiddenfile is not "" then
set lastupdate to do shell script "cat " & quoted form of hiddenfile
set report to report & {"", "Contents of hidden file:", lastupdate}
end if
try
set launchctllist to do shell script "launchctl list | grep -i 'stovell.keep.*spinning'"
on error
set launchctllist to "None found"
end try
set report to report & {"", "Status of all loaded Keep Drive Spinning launch agents:", launchctllist}
-- Do we have a loaded launch agent for this volume?
set agentstatus to ""
try
set launchagent to do shell script "grep -l " & quoted form of quoted form of sel_path & " ~/Library/LaunchAgents/info.stovell.KeepDriveSpinning*"
on error
set launchagent to ""
end try
if launchagent is not "" then
tell application "System Events"
set launchagent_contents to text of contents of XML file launchagent
set label to value of property list item "Label" of contents of property list file launchagent
set interval to value of property list item "StartInterval" of contents of property list file launchagent
end tell
set report to report & {"", "Launch agent path:", launchagent, "", "Launch agent contents:", launchagent_contents}
set agentstatus to do shell script "launchctl list | grep -i " & quoted form of label & " | cut -f 2 -"
end if
-- Okay, so we have a loaded launch agent. Is the hidden file writable? If not, is it locked?
if hiddenfile is not "" then
try
do shell script "test -w " & quoted form of hiddenfile
set w to true
set l to false
on error
set w to false
tell application "Finder" to set l to locked of ((POSIX file (hiddenfile)) as alias)
end try
if w then
set report to report & {"", "Writing to the hidden file:"}
try
set writeattempt to do shell script "echo " & quoted form of lastupdate & " > " & quoted form of hiddenfile & " && echo Succeeded"
on error errMsg
set writeattempt to errMsg
end try
set report to report & {writeattempt}
end if
end if
-- How long since the hidden file was last updated?
set elapsed to missing value
try
set lastupdate_seconds to do shell script "date -j -f '%F %T %Z' '+%s' " & quoted form of lastupdate
set elapsed to currentdate_seconds - lastupdate_seconds
end try
-- Output assessment
if launchagent is "" or agentstatus is "" then
set msg to {sel & " is not currently configured to be kept awake.", "Run Keep Drive Spinning to change this."}
else if hiddenfile is "" then
set hiddenfile to ".keepThisDriveSpinning"
if last character of sel_path is not "/" then set hiddenfile to "/" & hiddenfile
set hiddenfile to sel_path & hiddenfile
set msg to {hiddenfile & " does not exist.", "This file must exist to keep " & sel & " awake.", "Run Keep Drive Spinning again to fix this."}
else if l then
set msg to {hiddenfile & " is locked.", "Run Keep Drive Spinning again to fix this."}
tell application "Finder" to open information window of ((POSIX file hiddenfile) as alias)
else if not w then
set msg to {hiddenfile & " cannot be modified.", "This file must be modifiable to keep " & sel & " awake."}
tell application "Finder" to open information window of ((POSIX file hiddenfile) as alias)
else if elapsed is less than interval and agentstatus is "0" then
set msg to {"The Keep Drive Spinning launch agent for " & sel & " is working correctly."}
if interval is greater than 60 then
set msg to msg & {"The drive is currently updated every " & interval & " seconds.", "If the drive is sometimes falling asleep, try using 60 seconds."}
else
set msg to msg & {"If the drive falls asleep despite this, it may be suffering from a bad connection or failing hardware."}
end if
else
set report to {"The Keep Drive Spinning launch agent for " & sel & " is not working correctly.", "The reason for the failure could not be determined automatically.", "The diagnostic information below may yield a clue."} & report
end if
set report to {"Result:", "-------", msg, "", "", "Details:", "--------"} & report
giveReport(report)
on error errMsg
set report to report & {"", "An error occured:", errMsg}
giveReport(report)
end try
end repeat
end if
on giveReport(report)
set ASTID to AppleScript's text item delimiters
set AppleScript's text item delimiters to return
set report to report as text
set AppleScript's text item delimiters to ASTID
do shell script "now=$(date '+%F %H%M.%S'); echo " & quoted form of report & " > ~/Desktop/\"Keep Drive Spinning diagnostic $now.txt\"; open ~/Desktop/\"Keep Drive Spinning diagnostic $now.txt\""
end giveReport