Force Users to Restart or Shutdown by Profile not Policy

QGBrian
New Contributor II

Hi Jamf Community,

Our users are notorious for not shutting down their Macbooks nightly, which leads to countless issues and occasionally Jamf stops checking in or sending inventory. Currently I have a smart group that finds users that haven't rebooted in 5+ days. I have this smart group scoped to a policy that notifies the user and forces a shut down after 120 minutes, unless the reboot themselves. The problem is, if the laptop isn't checking in or the policy fails, the shut down never occurs as the policy remains pending and the laptop remains forever disconnected from Jamf (until we get our hands on it).

I am hoping to find a way to force shutdown or force restart any laptop that hasn't been rebooted in 5 days regardless of it's connection to Jamf. My thought is this needs to be a Policy rather than a Profile and needs to be sitting waiting on the device so that it can execute even when offline or failing to communicate with Jamf.

Any ideas?

Thank you!

18 REPLIES 18

mm2270
Legendary Contributor III

Given the core issue you've described, in that sometimes a Mac will stop checking into Jamf, then using a policy, as you already described, won't really be a good solution. You need something that will work "offline" and even when not connected to Jamf (regardless of if the device is successfully able to check in or not).

One possible way to address this would be with a LaunchDaemon and companion script. The script could run periodically, say every 5 minutes, checks the uptime, and if it's less than 5 days, then it just exits. If it's equal to or more than 5 days, it can alert the user and start a 120 minute shutdown countdown.

Do you think something like this could work for your scenario?

QGBrian
New Contributor II

Yes, that would be perfect. Any chance you have a script you can share?

I was testing last night using an Energy Saver profile set to Schedule a shutdown day and time, but it appears that gives the user a 10min countdown including the ability to cancel. If the laptop is in Sleep, the prompt displays when the laptop is opened, but again gives the option to Cancel. I would love a way to force the Energy Saver scheduled Shutdown.

mm2270
Legendary Contributor III

Hi. I don't have an exact script already created to do this, but putting one together won't take long. Give me a little bit and I will post back with a draft of something you can try.

One thing I didn't mention about the above is that, once a LaunchDaemon is deployed, you don't have direct control over how it functions. You can always adjust the script or LaunchDaemon or even disable it from a Jamf policy if needed, but when the Macs are disconnected from your Jamf server, either by not being on the proper network or some other reason, it will continue to run independently from Jamf. That's both good and bad, so just something to keep in mind.

QGBrian
New Contributor II

That would be great, thank you!

The LaunchDaemon seems to accomplish what I am looking for because I want this to run regardless of connection to Jamf. These devices are company owned and managed as well as enrolled in ABM. I am not sure I see any risk of this LaunchDaemon running uncontrollably. What cons concern you?

mm2270
Legendary Contributor III

I don't have any major concerns about it. It's just that unlike a Jamf Pro policy where you can quickly go into the policy and either disable it entirely, or add a system into the Exclusion tab to ensure it won't run on it, once you push out a LaunchDaemon and script, it won't be dependent or tied to anything in your Jamf console. You can still manipulate it from Jamf, but would need to put some commands into the Execute Command field or craft a script to make any changes to it, and then wait for your devices to check in. Not quite as direct as with policies, but not horrible either.

And no, there's really no risk of it running uncontrollably that I see, other than if something happened with the script where it stopped detecting the correct amount of uptime somehow.

QGBrian
New Contributor II

Got it. We can do extensive testing on our side before deploying across 500+ machines.

If you have a Daemon and script you could share, that would be great, as I am not sure how to make one.

Thank you!

mm2270
Legendary Contributor III

Here's an example script. Feel free to edit it however you need.

#!/bin/zsh

jamf_helper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper"

## Gets boot time in unix seconds
uptime_raw=$(/usr/sbin/sysctl kern.boottime | awk -F'[= |,]' '{print $6}')

## Gets current time in unix seconds
time_now=$(date +"%s")

## Convert to uptime in days
uptime_days=$(($((time_now-uptime_raw))/3600/24))

/bin/echo "This Mac has been up for $uptime_days days"

if [ "$uptime_days" -ge 5 ]; then
	/bin/echo "The uptime maximum has been reached."
	
	## Section below uses jamfHelper for the dialog. This can be swapped out for a different messaging tool if desired
	"$jamf_helper" \
		-windowType hud \
		-heading "Your Mac must reboot soon" \
		-description "Your Mac has been running for more than 5 days. In order to maintain a good working system, a restart is necessary. You have 2 hours until your Mac will be automatically restarted. You can also restart on your own when ready." \
		-icon "/System/Library/CoreServices/loginwindow.app/Contents/Resources/Restart.tiff" \
		-lockHUD \
		-timeout 7200 \
		-windowPosition lr \
		-countdown

	## Initiate immediate shutdown
	/sbin/shutdown -r now
else
	/bin/echo "Uptime limit not reached yet. Exiting."
	exit 0
fi

 

For the LaunchDaemon, it would be something like 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.company.label</string>
	<key>ProgramArguments</key>
	<array>
		<string>/path/to/script_name.sh</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
	<key>StartInterval</key>
	<integer>300</integer>
</dict>
</plist>

The above has generic info in it, so you have to change the "com.company.label" string to something that makes sense for you. Usually those are in reverse URL format, like com.acme.uptimereminder or something.

Also change the /path/to/script_name.sh with the actual path to where the script would be pushed locally on the Mac.

Lastly, the "300" integer value means it runs every 5 minutes (300 seconds), which is more than enough. You could shorten or lengthen that, but I'd suggest avoiding having this run too frequently since you're talking about only needing to take action after 5+ days of uptime. Even every 30 or 60 minutes is probably frequent enough actually.

Try those out and let me know if you run into any issues with it.

QGBrian
New Contributor II

I've updated the script and Daemon to match my needs for testing. I first created the script in Jamf and ran it via Policy successfully. But when trying to load and run it locally I am having issues. When running launchctl list I get Status 78.

  1. I created the shell script in BBEdit and saved it to my downloads.
  2. Then moved it to my usr/local
  3. I then created the Daemon and saved it as com.company.forcedshutdown.plist
  4. Then copied it via Terminal to /Library/LaunchDaemons.
  5. I then ran sudo launchctl load -w /Library/LaunchDaemons/com.company.forcedshutdown.plist
  6. Then ran sudo launchctl list and saw the Status 78.

Did I miss a step?

mm2270
Legendary Contributor III

Hey there. It's most likely the owner/group and permissions on the LaunchDaemon that aren't set correctly, but it might be helpful if you could post how the LaunchDaemon is formatted here when you can.

But in the meantime, launchd jobs have to be set to an owner/group of root/wheel, and the POSIX permissions usually need to be 644. You can fix these by running these commands in Terminal

 

sudo chown root:wheel /Library/LaunchDaemons/com.company.forcedshutdown.plist
sudo chmod 644 /Library/LaunchDaemons/com.company.forcedshutdown.plist

 

Then try loading it again.

Edit: Just taking one other look, running launchctl error 78 indicates:

78: Function not implemented

Which isn't particularly helpful. Maybe post the LaunchDaemon plist contents when you can. There may be some formatting error or something.

QGBrian
New Contributor II

Those commands don't appear to have made a change. Here is what I see before and after:

-rw-r--r--@    1     root     wheel      427     Apr  7     18:05    com.company.forcedshutdown.plist

mm2270
Legendary Contributor III

Can you post the contents of the LaunchDaemon plist? Or if you prefer not to do that, then run this against the plist to see if it's verifying as a valid xml file

xmllint --format /Library/LaunchDaemons/com.company.forcedshutdown.plist

QGBrian
New Contributor II
brian@L952-Brian LaunchDaemons % xmllint --format /Library/LaunchDaemons/com.company.forcedshutdown.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.company.forcedshutdown</string>
    <key>ProgramArguments</key>
    <array>
      <string>/usr/local/Forced\ Shutdown.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>StartInterval</key>
    <integer>60</integer>
  </dict>
</plist>

mm2270
Legendary Contributor III

Ok, I believe the problem is in this line

<string>/usr/local/Forced\ Shutdown.sh</string>

Believe it or not, with a Launchd Program or ProgramArguments string, you don't need to escape spaces, so remove the \ character. It should look like this:

<string>/usr/local/Forced Shutdown.sh</string>

But a quick word of advice - when naming scripts, it's really better not to use spaces. Replace them with underscores or dashes to avoid any future problems.

QGBrian
New Contributor II

That worked! Thank you. Yeah I normally stay away from the spaces in file names but had seen a Jamf video where there were spaces in their script name. The \ came from dragging the file name into terminal when finding the path I think.

Thank you again. We will do some more testing before packaging and deploying to our users via Jamf.

QGBrian
New Contributor II

Hi! Testing has been going great and I am playing around with some of the JamfHelper options. One thing I cannot figure out is how to let the user acknowledge the countdown and close the window without initiating an instant shutdown. I tried adding -cancelButton as well as -showDelayOptions, but neither worked. Any ideas?

mm2270
Legendary Contributor III

Hmm, yes, that's possible with a little reworking of the script.

Change this section of the script

if [ "$uptime_days" -ge 5 ]; then
	/bin/echo "The uptime maximum has been reached."
	
	## Section below uses jamfHelper for the dialog. This can be swapped out for a different messaging tool if desired
	## Changes:
	##    '&' is added to the end to place the dialog in the background
	##    changed to 'utility' from 'hud' and added OK button to the window
	##    changed 'shutdown -r now' to 'shutdown -r +120' to initiate a 2 hour countdown to restart
	"$jamf_helper" \
		-windowType hud \
		-heading "Your Mac must reboot soon" \
		-description "Your Mac has been running for more than 5 days. In order to maintain a good working system, a restart is necessary. You have 2 hours until your Mac will be automatically restarted. You can also restart on your own when ready." \
		-icon "/System/Library/CoreServices/loginwindow.app/Contents/Resources/Restart.tiff" \
		-timeout 7200 \
		-windowPosition lr \
		-countdown \
		-button1 "OK" \
		-defaultButton 1 &

	## Initiate 2 hour countdown to a restart
	/sbin/shutdown -r +120
else
	/bin/echo "Uptime limit not reached yet. Exiting."
	exit 0
fi

Note this section of the updated script

## Changes:
## '&' is added to the end to place the dialog in the background
## changed to 'utility' from 'hud' and added OK button to the window
## changed 'shutdown -r now' to 'shutdown -r +120 to initiate a 2 hour countdown to restart

So basically, the dialog comes up, stays on screen for the duration, OR the user can dismiss it and the restart countdown continues in the background either way.

Keep in mind though that because this is a 2 hour delay, some people could forget that their Mac will restart and end up surprised when it spontaneously restarts in the middle of something later.

QGBrian
New Contributor II

Thank you!

Testing is going well and we've made a few tweaks. Most notably, we had to initiate the shutdown before the Jamf Helper window, otherwise the shutdown minutes didn't seem to start until after they hit Accept. This meant they could wait 120min then the timer would shutdown in another 120min for a total of 4hrs from pop up to shutdown. Second, we've found that putting the computer to Sleep during the countdown pauses the countdown, but I am not sure that is a critical issue.

Where we still have issues/testing to do is actually around deployment through Jamf. We want to load the daemon after the package is deployed, but if a previous version is already running, you receive an error as you can't load a plist that is already loaded. You also can't unload a plist that isn't loaded, so we can't always unload before load. Any ideas?

#!/bin/zsh

jamf_helper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper"

## Gets boot time in unix seconds
uptime_raw=$(/usr/sbin/sysctl kern.boottime | awk -F'[= |,]' '{print $6}')

## Gets current time in unix seconds
time_now=$(date +"%s")

## Convert to uptime in days
uptime_days=$(($((time_now-uptime_raw))/60/60/24))

/bin/echo "This Mac has been up for $uptime_days days"

if [ "$uptime_days" -ge 1 ]; then
	/bin/echo "The uptime maximum has been reached."

	## Initiate shutdown now or +minutes
	/sbin/shutdown -r +120
	
	## Section below uses jamfHelper for the dialog. This can be swapped out for a different messaging tool if desired
	"$jamf_helper" \
		-windowType utility \
		-title "Shutdown Required" \
		-heading "Your Mac is overdue for a reboot." \
		-description "Your Mac has been running for more than 5 days. In order to maintain a good working system, a restart is necessary. You have 2 hours until your Mac will automatically shutdown. You can also restart on your own when ready within 2 hours." \
		-icon "/System/Library/CoreServices/loginwindow.app/Contents/Resources/Restart.tiff" \
		-timeout 7200 \
		-countdown \
	    -button1 "Accept" \
	    -defaultButton 1 \


else
	/bin/echo "Uptime limit not reached yet. Exiting."
	exit 0
fi

 

Were you able to get this script to work? I've been playing with this copy of it and didn't go the daemon route, just running it with reoccurring. The original one caused a reboot loop, so I'm testing yours now. Any modifications you suggest or recommend since this was posted?