Ruby Time to Integer does give you seconds since Epoch

“Time is stored internally as the number of seconds with fraction since the Epoch, January 1, 1970 00:00 UTC.”

 >>require 'time'
 >>Time.parse("January 1, 1970 00:00 UTC").to_i
 => 0

As expected, but nice to verify.

For the reverse, be sure to use Time.at rather than Time.new

>> Time.at(0).to_i
=> 0
>> Time.new(0).to_i
=> -62167201200

Tips for writing command line tools in ruby

  1. Option parsing: Read this article by Allen Wei on RubyLearning Blog for a great overview.  I recommend sticking with the built-in OptionParser if you want to reduce dependencies.
  2. If you want your code to be loadable so you can access functions and classes in the irb console for testing, use the following pattern:
    def main
     #option parsing and execution code here
    end
    if __FILE__ == $0
     main()
    end

    This way the main function will only be automatically called if the script is being executed on the command line.
    And in irb, you can call your functions and classes as you see fit for testing without triggering your whole script to run.

    Note that this is similar to the python __main__ test if you are coming from a python background.

  3. Naming without ruby’s.rb extension: You can name your executable without the rb extension if you wish, just be sure to include your shebang (#!)

    To use the user’s default ruby, use:
    #!/usr/bin/env ruby
    But in some cases you may want the specify the path to ruby so you can use macruby or rubycocoa if you are need those frameworks to be available
    #!/usr/bin/ruby

    When testing in irb,
    require 'script-name'
    won’t work without the rb extension, but
    load 'script-name'
    does work.
  4. Use exit codes. When your script fails or needs to communicate status at exit, use standard exit status codes.
    Exit zero for default success status
    exit 0
    Exit any other number for a failure or warning status. You choose the exit codes for your tool, but be sure to document them if they require more explanation than simple success or failure.
    exit 27
  5. Output to stderr using:
    $stderr.puts "error: problem ...."

Parsing Mac OS X System Profiler

It is pretty cool how Apple System Profiler has a command line equivalent (system_profiler). And it is pretty cool how system_profiler has a -xml option to allow for easier parsing. You might use this info for extracting asset information into a database or for puppet facter facts.

However if you’ve ever looked at that xml, you know that it is a tree full of unpredictable semi-structured data that was designed specifically for the GUI app. So even though you can parse it with your favorite plist parser, there is still a lot more work to do to get to the data you care about.

The tree structure is nice for a browsing through on a single machine, but not so good for reporting across many machines.

Apple stores most of the same data as key value pairs in its database for ARD reporting, but they do a lot of massaging of the data to get it that way.

It is possible to get at this data in an ARD database if you have an ARD collection server, but an ARD collection server isn’t for everyone and doesn’t serve every use case.

You can still get at the nicely formatted ARD information. ARD client includes a tool that outputs most, if not all of the asset information you care about in a much nicer structured format for reporting.

The tool is called sysinfocachegen and you use it like this:

sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Support/sysinfocachegen -p /tmp/com.yourorganization.systeminfo.plist

Just use your favorite language’s plist parser to read the plist.

Reliable record references when AppleScripting FileMaker

Using current record when AppleScripting FileMaker has a couple of problems. The current record can change out from under you, so by the next step in your script, you may be dealing with the wrong record. Simply storing the record result in a variable doesn’t work either because getting the result converts it to a native AppleScript list of the data from the record. Using a reference to current record suffers the same problem.

One solution is to use record IDs. I believe that record IDs are guaranteed to be static/unchanging, at least while the app is running.

To get the record ID for the current record:
set recID to ID of current record

To use the record ID later in your script:
get cell "my_field" of record ID recID

To get all record IDs for a given query:
set recordIDs to ID of every record whose cell "my_field" is "my_value"

Then access each found record by ID:
repeat with recID in recordIDs
   get cell "my_other_field" of record ID recID
end repeat

Note: The following method will also work for certain use cases since it appears to use the record ID internally as well:
set x to current record as reference

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.