Prevent a launchd job from rerunning until existing job finishes

I often need to schedule scripts to run at an interval, but I don’t know how long that script will take to complete and I don’t want the script to run again at its normal interval unless the script isn’t running.

I’ve done this with pid files and grepping through ps lists to exit the script if another instance is running, but I was wondering if there is something built into launchd to handle this.

It turns out that launchd does this by default. Just set up your job as normal using StartInterval and if your job hasn’t finished running by the next time it is scheduled to run, launchd will wait until the job finishes. If more than one schedule has passed, the missed jobs will be coalesced into one instance and run just once until the next scheduled run, much like it behaves if the machine goes to sleep.

Another win for launchd.

AD Plugin Join Replication Issues

I was getting random AD plugin connection issues after joining to Active Directory. dsconfigad showed no errors, but sometimes I would not get a connection and I would have to rejoin. The problem turned out to be related to replication.

The AD plugin initially has no knowledge of which AD site and domain controllers are considered local to your subnet, so it discovers any domain controllers and contacts one to lookup the site information. During this process, and in general, the AD Plugin keeps an LDAP connection open to the domain controller. The AD plugin likes to reuse these LDAP connections, presumably for performance reasons. When it is time to actually add the computer to the domain, the AD Plugin reuses this existing connection. The problem is that this domain controller is not necessarily one within your AD site.

At this point, if the Mac is restarted or DirectoryService is killed, any new connections will be made to a DC in the subnet’s AD site, but if your computer was added to a non-local DC, the local DCs may have no knowledge of your computer because the computer account has not yet replicated to them.

This problem can appear to be quite random because sometimes you’ll get lucky and get a local DC for the join, or you might catch the replication at the right time. You might also see bad password errors in the DirectoryService debug logs. I have filed a bug report on this, and I don’t have a good workaround for now other than — don’t reboot or restart DirectoryService after a join. Of course if you know your replication schedules, you could just wait until you are sure replication is completed.

This same issue can present itself with unjoins and rejoins.

You can see what domain controllers you are connecting to during the join using the following shell command assuming your are joining using dsconfigad:

while [ 1 ]; do if netstat -a | grep ldap| grep ESTAB; then ps auxww | grep dsconfigad | grep -v grep; date;fi; done

If you have joined, unjoined, and rejoined and think you may be seeing replication issues, compare the whenCreated attribute of the computer account on different domain controllers using ldapsearch.

ldapsearch -LLL -v -W -x -h domaincontrollerfromsite1.subdomain.forest.com -D username@subdomain.forest.com -b "OU=Computers,DC=subdomain,DC=forest,DC=com" CN=machine-join-name | grep whenCreated:

If an older out of sync computer account exists, its whenCreated date will be different from the domain controller the computer was just added to until the last join has replicated to all the servers.

Simple AFP Forensics using Access Logs

Mac OS X Server’s AFP server access logs aren’t the greatest (no full paths is a glaring omission), but if you have them enabled, they can be useful for finding who deleted a file or folder for example.

If the item’s name starts with “Important File”, this command gives us the ip address of the client that deleted the item :
file_server:~ root# grep -i "Delete Important File*" /Library/Logs/AppleFileService/AppleFileServiceAccess.log
IP 10.1.21.6 - - [08/Jul/2008:14:26:14 -0500] "Delete Important File 2009.xls" 0 0 0

Then we pass the ip address into this command to give us the login of the user:
file_server:~ root# grep 10.1.21.6 /Library/Logs/AppleFileService/AppleFileServiceAccess.log | grep Login
IP 10.1.21.6 - - [08/Jul/2008:09:05:43 -0500] "Login mpickens" 0 0 0

Finally we can use dscl to lookup the full name the user:
file_server:~ root# dscl localhost read /Search/Users/mpickens RealName
RealName: Pickens, Mary Ellen

Older logs are available too in zipped form. Use gunzip -c to read the contents.
file_server:~ root# gunzip -c '/Library/Logs/AppleFileService/AppleFileServiceAccess.log 12.11.07.gz' | grep Login | grep mpickens
IP 10.1.21.143 - - [14/Dec/2007:19:12:38 -0500] "Login mpickens" 0 0 0
IP 10.1.21.143 - - [14/Dec/2007:19:24:32 -0500] "Login mpickens" 0 0 0
IP 10.1.21.143 - - [17/Dec/2007:09:21:38 -0500] "Login mpickens" 0 0 0
IP 10.1.21.143 - - [17/Dec/2007:10:37:49 -0500] "Login mpickens" 0 0 0
...

Shell Injection with AppleScript’s do shell script

AppleScript’s do shell script capability is immensely useful, but if you are sending variable data to do shell script, always validate input and use quoted form of variableName. See the following example:

set dialogResult to (display dialog "Enter a directory name to pass to ls:" default answer ";say boo" buttons {"Cancel", "Quoted Form", "Raw"})

if button returned of dialogResult is "Quoted Form" then
try
set theCommand to "ls ~/" & quoted form of text returned of dialogResult
display dialog "Will execute:" & return & theCommand & return & "Proceed?"
do shell script theCommand
end try
else
try
set theCommand to "ls ~/" & text returned of dialogResult
display dialog "Will execute:" & return & theCommand & return & "Proceed?"
do shell script theCommand
end try
end if

Note you’ll have to fix the quotes to standard double quotes to get this to compile. I couldn’t get wordpress to cooperate.

Unix Environment Variable Scope/Security

I recently encountered a command line tool which exposed passwords in the process listing.

The command would also also accept a password as an environment variable. I was concerned with the security of storing a password in an environment variable.

This article at itworld.com does a nice job explaining environment variable scope.

Environment variables are only accessible in the shell in which they are set.

If you export the variable, it is accessible to any subshell of the shell in which it is exported. Simply logging in as another user on the system or even the same user does not allow access to the exported variable.

So, until someone corrects me, I believe that setting and exporting environment variables containing passwords in a script does protect the password from exposure. As soon as the command requiring the password has completed, the variable can be reset to an empty string to prevent any further access to the password.

Play mp3 from python on Mac

This site has a few nice little PyObjC samples.
One of them shows how to play a sound using AppKit’s NSSound. This is exactly what I was trying to do today.

And since PyObjC is built-in in 10.5, no addtional software is required.

Python is already batteries included, with PyObjC, you’ve got batteries galore.

ipython output:

In [1]: from AppKit import NSSound
In [2]: sound = NSSound.alloc()
In [3]: sound.initWithContentsOfFile_byReference_('/System/Library/CoreServices/Setup Assistant.app/Contents/Resources/TransitionSection.bundle/Contents/Resources/intro-sound.mp3', True)
Out[3]:
In [4]: sound.play()
Out[4]: True
In [5]: sound.stop()
Out[5]: True

No creation dates from stat

I was surprised to learn that creation dates are not included in stat output. I really thought stat would include something so basic. You get time of last access, time of last data modification, and time of last file status change, but creation date isn’t in there.

On the Mac, this info is stored in the resource fork. You can get it from the command line using GetFileInfo which is installed when you install Apple’s free Developer Tools.

/Developer/Tools/GetFileInfo -d path/to/file

In Applescript you can use info for:

creation date of (info for (choose file))

convert lines to comma separated items with tr

If you have output that is separated by new lines, but you really want it formatted into a single line with commas as separators or maybe a space as a separator, just pipe to tr.

Here is a simple example:

kserver:~ patternbuffer$ diskutil list | grep ^/dev
/dev/disk0
/dev/disk1

If we want them separated by spaces, we could do:

kserver:~ patternbuffer$ diskutil list | grep ^/dev | tr '\n' ' '
/dev/disk0 /dev/disk1

Note that there is a newline on the end of the output, so that trailing newline is also translated. So if you don’t want it there, you’ll have to chomp it off. I use sed, but use what you like.

Here is the same as above, but with commas, with sed to remove the trailing comma.

kserver:~ patternbuffer$ diskutil list | grep ^/dev | tr '\n' ',' | sed 's/.$//'
/dev/disk0,/dev/disk1

No ACL granularity for Extended Attributes

Recently I thought I had a great use for storing extended attributes. I needed to store the time a file had been updated by my file management system without effecting the actual file modifications times. Storing this information directly in the file metadata would ensure that it would not become out of sync with some external database tracking this information.

I wanted this data to be modifiable only by a specific user or group. Unfortunately, setting an ACL on extended attributes applies to all extended attributes. I didn’t want to disallow users from setting their own extended attributes on these files and folders, so I abandoned the idea.

I wonder if we’ll ever be able to set permissions on specific attributes.