Using launchctl to pass parameters when monitoring files

Hi,


I have a script that is triggered when a file is changed via launchctl's WatchPaths feature. When monitoring multiple files using this feature, I'd like to know which file was changed, rather than creating a separate script for each one.


For example, in the my plist I have the following:


<?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.shenje.amr-upload</string>

<key>ProgramArguments</key>

<array>

<string>/PATH-TO-SCRIPT/amr-upload.sh</string>

</array>

<key>WatchPaths</key>

<array>

<string>/PATH-TO/file1</string>

<string>/PATH-TO/file2</string>

<string>/PATH-TO/file3</string>

</array>

</dict>

</plist>


So, when either file1, file2 or file 3 changes, it triggers the amr-upload.sh script.


What I'd like though, is to know which file changed. Is that possible?

MacBook Pro, Mac OS X (10.7.2)

Posted on Jul 28, 2013 11:03 AM

Reply
9 replies

Jul 28, 2013 11:18 AM in response to Boworr

Possible? maybe. But not simple. Launchd jobs don't have any built-in mechanism for passing contextual information. They run scripts when certain conditions are met, that's all.


If you want to make yourself crazy you could try a two-level approach: make a launchd job that monitors the folder for new files and then writes and loads individual launchd plists for each added file, or maybe a launchd job that calls a script which writes file info to a database and checks it for changes at each invocation. But at that point it might be easier to write a stay-open applescript that watches the folder and forget about using launchd.

Jul 28, 2013 3:13 PM in response to twtwtw

Thanks for the reply. I guess my issue is that these files exist in multiple directories and any one of them could be changed by an external application, and they need to be processed quickly once they have been updated.


If they were all in one directory then I could use what I think you're suggesting, but as they're scattered around it makes it more difficult for one launchd job to monitor them all.


Sadly I haven't got a clue about applescript.

Jul 29, 2013 8:38 AM in response to Boworr

Well, ok, here's the beginning of a solution. First, use the launchd plist file that you already have, with the following modification (I'm going to assume the name of this plist file is "Watch Paths Job.plist"):


<key>ProgramArguments</key>

<array>

<string>osascript</string>

<string>/PATH-TO-SCRIPT/Watch Paths Script.scpt</string>

</array>


Second, create an empty plist file with the name "Storage File.plist". Either use the plist editor or make a copy of Watch Paths Job.plist and delete everything between <dict> and </dict>


Now open the applescript editor and copy the following script into it, saving it as an applescript with the name "Watch Paths Script.scpt".


tell application "System Events"


-- jobFile should point to the launchd plist file that calls this script

set jobFile to property list file "/PATH-TO-FILE/Watch Paths Job.plist"


-- storageFile should point to an (initially empty) plist file

set storageFile to property list file "/PATH-TO-FILE/Storage File.plist"


set watchFileList to value of property list item "WatchPaths" of jobFile

repeat with thisFile in watchFileList

set currentModDate to false

try


-- compare current modification date to stored modification date

set currentModDate to modification date of filethisFile

tell storageFile

set oldModDate to value of property list itemthisFile

end tell


if currentModDate > oldModDate then


-- if mod date changed, run script and update stored date

my actOnChangedFile(thisFile)

set value of property list itemthisFile to currentModDate

end if


on error errStr number errNum

if errNum = -1700 then


-- error reading property list item, most likely trying to read the wrong data type

set value of property list itemthisFile to currentModDate

else if errNum = -1728 then


-- either the property list item or the file to be watched does not exist

if currentModDatefalse then


-- file exists, so make new property list item

tell storageFile

makenewproperty list itemat end of property list itemswith properties {name:thisFile, value:currentModDate, kind:date}

end tell

end if

else


-- unknown error, report error so that it can be dealt with

display dialog (errNum & return & errStr) as string

end if

end try

end repeat

end tell


on actOnChangedFile(aFile)


-- I'm assuming here you pass the file path to the shell script as an argument

do shell script "/PATH-TO-SCRIPT/amr-upload.sh " & quoted form of aFile

end actOnChangedFile


It should work as follows:


  1. launchd sees a change in one of the files in the WatchPaths list and launches the applescript
  2. the applescript reads the list of watched files out of the launchd plist file, and checks the file modification date on each against the stored data in the storage file
  3. where necessary, the script launches the amr-upload shell script and updates the modification date stored in the storage file.


The try block does error handling, and currently manages cases where the watched file doesn't exist, or where the plist entry in the storage file is missing or of the wrong data type. If you run across other errors you'll need to note the error number and add a condition to handle the problem.

Jul 29, 2013 10:36 AM in response to Boworr

Franks' idea has real value - put the logic into your script.


The other approach is to have three launchd .plists - one per file.

That way, each file is monitored by its own launchd WatchPath and you can customize the parameters to the script accordingly:


<?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.shenje.amr-upload1</string>

<key>ProgramArguments</key>

<array>

<string>/PATH-TO-SCRIPT/amr-upload.sh</string>

<string>file1</string>

</array>

<key>WatchPaths</key>

<array>

<string>/PATH-TO/file1</string>

</array>

</dict>

</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>com.shenje.amr-upload2</string>

<key>ProgramArguments</key>

<array>

<string>/PATH-TO-SCRIPT/amr-upload.sh</string>

<string>file2</string>

</array>

<key>WatchPaths</key>

<array>

<string>/PATH-TO/file2</string>

</array>

</dict>

</plist>


The overhead of having three (or more) launchd tasks, each watching one file, vs. one launchd task watching multiple files is trivial.

Oct 7, 2013 10:32 PM in response to twtwtw

Hi,


I've just started learning Apple Scripting and ran across this great piece of code. Currently, I'm just trying to get a text file to duplicate into my Box Documents when modified. I've got just about all of it working except that I'm running into a problem with the property list.


I keep getting this error:


/Library/Scripts/PATHTO/script.scpt: execution error: System Events got an error: Can’t set property list item "/Users/user/PATHTO/Test_Bed/Test.txt" to date "Tuesday, October 8, 2013 12:13:31 AM". (-10006)


Also, I'm not 100% sure that I'm handling the duplicating portion correctly since I'm not seeing it copied over.


Below is the code as is, it's pretty similar to twtwtw's. Any help would be greatly appreciated:


tell application "System Events"


-- jobFile should point to the launchd plist file that calls this script

set jobFile to property list file "/Library/LaunchAgents/com.apple.backup_FileMasterWP.plist"


-- storageFile should point to an (initially empty) plist file

set storageFile to property list file "/Users/user/Documents/com.apple.backup_FileWPstorage.plist"


set watchFileList to value of property list item "WatchPaths" of jobFile

repeat with thisFile in watchFileList

set currentModDate to false

try


-- compare current modification date to stored modification date

set currentModDate to modification date of filethisFile

tell storageFile

set oldModDate to value of property list itemthisFile

end tell


if currentModDate > oldModDate then


-- if mod date changed, run script and update stored date

my actOnChangedFile(thisFile)

set value of property list itemthisFile to currentModDate as string

end if


on error errStr number errNum

if errNum = -1700 then


-- error reading property list item, most likely trying to read the wrong data type

set value of property list itemthisFile to currentModDate

else if errNum = -1728 then


-- either the property list item or the file to be watched does not exist

if currentModDatefalse then


-- file exists, so make new property list item

tell storageFile


makenewproperty list itemat end of property list itemswith properties {name:thisFile, value:currentModDate, kind:date}

end tell

end if

else


-- unknown error, report error so that it can be dealt with

display dialog (errNum & return & errStr) as string

end if

end try

end repeat

end tell


on actOnChangedFile(aFile)


--Destination directory

set destination to "Macintosh HD:Users:user:Box Documents:Backup"

set source to POSIX file of aFile as alias



-- Pass the file path to the shell script as an argument

tell application "Finder"


duplicatefilesourcetofolderdestination with replacing

end tell

end actOnChangedFile

Oct 8, 2013 8:54 AM in response to jonnyvicious

In the future, please start a new thread for a new problem. saves on confusion for everyone...


on line 20 you changed:


set value of property list itemthisFile to currentModDate


to:


set value of property list itemthisFile to currentModDate as string

I am guessing this is the source of your problem, since the script relies on this value to be a date object, not a string. Each item in this plist file should have a POSIX file path (a string) as the key and the associated file's last modification date (a date object) as its value.


Yes, I know: in plist files everything looks like a string. But each key-value has its type specified in the brackets:


<key>/Users/.../.../...txt</key>
<date>2013-05-30T15:19:06Z</date>


and the system tries to translate the values internally to the correct object type, and throws an error if it can't.

Oct 8, 2013 10:37 AM in response to twtwtw

Thanks for the response. Apologies for the confusion, I went ahead and made a new disucssion thread (https://discussions.apple.com/thread/5426481) - I hope this was the right thing to do.


Thank you for pointing out my mistake, I'm also pretty new to asking for help on forums as well.


As detailed in the new thread, I went ahead and changed it back and am still getting the same error. I'm not entirely sure what it could be, I've even tried to change permissions with not much luck.

This thread has been closed by the system or the community team. You may vote for any posts you find helpful, or search the Community for additional answers.

Using launchctl to pass parameters when monitoring files

Welcome to Apple Support Community
A forum where Apple customers help each other with their products. Get started with your Apple Account.