Troubleshooting LaunchD Scheduled Script

McAwesome
Valued Contributor

I've be tasked with creating a deferral prompt for a Managed OS workflow. I've gotten everything figured out....except for LaunchD to actually trigger the deferred policy at a later time. Here's the problem section of code.

## Writes a simple script that exists just to call the deferred policy
echo "/bin/launchctl unload /Library/LaunchDaemons/managedOS.plist" >> /Library/Application Support/JAMF/callManagedOSCountdown.sh
echo "/bin/rm /Library/LaunchDaemons/managedOS.plist" >> /Library/Application Support/JAMF/callManagedOSCountdown.sh
echo "/usr/local/bin/jamf policy -event $policyTriggerDeferred &" >> /Library/Application Support/JAMF/callManagedOSCountdown.sh
chown root:wheel /Library/Application Support/JAMF/callManagedOSCountdown.sh
chmod 644 /Library/Application Support/JAMF/callManagedOSCountdown.sh

## WRITE LAUNCHDAEMON 
#Create the plist
defaults write /Library/LaunchDaemons/managedOS.plist Label -string "managedOSDeferral"

#Add program argument to have it run the update script
defaults write /Library/LaunchDaemons/managedOS.plist ProgramArguments -array -string /bin/sh -string "/Library/Application Support/JAMF/callManagedOSCountdown.sh"
#Set the run inverval to run
defaults write /Library/LaunchDaemons/managedOS.plist StartInterval -integer $delayChosen 
#Set run at load
defaults write /Library/LaunchDaemons/managedOS.plist RunAtLoad -boolean yes
#Set ownership
chown root:wheel /Library/LaunchDaemons/managedOS.plist
chmod 644 /Library/LaunchDaemons/managedOS.plist

## LOAD LAUNCHDAEMON
/bin/launchctl load /Library/LaunchDaemons/managedOS.plist

I tried using cat to write the script locally and it always failed. Echoing in works, but is less than ideal. That script runs correctly when run manually on the machine.

As for the LaunchDaemon aspects, I have no idea what's going on. Sometimes it loads with a status of 127. Most of the time nothing happens. No errors to standard output. No logging of any kind even if I add that in. No feedback when it's loaded manually on the machine rather than through the script. I'm still new to LaunchD in general, so I have no idea what I'm missing here. Any suggestions?

I've been testing on macOS 10.12 through 11 with same results on all platforms.

10 REPLIES 10

sdagley
Esteemed Contributor II

@McAwesome Exactly how did writing your script via cat fail? That's a much more common mechanism of creating a LaunchDaemon .plist than using defaults write (although I commend your creativity for that approach).

In terms of diagnosing what's wrong with a LaunchDaemon, I'd highly recommend LaunchControl by soma-zone (they also produced this excellent launchd tutorial: https://launchd.info)

You should also see this article on the launchctl 2.0 syntax which provides an improved mechanism for loading/unloading LaunchDaemons/Agents: https://babodee.wordpress.com/2016/04/09/launchctl-2-0-syntax/

McAwesome
Valued Contributor

@sdagley I think I figured out why cat wasn't working. I had those segments embedded in an if section and indented to match that. For whatever reason that meant the script kept getting Unexpected End Of File errors. Removing the indentions resolved that part of the issue. Here's the revised version of that chunk.

            ## Writes a simple script that exists just to call the deferred policy

Cat > /Library/Application Support/JAMF/callManagedOSCountdown.sh <<EOF
/bin/launchctl unload /Library/LaunchDaemons/local.managedOS.plist
/bin/rm /Library/LaunchDaemons/local.managedOS.plist
/usr/local/bin/jamf policy -event $policyTriggerDeferred &
EOF
            chown root:wheel /Library/Application Support/JAMF/callManagedOSCountdown.sh
            chmod 644 /Library/Application Support/JAMF/callManagedOSCountdown.sh

            ## WRITE LAUNCHDAEMON 

            #Create the plist
cat > /Library/LaunchDaemons/local.managedOS.plist <<EOF
<?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.managedos</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/sh</string>
        <string>/Library/Application Support/JAMF/callManagedOSCountdown.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>StartInterval</key>
    <integer>$delayChosen</integer>
</dict>
</plist>
EOF

            #Set ownership
            chown root:wheel /Library/LaunchDaemons/local.managedOS.plist
            chmod 644 /Library/LaunchDaemons/local.managedOS.plist

## LOAD LAUNCHDAEMON
            launchctl load /Library/LaunchDaemons/local.managedOS.plist

Now the issue I'm running into is that either it never loads or if the string pointing to the script is incorrect it will load with status 127. I wasn't able to get the bootstrap commands to work reliably either, so since the launchd devs over there said it's likely safe for a while I'm relying on it.

Any ideas on how to correct for that part?

sdagley
Esteemed Contributor II

@McAwesome A few suggestions...

When you're writing files contains parameter substitutions with cat you need to make sure you're using the correct form. Take a look at this thread on Stack Overflow: How to cat <<EOF >> a file containing code? for examples

Your label local.managedos capitalization doesn't match your LaunchDaemon .plist name local.managedOS.plist

You need to escape the space in "Application Support" in your ProgramArguments array Turns out you don't want to escape spaces in the ProgramArguments array

As for using bootstrap and booutout, they offer precise control over the context for executing the daemon. The context for load and unload depend on the context running the command unless you also use the subcommand asuser.

McAwesome
Valued Contributor

@sdagley OK I figured out what I was doing wrong with Bootstrap and that seems to now be working reliably loading it under system now, though I'm not sure if for this purpose it's better to do that under the current signed in user or stick with system.

I wasn't escaping that character because it caused the 127 error I was getting. I now realize that was because I needed to add a space after <string>/bin/sh </string> so it would run as /bin/sh /Library/Application Support/JAMF/callManagedOSCountdown.sh instead of /bin/sh/Library/Application Support/JAMF/callManagedOSCountdown.sh, which definitely does not exist. Now I'm at error 78, which is if nothing else at least an improvement over what I was getting before, though not a particularly useful error code. Escaping that space causes errors and doesn't line up with the examples on that lanchd.info site. Same goes for adding a space into the /bin/sh bit. So I'm back at square 1.

Thanks again for the help you've given. It's pointed me in the right direction.

sdagley
Esteemed Contributor II

@McAwesome Have you tried LaunchControl to see what it thinks of your LaunchDaemon?

McAwesome
Valued Contributor

@sdagley I have. No errors, doesn't load, and attempting to force load fails for unknown reasons. That was how I discovered that I shouldn't escape the space in Application Support or it'd actually fill in Application\ Support.

sdagley
Esteemed Contributor II

@McAwesome Can you post the current version of your script and LaunchDaemon? If the LaunchDaemon is getting a clean bill of health from LaunchControl there's probably something we're missing.

McAwesome
Valued Contributor

@sdagley Here is the current version of the deferral section. I tried swapping over to scheduling it for a specific date and time rather than having the start time be every X seconds. This seems like it'd be the more reliable route for what I am going for. I can verify that the delayTime variables all get populated both in the main script and in the plist file correctly.

        #If the user select Upgrade Later, or some other value is returned, then the user is told how to run the upgrade themselves
        else

            # Making log more legible
            echo "----------SECOND PROMPT"

            delayTime=$(( $currentTime + $delayChosen ))
            echo "User has chosen to delay until after $(date -r $delayTime +'%b %d, %Y %r')"

            ## Writes a simple script that exists just to call the deferred policy

cat > /Library/Application Support/JAMF/callManagedOSCountdown.sh <<EOF
#!/bin/sh
/bin/launchctl bootout system /Library/LaunchDaemons/local.managedOS.plist
/bin/rm /Library/LaunchDaemons/local.managedOS.plist
/usr/local/bin/jamf policy -event $policyTriggerDeferred &
EOF
            chown root:wheel /Library/Application Support/JAMF/callManagedOSCountdown.sh
            chmod 755 /Library/Application Support/JAMF/callManagedOSCountdown.sh

            ## WRITE LAUNCHDAEMON 

            #Create the plist
cat > /Library/LaunchDaemons/local.managedOS.plist <<EOF
<?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.managedOS</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/sh</string>
        <string>/Library/Application Support/JAMF/callManagedOSCountdown.sh</string>
    </array>

    <key>RunAtLoad</key>
    <true/>

    <key>StartCalendarInterval</key>
    <dict>
        <key>Month</key>
        <integer>$(date -r $delayTime +%m)</integer>
        <key>Day</key>
        <integer>$(date -r $delayTime +%d)</integer>
        <key>Hour</key>
        <integer>$(date -r $delayTime +%H)</integer>
        <key>Minute</key>
        <integer>$(date -r $delayTime +%M)</integer>
    </dict>

    <key>StandardErrorPath</key>
    <string>/Users/<<redacted>>/stderr.log</string>
    <key>StandardOutPath</key>
    <string>/Users/<<redacted>>/stdout.log</string>

</dict>
</plist>
EOF

            #Set ownership
            chown root:wheel /Library/LaunchDaemons/local.managedOS.plist
            chmod 755 /Library/LaunchDaemons/local.managedOS.plist

        ## Bootstrap The LaunchDaemon
        launchctl bootstrap system /Library/LaunchDaemons/local.managedOS.plist

McAwesome
Valued Contributor

I can provide the entire script as well, though I'll have to verify it doesn't reference anything specific to my workplace that isn't essential. I don't think it does, other than where I was temporarily storing the logs.

sdagley
Esteemed Contributor II

@McAwesome This is bringing back memories of a problem I was having running a script that would call jamfHelper from a LaunchDaemon where the behavior differed depending on if the policy was being run from Self Service or a periodic checkin. I finally ended up triggering the script using this for ProgramArguments in my LaunchDaemon:

    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/jamf/bin/jamf</string>
        <string>runScript</string>
        <string>-script</string>
        <string>NameOfScript.sh</string>
        <string>-path</string>
        <string>/Library/Application Support/MyOrg</string>
    </array>

Before you consider that, 2 last things to suggest:
- Use 644 as the permissions for the LaunchDaemon .plist
- Remove the .plist extension in your call to launchctl bootout
(so it's just /bin/launchctl bootout system /Library/LaunchDaemons/local.managedOS)

WARNING - In case anyone finds this post and thinks of trying the jamf runScript verb, don't. The Jamf Pro 10.28.0 release notes list it as deprecated.