Updated version now available: DDM OS Reminder (2.1.0)
A major update to Mac Admins’ new favorite, MDM-agnostic, “set-it-and-forget-it” end-user reminder for Apple’s Declarative Device Management-enforced macOS update deadlines — now with Configuration Profile support and a
demomode for easy reminder dialog testing
Overview
While Apple’s Declarative Device Management (DDM) provides Mac Admins a powerful way to enforce macOS updates, its built-in notification is often too subtle for most:


DDM OS Reminder evaluates the most recent EnforcedInstallDate and setPastDuePaddedEnforcementDate entries in /var/log/install.log, then leverages a swiftDialog-enabled script and LaunchDaemon pair to dynamically deliver a more prominent end-user dialog reminding users to update their Mac to comply with DDM-enforced macOS update deadlines.


Features

Mac Admins can configure
daysBeforeDeadlineBlurscreento control how many days before the DDM-specified deadline the screen blurs when displaying your customized reminder dialog
- Customizable: Easily customize the reminder dialog’s title, message, icons and button text to fit your organization’s requirements by editing the included
.plistand distributing as part of a Configuration Profile via any MDM solution. - Easy Installation: The
assemble.zshscript makes it easy to deploy your reminder dialog and display frequency customizations via any MDM solution, so you can quickly roll out DDM OS Reminder across your entire organization. - Set-it-and-forget-it: Once configured and installed, a LaunchDaemon displays your customized reminder dialog — which automatically checks the installed version of macOS against the DDM-required macOS version — to remind your users if an update is required.
- Deadline Awareness: Each time a DDM-enforced macOS version or its deadline is updated via your MDM solution, the reminder dialog dynamically updates the countdown to the deadline and required macOS version, creating a sense of urgency for end users to update their Macs.
- Intelligently Intrusive: The reminder dialog is designed to be informative without being overly disruptive — first checking if the user is in an online meeting — so users can continue their work while still being reminded of the need to update.
- Logging: The script logs its actions to your specified log file, allowing Mac Admins to monitor its activity and troubleshoot as necessary.
- Demonstration Mode: A built-in
demomode allows Mac Admins to easily test the reminder dialog’s appearance and functionality.

Implementation
1. Demonstration
Jumpstart your DDM OS Reminder implementation by first downloading the
mainbranch of the GitHub repository and interacting with a demonstration on a non-production Mac.(Ideally, use a non-production Mac which is already in-scope of a pending Declarative Device Management-enforced macOS update from your MDM server.)
- Visit the DDM OS Reminder repository on GitHub.com
- Download the
mainbranch by selecting: Code > Download ZIP

- In an elevated Terminal session, change to the downloaded
DDM-OS-Reminder-maindirectory
cd ~/Downloads/DDM-OS-Reminder-main
- Execute the
reminderDialog.zshscript indemomode (elevated privileges required):
zsh reminderDialog.zsh demo

- Review the reminder dialog and interact with each of its buttons, re-executing
zsh reminderDialog.zsh demoas required - Review the script’s output:
root@XDT8675309 DDM-OS-Reminder-main # zsh reminderDialog.zsh demo dorm (2.0.0): 2025-12-06 12:00:31 - [PRE-FLIGHT] ### # DDM OS Reminder End-user Message (2.0.0) # http://snelson.us/ddm #### dorm (2.0.0): 2025-12-06 12:00:31 - [PRE-FLIGHT] Initiating … dorm (2.0.0): 2025-12-06 12:00:31 - [PRE-FLIGHT] Check for Logged-in System Accounts … dorm (2.0.0): 2025-12-06 12:00:31 - [PRE-FLIGHT] Current Logged-in User: dan dorm (2.0.0): 2025-12-06 12:00:31 - [PRE-FLIGHT] Current Logged-in User First Name (ID): Dan (502) dorm (2.0.0): 2025-12-06 12:00:31 - [PRE-FLIGHT] Complete dorm (2.0.0): 2025-12-06 12:00:31 - [NOTICE] Demo mode enabled dorm (2.0.0): 2025-12-06 12:00:34 - [NOTICE] Display Reminder Dialog to dan with additional options: --ontop dorm (2.0.0): 2025-12-06 12:00:53 - [INFO] Return Code: 0 dorm (2.0.0): 2025-12-06 12:00:53 - [NOTICE] dan clicked Open Software Update dorm (2.0.0): 2025-12-06 12:00:54 - [NOTICE] Checking if System Settings is open … dorm (2.0.0): 2025-12-06 12:00:54 - [INFO] System Settings is open; Telling System Settings to make a guest appearance … dorm (2.0.0): 2025-12-06 12:00:54 - [QUIT] Exiting … dorm (2.0.0): 2025-12-06 12:00:54 - [QUIT] Keep them movin' blades sharp!
1.1 Optional Demonstration Steps
While the following steps are optional, they should prove at least somewhat informative.
- Change
demoDeadlineOffsetDaysto a positive value to disableblurscreenand re-executezsh reminderDialog.zsh demo- Note: Only demo-related variables aren’t superseded by the Configuration Profile
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Demo Mode (i.e., zsh ~/Downloads/reminderDialog.zsh demo)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ "${1}" == "demo" ]]; then
notice "Demo mode enabled"
# Installed vs Required Version
installedmacOSVersion=$( sw_vers -productVersion )
demoMajorVersion="${installedmacOSVersion%%.*}"
ddmVersionString="${demoMajorVersion}.99"
# Days from today to simulate deadline (can be + or -)
demoDeadlineOffsetDays=-3 # positive → future deadline; negative → past due
if (( demoDeadlineOffsetDays < 0 )); then # Normalize the offset so “-3” becomes "-3d" and “7” becomes "+7d"
offsetString="${demoDeadlineOffsetDays}d" # → "-3d"
blurscreen="--blurscreen"
else
offsetString="+${demoDeadlineOffsetDays}d" # → "+7d"
blurscreen="--noblurscreen"
fi
- Execute the sample assembled script — with elevated privileges — which installs the combination of the following on your test Mac:
- Sample
reminderDialog.zsh - Sample
launchDaemonManagment.zsh
- Sample
zsh Resources/ddm-os-reminder-assembled-*.zsh
root@XDT8765309 DDM-OS-Reminder-main # zsh Resources/ddm-os-reminder-assembled-*.zsh dor (2.0.0): 2025-12-06 12:46:08 - [PRE-FLIGHT] ### # DDM OS Reminder (2.0.0) # http://snelson.us/ddm # # Reset Configuration: All ### dor (2.0.0): 2025-12-06 12:46:08 - [PRE-FLIGHT] Initiating … dor (2.0.0): 2025-12-06 12:46:08 - [PRE-FLIGHT] Complete! dor (2.0.0): 2025-12-06 12:46:08 - [PRE-FLIGHT] swiftDialog version 3.0.0.4922 found; proceeding... dor (2.0.0): 2025-12-06 12:46:08 - [NOTICE] Reset Configuration: All dor (2.0.0): 2025-12-06 12:46:09 - [INFO] Reset All Configuration Files … dor (2.0.0): 2025-12-06 12:46:09 - [INFO] Reset LaunchDaemon … dor (2.0.0): 2025-12-06 12:46:09 - [NOTICE] LaunchDaemon Status dor (2.0.0): 2025-12-06 12:46:09 - org.churchofjesuschrist.dor is NOT loaded dor (2.0.0): 2025-12-06 12:46:09 - Removing '/Library/LaunchDaemons/org.churchofjesuschrist.dor.plist' … dor (2.0.0): 2025-12-06 12:46:09 - Removed '/Library/LaunchDaemons/org.churchofjesuschrist.dor.plist' dor (2.0.0): 2025-12-06 12:46:09 - [INFO] Reset Script … dor (2.0.0): 2025-12-06 12:46:09 - Removing '/Library/Management/org.churchofjesuschrist/dor.zsh' … dor (2.0.0): 2025-12-06 12:46:09 - Removed '/Library/Management/org.churchofjesuschrist/dor.zsh' dor (2.0.0): 2025-12-06 12:46:09 - [NOTICE] Validating Script dor (2.0.0): 2025-12-06 12:46:09 - [NOTICE] Create 'DDM OS Reminder' script: /Library/Management/org.churchofjesuschrist/dor.zsh dor (2.0.0): 2025-12-06 12:46:09 - DDM OS Reminder script created dor (2.0.0): 2025-12-06 12:46:09 - Setting permissions … dor (2.0.0): 2025-12-06 12:46:09 - [NOTICE] Validating LaunchDaemon dor (2.0.0): 2025-12-06 12:46:09 - Checking for LaunchDaemon '/Library/LaunchDaemons/org.churchofjesuschrist.dor.plist' … dor (2.0.0): 2025-12-06 12:46:09 - [NOTICE] Create LaunchDaemon dor (2.0.0): 2025-12-06 12:46:09 - Creating '/Library/LaunchDaemons/org.churchofjesuschrist.dor.plist' … dor (2.0.0): 2025-12-06 12:46:09 - Setting permissions for '/Library/LaunchDaemons/org.churchofjesuschrist.dor.plist' … dor (2.0.0): 2025-12-06 12:46:09 - Loading 'org.churchofjesuschrist.dor' … dor (2.0.0): 2025-12-06 12:46:09 - [NOTICE] Status Checks dor (2.0.0): 2025-12-06 12:46:09 - I/O pause … dor (2.0.0): 2025-12-06 12:46:11 - [NOTICE] LaunchDaemon Status dor (2.0.0): 2025-12-06 12:46:11 - 67641 0 org.churchofjesuschrist.dor
- Monitor the client-side log file (using Control-C to stop)
tail -f /var/log/org.churchofjesuschrist.log
root@XDT8675309 DDM-OS-Reminder-main # tail -f /var/log/org.churchofjesuschrist.log dorm (2.0.0): 2025-12-06 12:46:09 - [INFO] DDM-enforced OS Version: 26.17 dorm (2.0.0): 2025-12-06 12:46:09 - [INFO] DDM-enforced OS Version Deadline: Fri, 12-Dec-2025, 6:00 p.m. dorm (2.0.0): 2025-12-06 12:46:09 - [NOTICE] Within 14-day reminder window; proceeding with reminder. dorm (2.0.0): 2025-12-06 12:46:09 - [NOTICE] Check dan's Display Sleep Assertions dorm (2.0.0): 2025-12-06 12:46:10 - [INFO] dan's Display Sleep Assertion has ended after 0 minute(s). dorm (2.0.0): 2025-12-06 12:46:10 - [NOTICE] No active Display Sleep Assertions detected; proceeding with reminder. dorm (2.0.0): 2025-12-06 12:46:10 - [NOTICE] Login Trigger Pause: Random 30 to 90 seconds dorm (2.0.0): 2025-12-06 12:46:10 - [INFO] Pausing for 1 minute(s), 10 second(s) … dor (2.0.0): 2025-12-06 12:46:11 - [NOTICE] LaunchDaemon Status dor (2.0.0): 2025-12-06 12:46:11 - 67641 0 org.churchofjesuschrist.dor dorm (2.0.0): 2025-12-06 12:47:21 - [NOTICE] Display Reminder Dialog to dan with additional options: --ontop dorm (2.0.0): 2025-12-06 12:47:38 - [INFO] Return Code: 2 dorm (2.0.0): 2025-12-06 12:47:38 - [NOTICE] dan clicked Remind Me Later dorm (2.0.0): 2025-12-06 12:47:38 - [QUIT] Exiting … dorm (2.0.0): 2025-12-06 12:47:38 - [QUIT] Keep them movin' blades sharp! ^C
- Kickstart the DDM OS Reminder LaunchDaemon (elevated privileges required)
launchctl kickstart -kp system/org.churchofjesuschrist.dor
root@XDT87675309 DDM-OS-Reminder-main # launchctl kickstart -kp system/org.churchofjesuschrist.dor service spawned with pid: 86753099
- Again Monitor the client-side log file (use Control-C to stop)
tail -f /var/log/org.churchofjesuschrist.log
- In your preferred code editor, open
Resources/ddm-os-reminder-assembled-*.zshand modify the value ofresetConfigurationto beUninstall
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# MDM Script Parameters
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Parameter 4: Configuration Files to Reset (i.e., None (blank) | All | LaunchDaemon | Script | Uninstall )
resetConfiguration="${4:-"Uninstall"}"
- Re-execute the sample assembled script (with elevated privileges) to uninstall DDM OS Reminder
zsh Resources/ddm-os-reminder-assembled-*.zsh
root@XDT8675309 DDM-OS-Reminder-main # zsh Resources/ddm-os-reminder-assembled-*.zsh dor (2.0.0): 2025-12-06 13:56:01 - [PRE-FLIGHT] ### # DDM OS Reminder (2.0.0) # http://snelson.us/ddm # # Reset Configuration: Uninstall ### dor (2.0.0): 2025-12-06 13:56:01 - [PRE-FLIGHT] Initiating … dor (2.0.0): 2025-12-06 13:56:01 - [PRE-FLIGHT] Complete! dor (2.0.0): 2025-12-06 13:56:01 - [PRE-FLIGHT] swiftDialog version 3.0.0.4922 found; proceeding... dor (2.0.0): 2025-12-06 13:56:01 - [NOTICE] Reset Configuration: Uninstall dor (2.0.0): 2025-12-06 13:56:01 - [WARNING] *** UNINSTALLING DDM OS Reminder *** dor (2.0.0): 2025-12-06 13:56:01 - [INFO] Uninstall LaunchDaemon … dor (2.0.0): 2025-12-06 13:56:01 - [NOTICE] LaunchDaemon Status dor (2.0.0): 2025-12-06 13:56:01 - - 0 org.churchofjesuschrist.dor dor (2.0.0): 2025-12-06 13:56:01 - Unload '/Library/LaunchDaemons/org.churchofjesuschrist.dor.plist' … dor (2.0.0): 2025-12-06 13:56:01 - [NOTICE] LaunchDaemon Status dor (2.0.0): 2025-12-06 13:56:01 - org.churchofjesuschrist.dor is NOT loaded dor (2.0.0): 2025-12-06 13:56:01 - Removing '/Library/LaunchDaemons/org.churchofjesuschrist.dor.plist' … dor (2.0.0): 2025-12-06 13:56:02 - Removed '/Library/LaunchDaemons/org.churchofjesuschrist.dor.plist' dor (2.0.0): 2025-12-06 13:56:02 - [INFO] Uninstall Script … dor (2.0.0): 2025-12-06 13:56:02 - Removing '/Library/Management/org.churchofjesuschrist/dor.zsh' … dor (2.0.0): 2025-12-06 13:56:02 - Removed '/Library/Management/org.churchofjesuschrist/dor.zsh' dor (2.0.0): 2025-12-06 13:56:02 - Organization directory not empty; other management files may still exist — leaving intact: /Library/Management/org.churchofjesuschrist dor (2.0.0): 2025-12-06 13:56:02 - Uninstalled all DDM OS Reminder configuration files dor (2.0.0): 2025-12-06 13:56:02 - [NOTICE] Thanks for trying DDM OS Reminder!
2. Basic Deployment
A basic deployment of DDM OS Reminder starts by distributing the sample
.plistas a Configuration Profile, then distributing the sample assembled script via your MDM solution(The following instructions presume you have completed the steps outlined in 1. Demonstration.)
- Use your MDM to distribute the sample
org.churchofjesuschrist.dorm.plist— as-is — to your test Mac, being certain to use the following preference domain:org.churchofjesuschrist.dorm- NOTE: Some Mac Admins have reported better success by deploying a
.mobileconfigfile, which can be easily generated by first runningResources/createPlist.zsh(version2.1.0b3or later).
- NOTE: Some Mac Admins have reported better success by deploying a
Sample org.churchofjesuschrist.dorm.plist
Latest version available on GitHub.
<?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>
<!-- Logging -->
<key>ScriptLog</key>
<string>/var/log/org.churchofjesuschrist.log</string>
<!-- Reminder timing -->
<key>DaysBeforeDeadlineDisplayReminder</key>
<integer>14</integer>
<key>DaysBeforeDeadlineBlurscreen</key>
<integer>3</integer>
<key>MeetingDelay</key>
<integer>75</integer>
<!-- Branding -->
<key>OrganizationOverlayIconURL</key>
<string>https://usw2.ics.services.jamfcloud.com/icon/hash_4804203ac36cbd7c83607487f4719bd4707f2e283500f54428153af17da082e2</string>
<key>SwapOverlayAndLogo</key>
<false/>
<key>DateFormatDeadlineHumanReadable</key>
<string>+%a, %d-%b-%Y, %-l:%M %p</string>
<!-- Support block -->
<key>SupportTeamName</key>
<string>IT Support</string>
<key>SupportTeamPhone</key>
<string>+1 (801) 555-1212</string>
<key>SupportTeamEmail</key>
<string>rescue@domain.org</string>
<key>SupportTeamWebsite</key>
<string>https://support.domain.org</string>
<key>SupportKB</key>
<string>KB8675309</string>
<key>InfoButtonAction</key>
<string>https://servicenow.domain.org/support?id=kb_article_view&sysparm_article=KB8675309</string>
<key>SupportKBURL</key>
<string>[KB8675309](https://servicenow.domain.org/support?id=kb_article_view&sysparm_article=KB8675309)</string>
<!-- Dialog text -->
<key>Title</key>
<string>macOS {titleMessageUpdateOrUpgrade} required</string>
<key>Button1Text</key>
<string>Open Software Update</string>
<key>Button2Text</key>
<string>Remind Me Later</string>
<key>InfoButtonText</key>
<string>KB8675309</string>
<key>Message</key>
<string>**A required macOS {titleMessageUpdateOrUpgrade} is now available**<br>---<br>Happy {weekday}, {loggedInUserFirstname}!<br><br>Please {titleMessageUpdateOrUpgrade} to macOS **{ddmVersionString}** to ensure your Mac remains secure and compliant with organizational policies.<br><br>To perform the {titleMessageUpdateOrUpgrade} now, click **{button1text}**, review the on-screen instructions, then click **{softwareUpdateButtonText}**.<br><br>If you are unable to perform this {titleMessageUpdateOrUpgrade} now, click **{button2text}** to be reminded again later.<br><br>However, your device **will automatically restart and {titleMessageUpdateOrUpgrade}** on **{ddmEnforcedInstallDateHumanReadable}** if you have not {titleMessageUpdateOrUpgrade}d before the deadline.<br><br>For assistance, please contact **{supportTeamName}** by clicking the (?) button in the bottom, right-hand corner.</string>
<!-- Infobox -->
<key>InfoBox</key>
<string>**Current:** {installedmacOSVersion}<br><br>**Required:** {ddmVersionString}<br><br>**Deadline:** {ddmVersionStringDeadlineHumanReadable}<br><br>**Day(s) Remaining:** {ddmVersionStringDaysRemaining}</string>
<!-- Help section -->
<key>HelpMessage</key>
<string>For assistance, please contact: **{supportTeamName}**<br>- **Telephone:** {supportTeamPhone}<br>- **Email:** {supportTeamEmail}<br>- **Website:** {supportTeamWebsite}<br>- **Knowledge Base Article:** {supportKBURL}<br><br>**User Information:**<br>- **Full Name:** {userfullname}<br>- **User Name:** {username}<br><br>**Computer Information:**<br>- **Computer Name:** {computername}<br>- **Serial Number:** {serialnumber}<br>- **macOS:** {osversion}<br><br>**Script Information:**<br>- **Dialog:** {dialogVersion}<br>- **Script:** {scriptVersion}<br></string>
<key>HelpImage</key>
<string>qr={infobuttonaction}</string>
</dict>
</plist>
- Use your MDM to distribute the sample
Resources/ddm-os-reminder-assembled-YYYY-MM-DD-HHMMSS.zshscript to your test Mac
Optional: Usezsh Resources/createSelfExtracting.zshto create a self-extracting script (which is easier to deploy with some MDMs)
Resources/ddm-os-reminder-assembled-YYYY-MM-DD-HHMMSS.zsh
Latest version available on GitHub in the Resources directory.
#!/bin/zsh --no-rcs
# shellcheck shell=bash
####################################################################################################
#
# DDM OS Reminder
# https://snelson.us/ddm
#
# Mac Admins’ new favorite, MDM-agnostic, “set-it-and-forget-it” end-user messaging for Apple’s
# Declarative Device Management-enforced macOS update deadlines.
#
# While Apple's Declarative Device Management (DDM) provides Mac Admins a powerful method to enforce
# macOS updates, its built-in notification tends to be too subtle for most Mac Admins.
#
# DDM OS Reminder evaluates the most recent `EnforcedInstallDate` and `setPastDuePaddedEnforcementDate`
# entries in `/var/log/install.log`, then leverages a swiftDialog-enabled script and LaunchDaemon pair
# to dynamically deliver a more prominent end-user message of when the user’s Mac needs to be updated
# to comply with DDM-enforced macOS update deadlines.
#
####################################################################################################
#
# HISTORY
#
# Version 2.0.0, 06-Dec-2025, Dan K. Snelson (@dan-snelson)
# - Reorganized script structure for (hopefully) improved clarity
# - Defined `swiftDialogMinimumRequiredVersion` (Addresses #16; thanks for the heads-up, @deski-arnaud!)
# - Refactored `displayReminderDialog` function's Exit Code `3` to re-display dialog after 61 seconds when infobutton (i.e., KB) is clicked (Inspired by Pull Request: #20; thanks, @TazNZ!)
# - Refactored `daysBeforeDeadlineBlurscreen` logic to use seconds (instead of days) for more precise control (thanks for the suggestion, @Ancaeus!)
# - Added a "demo" mode to the `reminderDialog.zsh` script for testing purposes (thanks for the suggestion, Max S!)
# - Added ability to read variables from `.plist` (Pull Request #22; thanks, Obi-@maxsundellacne!)
#
####################################################################################################
####################################################################################################
#
# Global Variables
#
####################################################################################################
export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local:/usr/local/bin
# Script Version
scriptVersion="2.0.0"
# Client-side Log
scriptLog="/var/log/org.churchofjesuschrist.log"
# Minimum Required Version of swiftDialog
swiftDialogMinimumRequiredVersion="2.5.6.4805"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# MDM Script Parameters
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Parameter 4: Configuration Files to Reset (i.e., None (blank) | All | LaunchDaemon | Script | Uninstall )
resetConfiguration="${4:-"All"}"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Organization Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Organization's Reverse Domain Name Notation (i.e., com.company.division)
reverseDomainNameNotation="org.churchofjesuschrist"
# Script Human-readabale Name
humanReadableScriptName="DDM OS Reminder"
# Organization's Script Name
organizationScriptName="dor"
# Organization's Directory (i.e., where your client-side scripts reside)
organizationDirectory="/Library/Management/org.churchofjesuschrist"
# LaunchDaemon Name & Path
launchDaemonLabel="${reverseDomainNameNotation}.${organizationScriptName}"
launchDaemonPath="/Library/LaunchDaemons/${launchDaemonLabel}.plist"
####################################################################################################
#
# Functions
#
####################################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Client-side Logging
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function updateScriptLog() {
echo "${organizationScriptName} ($scriptVersion): $( date +%Y-%m-%d\ %H:%M:%S ) - ${1}" | tee -a "${scriptLog}"
}
function preFlight() { updateScriptLog "[PRE-FLIGHT] ${1}"; }
function logComment() { updateScriptLog " ${1}"; }
function notice() { updateScriptLog "[NOTICE] ${1}"; }
function info() { updateScriptLog "[INFO] ${1}"; }
function errorOut() { updateScriptLog "[ERROR] ${1}"; }
function error() { updateScriptLog "[ERROR] ${1}"; let errorCount++; }
function warning() { updateScriptLog "[WARNING] ${1}"; let errorCount++; }
function fatal() { updateScriptLog "[FATAL ERROR] ${1}"; exit 1; }
function quitOut() { updateScriptLog "[QUIT] ${1}"; }
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Reset Configuration
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function resetConfiguration() {
notice "Reset Configuration: ${1}"
# Ensure the directory exists
mkdir -p "${organizationDirectory}"
# Secure ownership
chown -R root:wheel "${organizationDirectory}"
# Secure directory permissions (no world-writable bits)
chmod 755 "${organizationDirectory}"
chmod 755 "${organizationDirectory}/${reverseDomainNameNotation}"
case ${1} in
"All" )
info "Reset All Configuration Files … "
# Reset LaunchDaemon
info "Reset LaunchDaemon … "
launchDaemonStatus
if [[ -n "${launchDaemonStatus}" ]]; then
logComment "Unload '${launchDaemonPath}' … "
launchctl bootout system "${launchDaemonPath}"
launchDaemonStatus
fi
logComment "Removing '${launchDaemonPath}' … "
rm -f "${launchDaemonPath}" 2>&1
logComment "Removed '${launchDaemonPath}'"
# Reset Script
info "Reset Script … "
logComment "Removing '${organizationDirectory}/${organizationScriptName}.zsh' … "
rm -f "${organizationDirectory}/${organizationScriptName}.zsh"
logComment "Removed '${organizationDirectory}/${organizationScriptName}.zsh' "
;;
"LaunchDaemon" )
info "Reset LaunchDaemon … "
launchDaemonStatus
if [[ -n "${launchDaemonStatus}" ]]; then
logComment "Unload '${launchDaemonPath}' … "
launchctl bootout system "${launchDaemonPath}"
launchDaemonStatus
fi
logComment "Removing '${launchDaemonPath}' … "
rm -f "${launchDaemonPath}" 2>&1
logComment "Removed '${launchDaemonPath}'"
;;
"Script" )
info "Reset Script … "
logComment "Removing '${organizationDirectory}/${organizationScriptName}.zsh' … "
rm -f "${organizationDirectory}/${organizationScriptName}.zsh"
logComment "Removed '${organizationDirectory}/${organizationScriptName}.zsh' "
;;
"Uninstall" )
warning "*** UNINSTALLING ${humanReadableScriptName} ***"
# Uninstall LaunchDaemon
info "Uninstall LaunchDaemon … "
launchDaemonStatus
if [[ -n "${launchDaemonStatus}" ]]; then
logComment "Unload '${launchDaemonPath}' … "
launchctl bootout system "${launchDaemonPath}"
launchDaemonStatus
fi
logComment "Removing '${launchDaemonPath}' … "
rm -f "${launchDaemonPath}" 2>&1
logComment "Removed '${launchDaemonPath}'"
# Uninstall Script
info "Uninstall Script … "
logComment "Removing '${organizationDirectory}/${organizationScriptName}.zsh' … "
rm -f "${organizationDirectory}/${organizationScriptName}.zsh"
logComment "Removed '${organizationDirectory}/${organizationScriptName}.zsh' "
# Remove legacy nested directory if it exists and is empty (pre-v1.3.0 cleanup)
if [[ -d "${organizationDirectory}/${reverseDomainNameNotation}" ]]; then
if [[ -z "$(ls -A "${organizationDirectory}/${reverseDomainNameNotation}")" ]]; then
logComment "Removing legacy nested directory: ${organizationDirectory}/${reverseDomainNameNotation}"
rmdir "${organizationDirectory}/${reverseDomainNameNotation}"
logComment "Removed legacy nested directory"
else
logComment "Legacy nested directory not empty; leaving intact: ${organizationDirectory}/${reverseDomainNameNotation}"
fi
fi
# Remove organization directory if empty
if [[ -d "${organizationDirectory}" ]]; then
if [[ -z "$(ls -A "${organizationDirectory}")" ]]; then
logComment "Removing empty organization directory: ${organizationDirectory}"
rmdir "${organizationDirectory}"
logComment "Removed empty organization directory"
else
logComment "Organization directory not empty; other management files may still exist — leaving intact: ${organizationDirectory}"
fi
fi
# Exit
logComment "Uninstalled all ${humanReadableScriptName} configuration files"
notice "Thanks for trying ${humanReadableScriptName}!"
exit 0
;;
* )
warning "None of the expected reset options was entered; don't reset anything"
;;
esac
}
function createDDMOSReminderScript() {
notice "Create '${humanReadableScriptName}' script: ${organizationDirectory}/${organizationScriptName}.zsh"
(
cat <<'ENDOFSCRIPT'
#!/bin/zsh --no-rcs
# shellcheck shell=bash
####################################################################################################
#
# Declarative Device Management macOS Reminder: End-user Message
#
# http://snelson.us/ddm
#
####################################################################################################
####################################################################################################
#
# Global Variables
#
####################################################################################################
export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local:/usr/local/bin
# Script Version
scriptVersion="2.0.0"
# Client-side Log
scriptLog="/var/log/org.churchofjesuschrist.log"
# Load is-at-least for version comparison
autoload -Uz is-at-least
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Organization Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Script Human-readable Name
humanReadableScriptName="DDM OS Reminder End-user Message"
# Organization's reverse domain (used for plist domains)
reverseDomainNameNotation="org.churchofjesuschrist"
# Organization's Script Name
organizationScriptName="dorm"
# Preference plist domains
preferenceDomain="${reverseDomainNameNotation}.${organizationScriptName}"
managedPreferencesPlist="/Library/Managed Preferences/${preferenceDomain}"
localPreferencesPlist="/Library/Preferences/${preferenceDomain}"
# Organization's number of days before deadline to starting displaying reminders
daysBeforeDeadlineDisplayReminder="14"
# Organization's number of days before deadline to enable swiftDialog's blurscreen
daysBeforeDeadlineBlurscreen="3"
# Organization's Meeting Delay (in minutes)
meetingDelay="75"
# Date format for deadlines (used with date -jf)
dateFormatDeadlineHumanReadable="+%a, %d-%b-%Y, %-l:%M %p"
# Swap main icon and overlay icon (set to YES, true, or 1 to enable)
swapOverlayAndLogo="NO"
####################################################################################################
#
# Functions
#
####################################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Client-side Logging
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function updateScriptLog() {
echo "${organizationScriptName} ($scriptVersion): $( date +%Y-%m-%d\ %H:%M:%S ) - ${1}" # | tee -a "${scriptLog}"
}
function preFlight() { updateScriptLog "[PRE-FLIGHT] ${1}"; }
function logComment() { updateScriptLog " ${1}"; }
function notice() { updateScriptLog "[NOTICE] ${1}"; }
function info() { updateScriptLog "[INFO] ${1}"; }
function errorOut() { updateScriptLog "[ERROR] ${1}"; }
function error() { updateScriptLog "[ERROR] ${1}"; let errorCount++; }
function warning() { updateScriptLog "[WARNING] ${1}"; let errorCount++; }
function fatal() { updateScriptLog "[FATAL ERROR] ${1}"; exit 1; }
function quitOut() { updateScriptLog "[QUIT] ${1}"; }
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Preference Helpers
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function setPreferenceValue() {
local targetVariable="${1}"
local managedValue="${2}"
local localValue="${3}"
local defaultValue="${4}"
local chosenValue=""
if [[ -n "${managedValue}" ]]; then
chosenValue="${managedValue}"
elif [[ -n "${localValue}" ]]; then
chosenValue="${localValue}"
else
chosenValue="${defaultValue}"
fi
printf -v "${targetVariable}" '%s' "${chosenValue}"
}
function setNumericPreferenceValue() {
local targetVariable="${1}"
local managedValue="${2}"
local localValue="${3}"
local defaultValue="${4}"
local candidate=""
if [[ -n "${managedValue}" && "${managedValue}" == <-> ]]; then
candidate="${managedValue}"
elif [[ -n "${localValue}" && "${localValue}" == <-> ]]; then
candidate="${localValue}"
else
candidate="${defaultValue}"
fi
printf -v "${targetVariable}" '%s' "${candidate}"
}
function replacePlaceholders() {
local targetVariable="${1}"
local value="${(P)targetVariable}"
# Handle both {placeholder} from plist and \{placeholder\} from inline defaults
value=${value//\{weekday\}/$( date +'%A' )}
value=${value//'{weekday}'/$( date +'%A' )}
value=${value//\{userfirstname\}/${loggedInUserFirstname}}
value=${value//'{userfirstname}'/${loggedInUserFirstname}}
value=${value//\{loggedInUserFirstname\}/${loggedInUserFirstname}}
value=${value//'{loggedInUserFirstname}'/${loggedInUserFirstname}}
value=${value//\{ddmVersionString\}/${ddmVersionString}}
value=${value//'{ddmVersionString}'/${ddmVersionString}}
value=${value//\{ddmEnforcedInstallDateHumanReadable\}/${ddmEnforcedInstallDateHumanReadable}}
value=${value//'{ddmEnforcedInstallDateHumanReadable}'/${ddmEnforcedInstallDateHumanReadable}}
value=${value//\{installedmacOSVersion\}/${installedmacOSVersion}}
value=${value//'{installedmacOSVersion}'/${installedmacOSVersion}}
value=${value//\{ddmVersionStringDeadlineHumanReadable\}/${ddmVersionStringDeadlineHumanReadable}}
value=${value//'{ddmVersionStringDeadlineHumanReadable}'/${ddmVersionStringDeadlineHumanReadable}}
value=${value//\{ddmVersionStringDaysRemaining\}/${ddmVersionStringDaysRemaining}}
value=${value//'{ddmVersionStringDaysRemaining}'/${ddmVersionStringDaysRemaining}}
value=${value//\{titleMessageUpdateOrUpgrade\}/${titleMessageUpdateOrUpgrade}}
value=${value//'{titleMessageUpdateOrUpgrade}'/${titleMessageUpdateOrUpgrade}}
value=${value//\{softwareUpdateButtonText\}/${softwareUpdateButtonText}}
value=${value//'{softwareUpdateButtonText}'/${softwareUpdateButtonText}}
value=${value//\{button1text\}/${button1text}}
value=${value//'{button1text}'/${button1text}}
value=${value//\{button2text\}/${button2text}}
value=${value//'{button2text}'/${button2text}}
value=${value//\{supportTeamName\}/${supportTeamName}}
value=${value//'{supportTeamName}'/${supportTeamName}}
value=${value//\{supportTeamPhone\}/${supportTeamPhone}}
value=${value//'{supportTeamPhone}'/${supportTeamPhone}}
value=${value//\{supportTeamEmail\}/${supportTeamEmail}}
value=${value//'{supportTeamEmail}'/${supportTeamEmail}}
value=${value//\{supportTeamWebsite\}/${supportTeamWebsite}}
value=${value//'{supportTeamWebsite}'/${supportTeamWebsite}}
value=${value//\{supportKBURL\}/${supportKBURL}}
value=${value//'{supportKBURL}'/${supportKBURL}}
value=${value//\{supportKB\}/${supportKB}}
value=${value//'{supportKB}'/${supportKB}}
value=${value//\{infobuttonaction\}/${infobuttonaction}}
value=${value//'{infobuttonaction}'/${infobuttonaction}}
value=${value//\{dialogVersion\}/$(/usr/local/bin/dialog -v 2>/dev/null)}
value=${value//'{dialogVersion}'/$(/usr/local/bin/dialog -v 2>/dev/null)}
value=${value//\{scriptVersion\}/${scriptVersion}}
value=${value//'{scriptVersion}'/${scriptVersion}}
printf -v "${targetVariable}" '%s' "${value}"
}
function applyHideRules() {
# Hide info button explicitly
if [[ "${infobuttontext}" == "hide" ]]; then
infobuttontext=""
fi
# Hide help image (QR) if requested
if [[ "${helpimage}" == "hide" ]]; then
helpimage=""
fi
}
function loadPreferenceOverrides() {
if [[ -f ${managedPreferencesPlist}.plist ]]; then
scriptLog_managed=$(defaults read "${managedPreferencesPlist}" ScriptLog 2> /dev/null)
daysBeforeDeadlineDisplayReminder_managed=$(defaults read "${managedPreferencesPlist}" DaysBeforeDeadlineDisplayReminder 2> /dev/null)
daysBeforeDeadlineBlurscreen_managed=$(defaults read "${managedPreferencesPlist}" DaysBeforeDeadlineBlurscreen 2> /dev/null)
meetingDelay_managed=$(defaults read "${managedPreferencesPlist}" MeetingDelay 2> /dev/null)
organizationOverlayiconURL_managed=$(defaults read "${managedPreferencesPlist}" OrganizationOverlayIconURL 2> /dev/null)
swapOverlayAndLogo_managed=$(defaults read "${managedPreferencesPlist}" SwapOverlayAndLogo 2> /dev/null)
dateFormatDeadlineHumanReadable_managed=$(defaults read "${managedPreferencesPlist}" DateFormatDeadlineHumanReadable 2> /dev/null)
supportTeamName_managed=$(defaults read "${managedPreferencesPlist}" SupportTeamName 2> /dev/null)
supportTeamPhone_managed=$(defaults read "${managedPreferencesPlist}" SupportTeamPhone 2> /dev/null)
supportTeamEmail_managed=$(defaults read "${managedPreferencesPlist}" SupportTeamEmail 2> /dev/null)
supportTeamWebsite_managed=$(defaults read "${managedPreferencesPlist}" SupportTeamWebsite 2> /dev/null)
supportKB_managed=$(defaults read "${managedPreferencesPlist}" SupportKB 2> /dev/null)
infobuttonaction_managed=$(defaults read "${managedPreferencesPlist}" InfoButtonAction 2> /dev/null)
supportKBURL_managed=$(defaults read "${managedPreferencesPlist}" SupportKBURL 2> /dev/null)
title_managed=$(defaults read "${managedPreferencesPlist}" Title 2> /dev/null)
button1text_managed=$(defaults read "${managedPreferencesPlist}" Button1Text 2> /dev/null)
button2text_managed=$(defaults read "${managedPreferencesPlist}" Button2Text 2> /dev/null)
message_managed=$(defaults read "${managedPreferencesPlist}" Message 2> /dev/null)
infobuttontext_managed=$(defaults read "${managedPreferencesPlist}" InfoButtonText 2> /dev/null)
infobox_managed=$(defaults read "${managedPreferencesPlist}" InfoBox 2> /dev/null)
helpmessage_managed=$(defaults read "${managedPreferencesPlist}" HelpMessage 2> /dev/null)
helpimage_managed=$(defaults read "${managedPreferencesPlist}" HelpImage 2> /dev/null)
fi
if [[ -f ${localPreferencesPlist}.plist ]]; then
scriptLog_local=$(defaults read "${localPreferencesPlist}" ScriptLog 2> /dev/null)
daysBeforeDeadlineDisplayReminder_local=$(defaults read "${localPreferencesPlist}" DaysBeforeDeadlineDisplayReminder 2> /dev/null)
daysBeforeDeadlineBlurscreen_local=$(defaults read "${localPreferencesPlist}" DaysBeforeDeadlineBlurscreen 2> /dev/null)
meetingDelay_local=$(defaults read "${localPreferencesPlist}" MeetingDelay 2> /dev/null)
organizationOverlayiconURL_local=$(defaults read "${localPreferencesPlist}" OrganizationOverlayIconURL 2> /dev/null)
swapOverlayAndLogo_local=$(defaults read "${localPreferencesPlist}" SwapOverlayAndLogo 2> /dev/null)
dateFormatDeadlineHumanReadable_local=$(defaults read "${localPreferencesPlist}" DateFormatDeadlineHumanReadable 2> /dev/null)
supportTeamName_local=$(defaults read "${localPreferencesPlist}" SupportTeamName 2> /dev/null)
supportTeamPhone_local=$(defaults read "${localPreferencesPlist}" SupportTeamPhone 2> /dev/null)
supportTeamEmail_local=$(defaults read "${localPreferencesPlist}" SupportTeamEmail 2> /dev/null)
supportTeamWebsite_local=$(defaults read "${localPreferencesPlist}" SupportTeamWebsite 2> /dev/null)
supportKB_local=$(defaults read "${localPreferencesPlist}" SupportKB 2> /dev/null)
infobuttonaction_local=$(defaults read "${localPreferencesPlist}" InfoButtonAction 2> /dev/null)
supportKBURL_local=$(defaults read "${localPreferencesPlist}" SupportKBURL 2> /dev/null)
title_local=$(defaults read "${localPreferencesPlist}" Title 2> /dev/null)
button1text_local=$(defaults read "${localPreferencesPlist}" Button1Text 2> /dev/null)
button2text_local=$(defaults read "${localPreferencesPlist}" Button2Text 2> /dev/null)
message_local=$(defaults read "${localPreferencesPlist}" Message 2> /dev/null)
infobuttontext_local=$(defaults read "${localPreferencesPlist}" InfoButtonText 2> /dev/null)
infobox_local=$(defaults read "${localPreferencesPlist}" InfoBox 2> /dev/null)
helpmessage_local=$(defaults read "${localPreferencesPlist}" HelpMessage 2> /dev/null)
helpimage_local=$(defaults read "${localPreferencesPlist}" HelpImage 2> /dev/null)
fi
setPreferenceValue "scriptLog" "${scriptLog_managed}" "${scriptLog_local}" "${scriptLog}"
setNumericPreferenceValue "daysBeforeDeadlineDisplayReminder" "${daysBeforeDeadlineDisplayReminder_managed}" "${daysBeforeDeadlineDisplayReminder_local}" "${daysBeforeDeadlineDisplayReminder}"
setNumericPreferenceValue "daysBeforeDeadlineBlurscreen" "${daysBeforeDeadlineBlurscreen_managed}" "${daysBeforeDeadlineBlurscreen_local}" "${daysBeforeDeadlineBlurscreen}"
setNumericPreferenceValue "meetingDelay" "${meetingDelay_managed}" "${meetingDelay_local}" "${meetingDelay}"
setPreferenceValue "swapOverlayAndLogo" "${swapOverlayAndLogo_managed}" "${swapOverlayAndLogo_local}" "${swapOverlayAndLogo}"
setPreferenceValue "dateFormatDeadlineHumanReadable" "${dateFormatDeadlineHumanReadable_managed}" "${dateFormatDeadlineHumanReadable_local}" "${dateFormatDeadlineHumanReadable}"
# Ensure date format starts with '+' as required by `date`
[[ "${dateFormatDeadlineHumanReadable}" != +* ]] && dateFormatDeadlineHumanReadable="+${dateFormatDeadlineHumanReadable}"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Current Logged-in User
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function currentLoggedInUser() {
loggedInUser=$( echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ { print $3 }' )
preFlight "Current Logged-in User: ${loggedInUser}"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Installed OS vs. DDM-enforced OS Comparison
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
installedOSvsDDMenforcedOS() {
# Installed macOS Version
installedmacOSVersion=$( sw_vers -productVersion )
notice "Installed macOS Version: $installedmacOSVersion"
# DDM-enforced macOS Version
ddmLogEntry=$( grep "EnforcedInstallDate" /var/log/install.log | tail -n 1 )
if [[ -z "$ddmLogEntry" ]]; then
versionComparisonResult="No DDM enforcement log entry found; please confirm this Mac is in-scope for DDM-enforced updates."
return
fi
# Parse enforced date and version
ddmEnforcedInstallDate="${${ddmLogEntry##*|EnforcedInstallDate:}%%|*}"
ddmVersionString="${${ddmLogEntry##*|VersionString:}%%|*}"
# DDM-enforced Deadline
ddmVersionStringDeadline="${ddmEnforcedInstallDate%%T*}"
deadlineEpoch=$( date -jf "%Y-%m-%dT%H:%M:%S" "$ddmEnforcedInstallDate" "+%s" 2>/dev/null )
ddmVersionStringDeadlineHumanReadable=$( date -jf "%Y-%m-%dT%H:%M:%S" "$ddmEnforcedInstallDate" "${dateFormatDeadlineHumanReadable}" 2>/dev/null )
# Fallback to default if format fails
if [[ -z "${ddmVersionStringDeadlineHumanReadable}" ]]; then
ddmVersionStringDeadlineHumanReadable=$( date -jf "%Y-%m-%dT%H:%M:%S" "$ddmEnforcedInstallDate" "+%a, %d-%b-%Y, %-l:%M %p" 2>/dev/null )
fi
ddmVersionStringDeadlineHumanReadable=${ddmVersionStringDeadlineHumanReadable// AM/ a.m.}
ddmVersionStringDeadlineHumanReadable=${ddmVersionStringDeadlineHumanReadable// PM/ p.m.}
# DDM-enforced Install Date
if (( deadlineEpoch <= $(date +%s) )); then
# Enforcement deadline passed
notice "DDM enforcement deadline has passed; evaluating post-deadline enforcement …"
# Read Apple's internal padded enforcement date from install.log
pastDueDeadline=$(grep "setPastDuePaddedEnforcementDate" /var/log/install.log | tail -n 1)
if [[ -n "$pastDueDeadline" ]]; then
paddedDateRaw="${pastDueDeadline#*setPastDuePaddedEnforcementDate is set: }"
paddedEpoch=$( date -jf "%a %b %d %H:%M:%S %Y" "$paddedDateRaw" "+%s" 2>/dev/null )
info "Found setPastDuePaddedEnforcementDate: ${paddedDateRaw:-Unparseable}"
if [[ -n "$paddedEpoch" ]]; then
ddmEnforcedInstallDateHumanReadable=$( date -jf "%s" "$paddedEpoch" "${dateFormatDeadlineHumanReadable}" 2>/dev/null )
if [[ -z "${ddmEnforcedInstallDateHumanReadable}" ]]; then
ddmEnforcedInstallDateHumanReadable=$( date -jf "%s" "$paddedEpoch" "+%a, %d-%b-%Y, %-l:%M %p" 2>/dev/null )
fi
info "Using ${ddmEnforcedInstallDateHumanReadable} for enforced install date"
else
warning "Unable to parse padded enforcement date from install.log"
ddmEnforcedInstallDateHumanReadable="Unavailable"
fi
else
warning "No setPastDuePaddedEnforcementDate found in install.log"
ddmEnforcedInstallDateHumanReadable="Unavailable"
fi
info "Effective enforcement source: setPastDuePaddedEnforcementDate"
else
# Deadline still in the future
ddmEnforcedInstallDateHumanReadable="$ddmVersionStringDeadlineHumanReadable"
fi
# Normalize AM/PM formatting
ddmEnforcedInstallDateHumanReadable=${ddmEnforcedInstallDateHumanReadable// AM/ a.m.}
ddmEnforcedInstallDateHumanReadable=${ddmEnforcedInstallDateHumanReadable// PM/ p.m.}
# Blurscreen logic (based on precise timestamp comparison)
nowEpoch=$(date +%s)
secondsUntilDeadline=$(( deadlineEpoch - nowEpoch ))
blurThresholdSeconds=$(( daysBeforeDeadlineBlurscreen * 86400 ))
ddmVersionStringDaysRemaining=$(( (secondsUntilDeadline + 43200) / 86400 )) # Round to nearest whole day
if (( secondsUntilDeadline <= blurThresholdSeconds )); then
blurscreen="--blurscreen"
else
blurscreen="--noblurscreen"
fi
# Version Comparison Result
if is-at-least "$ddmVersionString" "$installedmacOSVersion"; then
versionComparisonResult="Up-to-date"
info "DDM-enforced OS Version: $ddmVersionString"
else
versionComparisonResult="Update Required"
info "DDM-enforced OS Version: $ddmVersionString"
info "DDM-enforced OS Version Deadline: $ddmVersionStringDeadlineHumanReadable"
majorInstalled="${installedmacOSVersion%%.*}"
majorDDM="${ddmVersionString%%.*}"
if [[ "$majorInstalled" != "$majorDDM" ]]; then
titleMessageUpdateOrUpgrade="upgrade"
softwareUpdateButtonText="Upgrade Now"
else
titleMessageUpdateOrUpgrade="update"
softwareUpdateButtonText="Restart Now"
fi
fi
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Check User's Display Sleep Assertions (thanks, @techtrekkie!)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function checkUserDisplaySleepAssertions() {
notice "Check ${loggedInUser}'s Display Sleep Assertions"
local intervalSeconds=300 # Default: 300 seconds (i.e., 5 minutes)
local intervalMinutes=$(( intervalSeconds / 60 ))
local maxChecks=$(( meetingDelay * 60 / intervalSeconds ))
local checkCount=0
while (( checkCount < maxChecks )); do
local previousIFS="${IFS}"
IFS=$'\n'
local displayAssertionsArray
displayAssertionsArray=( $(pmset -g assertions | awk '/NoDisplaySleepAssertion | PreventUserIdleDisplaySleep/ && match($0,/\(.+\)/) && ! /coreaudiod/ {gsub(/^\ +/,"",$0); print};') )
if [[ -n "${displayAssertionsArray[*]}" ]]; then
userDisplaySleepAssertions="TRUE"
((checkCount++))
for displayAssertion in "${displayAssertionsArray[@]}"; do
info "Found the following Display Sleep Assertion(s): $(echo "${displayAssertion}" | awk -F ':' '{print $1;}')"
done
info "Check ${checkCount} of ${maxChecks}: Display Sleep Assertion still active; pausing reminder. (Will check again in ${intervalMinutes} minute(s).)"
IFS="${previousIFS}"
sleep "${intervalSeconds}"
else
userDisplaySleepAssertions="FALSE"
info "${loggedInUser}'s Display Sleep Assertion has ended after $(( checkCount * intervalMinutes )) minute(s)."
IFS="${previousIFS}"
return 0 # No active Display Sleep Assertions found
fi
done
if [[ "${userDisplaySleepAssertions}" == "TRUE" ]]; then
info "Presentation delay limit (${meetingDelay} min) reached after ${maxChecks} checks. Proceeding with reminder."
return 1 # Presentation still active after full delay
fi
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Update Required Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function updateRequiredVariables() {
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Organization's Branding Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Organization's Overlayicon URL
local defaultOverlayiconURL="${organizationOverlayiconURL:-"https://usw2.ics.services.jamfcloud.com/icon/hash_4804203ac36cbd7c83607487f4719bd4707f2e283500f54428153af17da082e2"}"
setPreferenceValue "organizationOverlayiconURL" "${organizationOverlayiconURL_managed}" "${organizationOverlayiconURL_local}" "${defaultOverlayiconURL}"
# Download the overlayicon from ${organizationOverlayiconURL}
if [[ -n "${organizationOverlayiconURL}" ]]; then
# notice "Downloading overlayicon from '${organizationOverlayiconURL}' …"
curl -o "/var/tmp/overlayicon.png" "${organizationOverlayiconURL}" --silent --show-error --fail
if [[ "$?" -ne 0 ]]; then
echo "Error: Failed to download the overlayicon from '${organizationOverlayiconURL}'."
overlayicon="/System/Library/CoreServices/Finder.app"
else
overlayicon="/var/tmp/overlayicon.png"
fi
else
overlayicon="/System/Library/CoreServices/Finder.app"
fi
# macOS Installer Icon URL
majorDDM="${ddmVersionString%%.*}"
case ${majorDDM} in
14) macOSIconURL="https://ics.services.jamfcloud.com/icon/hash_eecee9688d1bc0426083d427d80c9ad48fa118b71d8d4962061d4de8d45747e7" ;;
15) macOSIconURL="https://ics.services.jamfcloud.com/icon/hash_0968afcd54ff99edd98ec6d9a418a5ab0c851576b687756dc3004ec52bac704e" ;;
26) macOSIconURL="https://ics.services.jamfcloud.com/icon/hash_7320c100c9ca155dc388e143dbc05620907e2d17d6bf74a8fb6d6278ece2c2b4" ;;
*) macOSIconURL="https://ics.services.jamfcloud.com/icon/hash_4555d9dc8fecb4e2678faffa8bdcf43cba110e81950e07a4ce3695ec2d5579ee" ;;
esac
# Download the icon from ${macOSIconURL}
if [[ -n "${macOSIconURL}" ]]; then
# notice "Downloading icon from '${macOSIconURL}' …"
curl -o "/var/tmp/icon.png" "${macOSIconURL}" --silent --show-error --fail
if [[ "$?" -ne 0 ]]; then
error "Failed to download the icon from '${macOSIconURL}'."
icon="/System/Library/CoreServices/Finder.app"
else
icon="/var/tmp/icon.png"
fi
fi
if [[ "${swapOverlayAndLogo}" == "1" || "${swapOverlayAndLogo:l}" == "true" || "${swapOverlayAndLogo:l}" == "yes" ]]; then
tmp="$icon"
icon="$overlayicon"
overlayicon="$tmp"
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# swiftDialog Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# swiftDialog Binary Path
dialogBinary="/usr/local/bin/dialog"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# IT Support Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
local defaultSupportTeamName="${supportTeamName:-"IT Support"}"
setPreferenceValue "supportTeamName" "${supportTeamName_managed}" "${supportTeamName_local}" "${defaultSupportTeamName}"
local defaultSupportTeamPhone="${supportTeamPhone:-"+1 (801) 555-1212"}"
setPreferenceValue "supportTeamPhone" "${supportTeamPhone_managed}" "${supportTeamPhone_local}" "${defaultSupportTeamPhone}"
local defaultSupportTeamEmail="${supportTeamEmail:-"rescue@domain.org"}"
setPreferenceValue "supportTeamEmail" "${supportTeamEmail_managed}" "${supportTeamEmail_local}" "${defaultSupportTeamEmail}"
local defaultSupportTeamWebsite="${supportTeamWebsite:-"https://support.domain.org"}"
setPreferenceValue "supportTeamWebsite" "${supportTeamWebsite_managed}" "${supportTeamWebsite_local}" "${defaultSupportTeamWebsite}"
local defaultSupportKB="${supportKB:-"KB8675309"}"
setPreferenceValue "supportKB" "${supportKB_managed}" "${supportKB_local}" "${defaultSupportKB}"
local defaultInfobuttonaction="https://servicenow.domain.org/support?id=kb_article_view&sysparm_article=${supportKB}"
setPreferenceValue "infobuttonaction" "${infobuttonaction_managed}" "${infobuttonaction_local}" "${defaultInfobuttonaction}"
local defaultSupportKBURL="[${supportKB}](${infobuttonaction})"
setPreferenceValue "supportKBURL" "${supportKBURL_managed}" "${supportKBURL_local}" "${defaultSupportKBURL}"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Title, Message and Button Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
local defaultTitle="macOS ${titleMessageUpdateOrUpgrade} required"
setPreferenceValue "title" "${title_managed}" "${title_local}" "${defaultTitle}"
replacePlaceholders "title"
local defaultButton1text="${button1text:-"Open Software Update"}"
setPreferenceValue "button1text" "${button1text_managed}" "${button1text_local}" "${defaultButton1text}"
local defaultButton2text="${button2text:-"Remind Me Later"}"
setPreferenceValue "button2text" "${button2text_managed}" "${button2text_local}" "${defaultButton2text}"
local defaultInfobuttontext="${infobuttontext:-${supportKB}}"
setPreferenceValue "infobuttontext" "${infobuttontext_managed}" "${infobuttontext_local}" "${defaultInfobuttontext}"
local defaultAction="${action:-"x-apple.systempreferences:com.apple.preferences.softwareupdate"}"
printf -v "action" '%s' "${defaultAction}"
local defaultMessage="**A required macOS ${titleMessageUpdateOrUpgrade:l} is now available**<br>---<br>Happy $( date +'%A' ), ${loggedInUserFirstname}!<br><br>Please ${titleMessageUpdateOrUpgrade:l} to macOS **${ddmVersionString}** to ensure your Mac remains secure and compliant with organizational policies.<br><br>To perform the ${titleMessageUpdateOrUpgrade:l} now, click **${button1text}**, review the on-screen instructions, then click **${softwareUpdateButtonText}**.<br><br>If you are unable to perform this ${titleMessageUpdateOrUpgrade:l} now, click **${button2text}** to be reminded again later.<br><br>However, your device **will automatically restart and ${titleMessageUpdateOrUpgrade:l}** on **${ddmEnforcedInstallDateHumanReadable}** if you have not ${titleMessageUpdateOrUpgrade:l}d before the deadline.<br><br>For assistance, please contact **${supportTeamName}** by clicking the (?) button in the bottom, right-hand corner."
setPreferenceValue "message" "${message_managed}" "${message_local}" "${defaultMessage}"
replacePlaceholders "message"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Infobox Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
local defaultInfobox="**Current:** ${installedmacOSVersion}<br><br>**Required:** ${ddmVersionString}<br><br>**Deadline:** ${ddmVersionStringDeadlineHumanReadable}<br><br>**Day(s) Remaining:** ${ddmVersionStringDaysRemaining}"
setPreferenceValue "infobox" "${infobox_managed}" "${infobox_local}" "${defaultInfobox}"
replacePlaceholders "infobox"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Help Message Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
local defaultHelpmessage="For assistance, please contact: **${supportTeamName}**<br>- **Telephone:** ${supportTeamPhone}<br>- **Email:** ${supportTeamEmail}<br>- **Website:** ${supportTeamWebsite}<br>- **Knowledge Base Article:** ${supportKBURL}<br><br>**User Information:**<br>- **Full Name:** {userfullname}<br>- **User Name:** {username}<br><br>**Computer Information:**<br>- **Computer Name:** {computername}<br>- **Serial Number:** {serialnumber}<br>- **macOS:** {osversion}<br><br>**Script Information:**<br>- **Dialog:** $(/usr/local/bin/dialog -v)<br>- **Script:** ${scriptVersion}<br>"
setPreferenceValue "helpmessage" "${helpmessage_managed}" "${helpmessage_local}" "${defaultHelpmessage}"
replacePlaceholders "helpmessage"
local defaultHelpimage="qr=${infobuttonaction}"
setPreferenceValue "helpimage" "${helpimage_managed}" "${helpimage_local}" "${defaultHelpimage}"
replacePlaceholders "helpimage"
applyHideRules
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Display Reminder Dialog
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function displayReminderDialog() {
additionalDialogOptions=("$@")
notice "Display Reminder Dialog to ${loggedInUser} with additional options: ${additionalDialogOptions}"
dialogArgs=(
--title "${title}"
--message "${message}"
--icon "${icon}"
--iconsize 250
--overlayicon "${overlayicon}"
--infobox "${infobox}"
--button1text "${button1text}"
--button2text "${button2text}"
--messagefont "size=14"
--width 800
--height 600
"${blurscreen}"
"${additionalDialogOptions[@]}"
)
[[ -n "${infobuttontext}" ]] && dialogArgs+=(--infobuttontext "${infobuttontext}")
[[ -n "${helpmessage}" ]] && dialogArgs+=(--helpmessage "${helpmessage}")
[[ -n "${helpimage}" ]] && dialogArgs+=(--helpimage "${helpimage}")
${dialogBinary} "${dialogArgs[@]}"
returncode=$?
info "Return Code: ${returncode}"
case ${returncode} in
0) ## Process exit code 0 scenario here
notice "${loggedInUser} clicked ${button1text}"
if [[ "${action}" == *"systempreferences"* ]]; then
su - "$(stat -f%Su /dev/console)" -c "open '${action}'"
notice "Checking if System Settings is open …"
until osascript -e 'application "System Settings" is running' >/dev/null 2>&1; do
info "Pending System Settings launch …"
sleep 0.5
done
info "System Settings is open; Telling System Settings to make a guest appearance …"
su - "$(stat -f%Su /dev/console)" -c '
timeout=10
while ((timeout > 0)); do
if osascript -e "application \"System Settings\" is running" >/dev/null 2>&1; then
if osascript -e "tell application \"System Settings\" to activate" >/dev/null 2>&1; then
exit 0
fi
fi
sleep 0.5
((timeout--))
done
exit 1
'
else
su - "$(stat -f%Su /dev/console)" -c "open '${action}'"
fi
quitScript "0"
;;
2) ## Process exit code 2 scenario here
notice "${loggedInUser} clicked ${button2text}"
quitScript "0"
;;
3) ## Process exit code 3 scenario here
notice "${loggedInUser} clicked ${infobuttontext}"
info "Disabling blurscreen, hiding dialog and opening KB article: ${infobuttontext}"
echo "blurscreen: disable" >> /var/tmp/dialog.log
echo "hide:" >> /var/tmp/dialog.log
su \- "$(stat -f%Su /dev/console)" -c "open '${infobuttonaction}'"
info "Waiting 61 seconds before re-showing dialog …"
sleep 61
displayReminderDialog --ontop --moveable
;;
4) ## Process exit code 4 scenario here
notice "User allowed timer to expire"
quitScript "0"
;;
20) ## Process exit code 20 scenario here
notice "User had Do Not Disturb enabled"
quitScript "0"
;;
*) ## Catch all processing
notice "Something else happened; Exit code: ${returncode}"
quitScript "${returncode}"
;;
esac
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Quit Script (thanks, @bartreadon!)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function quitScript() {
quitOut "Exiting …"
# Remove overlay icon
if [[ -f "${icon}" ]] && [[ "${icon}" != "/System/Library/CoreServices/Finder.app" ]]; then
rm -f "${icon}"
fi
# Remove default dialog.log
rm -f /var/tmp/dialog.log
quitOut "Keep them movin' blades sharp!"
exit "${1}"
}
####################################################################################################
#
# Apply Preference Overrides
#
####################################################################################################
loadPreferenceOverrides
####################################################################################################
#
# Pre-flight Checks
#
####################################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Client-side Logging
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ ! -f "${scriptLog}" ]]; then
touch "${scriptLog}"
if [[ -f "${scriptLog}" ]]; then
preFlight "Created specified scriptLog: ${scriptLog}"
else
fatal "Unable to create specified scriptLog '${scriptLog}'; exiting.\n\n(Is this script running as 'root' ?)"
fi
else
# preFlight "Specified scriptLog '${scriptLog}' exists; writing log entries to it"
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Logging Preamble
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
preFlight "\n\n###\n# $humanReadableScriptName (${scriptVersion})\n# http://snelson.us/ddm\n####\n"
preFlight "Initiating …"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Confirm script is running as root
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ $(id -u) -ne 0 ]]; then
fatal "This script must be run as root; exiting."
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Validate Logged-in System Accounts
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
preFlight "Check for Logged-in System Accounts …"
currentLoggedInUser
counter="1"
until { [[ -n "${loggedInUser}" && "${loggedInUser}" != "loginwindow" ]] || [[ "${counter}" -gt "30" ]]; } ; do
preFlight "Logged-in User Counter: ${counter}"
currentLoggedInUser
sleep 2
((counter++))
done
loggedInUserFullname=$( id -F "${loggedInUser}" )
loggedInUserFirstname=$( echo "$loggedInUserFullname" | sed -E 's/^.*, // ; s/([^ ]*).*/\1/' | sed 's/\(.\{25\}\).*/\1…/' | awk '{print ( $0 == toupper($0) ? toupper(substr($0,1,1))substr(tolower($0),2) : toupper(substr($0,1,1))substr($0,2) )}' )
loggedInUserID=$( id -u "${loggedInUser}" )
preFlight "Current Logged-in User First Name (ID): ${loggedInUserFirstname} (${loggedInUserID})"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Complete
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
preFlight "Complete"
####################################################################################################
#
# Program
#
####################################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Installed OS vs. DDM-enforced OS Comparison
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
installedOSvsDDMenforcedOS
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# If Update Required, Display Dialog Window (respecting Display Reminder threshold)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ "${versionComparisonResult}" == "Update Required" ]]; then
# Skip notifications if we're outside the display reminder window (thanks for the suggestion, @kristian!)
if (( ddmVersionStringDaysRemaining > daysBeforeDeadlineDisplayReminder )); then
notice "Deadline still ${ddmVersionStringDaysRemaining} days away; skipping reminder until within ${daysBeforeDeadlineDisplayReminder}-day window."
quitScript "0"
else
notice "Within ${daysBeforeDeadlineDisplayReminder}-day reminder window; proceeding with reminder."
fi
# Confirm the currently logged-in user is "available" to be reminded
# If the deadline is more than 24 hours away, and the user has an active Display Assertion, exit the script
if [[ "${ddmVersionStringDaysRemaining}" -gt 1 ]]; then
if checkUserDisplaySleepAssertions; then
notice "No active Display Sleep Assertions detected; proceeding with reminder."
else
notice "Presentation still active after ${meetingDelay} minutes; exiting quietly."
quitScript "0"
fi
else
info "Deadline is within 24 hours; ignoring ${loggedInUser}'s Display Sleep Assertions."
fi
# Randomly pause script during its launch hours of 8 a.m. and 4 p.m.; Login pause of 30 to 90 seconds
currentHour=$(( $(date +%H) ))
currentMinute=$(( $(date +%M) ))
if (( currentHour == 8 || currentHour == 16 )) && (( currentMinute == 0 )); then
notice "Daily Trigger Pause: Random 0 to 20 minutes"
sleepSeconds=$(( RANDOM % 1200 ))
else
notice "Login Trigger Pause: Random 30 to 90 seconds"
sleepSeconds=$(( 30 + RANDOM % 61 ))
fi
if (( sleepSeconds >= 60 )); then
(( pauseMinutes = sleepSeconds / 60 ))
(( pauseSeconds = sleepSeconds % 60 ))
if (( pauseSeconds == 0 )); then
humanReadablePause="${pauseMinutes} minute(s)"
else
humanReadablePause="${pauseMinutes} minute(s), ${pauseSeconds} second(s)"
fi
else
humanReadablePause="${sleepSeconds} second(s)"
fi
info "Pausing for ${humanReadablePause} …"
sleep "${sleepSeconds}"
# Update Required Variables
updateRequiredVariables
# Display reminder dialog (with blurscreen, depending on proximity to deadline)
displayReminderDialog --ontop
else
info "Version Comparison Result: ${versionComparisonResult}"
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Exit
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
exit 0
ENDOFSCRIPT
) > "${organizationDirectory}/${organizationScriptName}.zsh"
logComment "${humanReadableScriptName} script created"
logComment "Setting permissions …"
chown root:wheel "${organizationDirectory}/${organizationScriptName}.zsh"
chmod 755 "${organizationDirectory}/${organizationScriptName}.zsh"
chmod +x "${organizationDirectory}/${organizationScriptName}.zsh"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# CREATE LAUNCHDAEMON
#
# The following function creates the LaunchDaemon which executes the previously created
# client-side DDM OS Reminder script.
#
# We've elected to prompt our users twice a day (8 a.m. and 4 p.m.) to ensure they see the message.
#
# NOTE: Leave a full return at the end of the content before the "ENDOFLAUNCHDAEMON" line.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function createLaunchDaemon() {
notice "Create LaunchDaemon"
logComment "Creating '${launchDaemonPath}' …"
(
cat <<ENDOFLAUNCHDAEMON
<?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>${launchDaemonLabel}</string>
<key>UserName</key>
<string>root</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>${organizationDirectory}/${organizationScriptName}.zsh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/bin:/bin:/usr/sbin:/sbin:/usr/local:/usr/local/bin</string>
</dict>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key>
<integer>8</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>16</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</array>
<key>StandardErrorPath</key>
<string>${scriptLog}</string>
<key>StandardOutPath</key>
<string>${scriptLog}</string>
</dict>
</plist>
ENDOFLAUNCHDAEMON
) > "${launchDaemonPath}"
logComment "Setting permissions for '${launchDaemonPath}' …"
chmod 644 "${launchDaemonPath}"
chown root:wheel "${launchDaemonPath}"
logComment "Loading '${launchDaemonLabel}' …"
launchctl bootstrap system "${launchDaemonPath}"
launchctl start "${launchDaemonPath}"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# LaunchDaemon Status
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function launchDaemonStatus() {
notice "LaunchDaemon Status"
launchDaemonStatus=$( launchctl list | grep "${launchDaemonLabel}" )
if [[ -n "${launchDaemonStatus}" ]]; then
logComment "${launchDaemonStatus}"
else
logComment "${launchDaemonLabel} is NOT loaded"
fi
}
####################################################################################################
#
# Pre-flight Checks
#
####################################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Client-side Logging
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ ! -f "${scriptLog}" ]]; then
touch "${scriptLog}"
if [[ -f "${scriptLog}" ]]; then
preFlight "Created specified scriptLog: ${scriptLog}"
else
fatal "Unable to create specified scriptLog '${scriptLog}'; exiting.
(Is this script running as 'root' ?)"
fi
else
# preFlight "Specified scriptLog '${scriptLog}' exists; writing log entries to it"
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Logging Preamble
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
preFlight "
###
# $humanReadableScriptName (${scriptVersion})
# http://snelson.us/ddm
#
# Reset Configuration: ${resetConfiguration}
###
"
preFlight "Initiating …"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Confirm script is running as root
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ $(id -u) -ne 0 ]]; then
fatal "This script must be run as root; exiting."
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Validate / install swiftDialog (Thanks big bunches, @acodega!)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function dialogInstall() {
# Get the URL of the latest PKG From the Dialog GitHub repo
dialogURL=$(curl -L --silent --fail "https://api.github.com/repos/swiftDialog/swiftDialog/releases/latest" | awk -F '"' "/browser_download_url/ && /pkg\"/ { print \$4; exit }")
# Expected Team ID of the downloaded PKG
expectedDialogTeamID="PWA5E9TQ59"
preFlight "Installing swiftDialog..."
# Create temporary working directory
workDirectory=$( basename "$0" )
tempDirectory=$( mktemp -d "/private/tmp/$workDirectory.XXXXXX" )
# Download the installer package
curl --location --silent "$dialogURL" -o "$tempDirectory/Dialog.pkg"
# Verify the download
teamID=$(spctl -a -vv -t install "$tempDirectory/Dialog.pkg" 2>&1 | awk '/origin=/ {print $NF }' | tr -d '()')
# Install the package if Team ID validates
if [[ "$expectedDialogTeamID" == "$teamID" ]]; then
installer -pkg "$tempDirectory/Dialog.pkg" -target /
sleep 2
dialogVersion=$( /usr/local/bin/dialog --version )
preFlight "swiftDialog version ${dialogVersion} installed; proceeding..."
else
# Display a so-called "simple" dialog if Team ID fails to validate
osascript -e 'display dialog "Please advise your Support Representative of the following error:
• Dialog Team ID verification failed
" with title "Mac Health Check Error" buttons {"Close"} with icon caution'
completionActionOption="Quit"
exitCode="1"
quitScript
fi
# Remove the temporary working directory when done
rm -Rf "$tempDirectory"
}
function dialogCheck() {
# Check for Dialog and install if not found
if [ ! -x "/Library/Application Support/Dialog/Dialog.app" ]; then
preFlight "swiftDialog not found. Installing..."
dialogInstall
else
dialogVersion=$(/usr/local/bin/dialog --version)
if [[ "${dialogVersion}" < "${swiftDialogMinimumRequiredVersion}" ]]; then
preFlight "swiftDialog version ${dialogVersion} found but swiftDialog ${swiftDialogMinimumRequiredVersion} or newer is required; updating..."
dialogInstall
else
preFlight "swiftDialog version ${dialogVersion} found; proceeding..."
fi
fi
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Complete
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
preFlight "Complete!"
####################################################################################################
#
# Program
#
####################################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Validate / install swiftDialog
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
dialogCheck
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Reset Configuration
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
resetConfiguration "${resetConfiguration}"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Script Validation / Creation
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
notice "Validating Script"
if [[ -f "${organizationDirectory}/${organizationScriptName}.zsh" ]]; then
logComment "${humanReadableScriptName} script '"${organizationDirectory}/${organizationScriptName}.zsh"' exists"
else
createDDMOSReminderScript
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# LaunchDaemon Validation / Creation
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
notice "Validating LaunchDaemon"
logComment "Checking for LaunchDaemon '${launchDaemonPath}' …"
if [[ -f "${launchDaemonPath}" ]]; then
logComment "LaunchDaemon '${launchDaemonPath}' exists"
launchDaemonStatus
if [[ -n "${launchDaemonStatus}" ]]; then
logComment "${launchDaemonLabel} IS loaded"
else
logComment "Loading '${launchDaemonLabel}' …"
launchctl asuser $(id -u) bootstrap gui/$(id -u) "${launchDaemonPath}"
launchDaemonStatus
fi
else
createLaunchDaemon
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Status Checks
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
notice "Status Checks"
logComment "I/O pause …"
sleep 1.3
launchDaemonStatus
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Exit
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
exit 0
- Monitor the client-side log file (using Control-C to break)
tail -f /var/log/org.churchofjesuschrist.log
- Kickstart the DDM OS Reminder LaunchDaemon (elevated privileges required)
launchctl kickstart -kp system/org.churchofjesuschrist.dor
- In your preferred code editor, modify the various
stringvalues in the sampleorg.churchofjesuschrist.dorm.plistto suit your organization, being careful to preserve the XML-escaped text:- Logging
ScriptLog
- Branding
OrganizationOverlayIconURL
- Support
SupportTeamNameSupportTeamPhoneSupportTeamEmailSupportTeamWebsiteSupportKBInfoButtonActionSupportKBURL
- Dialog
TitleButton1TextButton2Text(set tohideto supress displaying)InfoButtonText(set tohideto supress displaying)Message
- Infobox
InfoBox
- Help
HelpMessageHelpImage(set tohideto supress displaying)
- Logging
- Distribute an updated Configuration Profile with your customized
.plistto your test Mac
- Kickstart the DDM OS Reminder LaunchDaemon (elevated privileges required)
launchctl kickstart -kp system/org.churchofjesuschrist.dor
- Monitor the client-side log file (using Control-C to break)
tail -f /var/log/org.churchofjesuschrist.log
- Execute the script under
xtracewith a custom prompt; once the reminder dialog appears, the last several output blocks tend to be the most informative:
PS4='+%3l:%I → ' zsh -x /Library/Management/org.churchofjesuschrist/dor.zsh 2>&1
3. Advanced Deployment
An advanced deployment of DDM OS Reminder leverages a custom preference domain, which is identically referenced in all of your customized files
- Open Terminal and change to your Downloads directory
cd ~/Downloads
- Clone the DDM OS Reminder repository to your Downloads directory:
git clone https://github.com/dan-snelson/DDM-OS-Reminder.git
- Change to the DDM-OS-Reminder directory:
cd DDM-OS-Reminder
- Determine the current Reverse Domain Name Notation value (i.e.,
reverseDomainNameNotation)
git grep --no-recursive 'reverseDomainNameNotation='
- Modify the following command to search for the current Reverse Domain Name Notation value (i.e.,
currentRDNN) and replace all matches with your organization’s preferred value (i.e.,newRDNN)
git grep -z -l 'currentRDNN' | xargs -0 perl -pi -e 's/currentRDNN/newRDNN/g'
- Confirm the changes:
git grep -n 'newRDNN'
- Rename a copy of
org.churchofjesuschrist.dorm.plistto match your organization’s Reverse Domain Name Notation value (i.e.,newRDNN)
- In your preferred code editor, modify the various
stringvalues for your organization, being careful to preserve the XML-escaped text:- Logging
ScriptLog
- Branding
OrganizationOverlayIconURL
- Support
SupportTeamNameSupportTeamPhoneSupportTeamEmailSupportTeamWebsiteSupportKBInfoButtonActionSupportKBURL
- Dialog
TitleButton1TextButton2Text(set tohideto supress displaying)InfoButtonText(set tohideto supress displaying)Message
- Infobox
InfoBox
- Help
HelpMessageHelpImage(set tohideto supress displaying)
- Logging
- Distribute a Configuration Profile with your customized
.plistto your test Mac - Kickstart your DDM OS Reminder LaunchDaemon (elevated privileges required)
launchctl kickstart -kp system/com.company.dor
- Monitor your client-side log file
tail -f /var/log/com.company.log
- Execute your script under
xtracewith a custom prompt; once the reminder dialog appears, the last several output blocks tend to be the most informative:
PS4='+%3l:%I → ' zsh -x /Library/Management/com.company/dor.zsh 2>&1
3.1 Optional Advanced Deployment
The following optional steps will help you fine-tune your
reminderDialog.zshscript using familiar Markdown, which is then easily converted to XML-escaped strings by thecreatePlist.zshscript.Additionally, instructions are provided for customizing the LaunchDaemon to meet your organizational requirement.
- Edit
reminderDialog.zshas desired (which contains the logic and sample code to dynamically display the reminder dialog)- Search for the various
local defaultvariables and update for your organization (there are more than a dozen throughout various locations in the script)
- Search for the various
…
# Organization's Overlayicon URL
local defaultOverlayiconURL="${organizationOverlayiconURL:-"https://usw2.ics.services.jamfcloud.com/icon/hash_4804203ac36cbd7c83607487f4719bd4707f2e283500f54428153af17da082e2"}"
…
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# IT Support Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
local defaultSupportTeamName="${supportTeamName:-"IT Support"}"
setPreferenceValue "supportTeamName" "${supportTeamName_managed}" "${supportTeamName_local}" "${defaultSupportTeamName}"
local defaultSupportTeamPhone="${supportTeamPhone:-"+1 (801) 555-1212"}"
setPreferenceValue "supportTeamPhone" "${supportTeamPhone_managed}" "${supportTeamPhone_local}" "${defaultSupportTeamPhone}"
local defaultSupportTeamEmail="${supportTeamEmail:-"rescue@domain.org"}"
setPreferenceValue "supportTeamEmail" "${supportTeamEmail_managed}" "${supportTeamEmail_local}" "${defaultSupportTeamEmail}"
local defaultSupportTeamWebsite="${supportTeamWebsite:-"https://support.domain.org"}"
setPreferenceValue "supportTeamWebsite" "${supportTeamWebsite_managed}" "${supportTeamWebsite_local}" "${defaultSupportTeamWebsite}"
local defaultSupportKB="${supportKB:-"KB8675309"}"
setPreferenceValue "supportKB" "${supportKB_managed}" "${supportKB_local}" "${defaultSupportKB}"
local defaultInfobuttonaction="https://servicenow.domain.org/support?id=kb_article_view&sysparm_article=${supportKB}"
setPreferenceValue "infobuttonaction" "${infobuttonaction_managed}" "${infobuttonaction_local}" "${defaultInfobuttonaction}"
local defaultSupportKBURL="[${supportKB}](${infobuttonaction})"
setPreferenceValue "supportKBURL" "${supportKBURL_managed}" "${supportKBURL_local}" "${defaultSupportKBURL}"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Title, Message and Button Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
local defaultTitle="macOS ${titleMessageUpdateOrUpgrade} required"
setPreferenceValue "title" "${title_managed}" "${title_local}" "${defaultTitle}"
replacePlaceholders "title"
local defaultButton1text="${button1text:-"Open Software Update"}"
setPreferenceValue "button1text" "${button1text_managed}" "${button1text_local}" "${defaultButton1text}"
local defaultButton2text="${button2text:-"Remind Me Later"}"
setPreferenceValue "button2text" "${button2text_managed}" "${button2text_local}" "${defaultButton2text}"
local defaultInfobuttontext="${infobuttontext:-${supportKB}}"
setPreferenceValue "infobuttontext" "${infobuttontext_managed}" "${infobuttontext_local}" "${defaultInfobuttontext}"
local defaultAction="${action:-"x-apple.systempreferences:com.apple.preferences.softwareupdate"}"
printf -v "action" '%s' "${defaultAction}"
local defaultMessage="**A required macOS ${titleMessageUpdateOrUpgrade:l} is now available**<br>---<br>Happy $( date +'%A' ), ${loggedInUserFirstname}!<br><br>Please ${titleMessageUpdateOrUpgrade:l} to macOS **${ddmVersionString}** to ensure your Mac remains secure and compliant with organizational policies.<br><br>To perform the ${titleMessageUpdateOrUpgrade:l} now, click **${button1text}**, review the on-screen instructions, then click **${softwareUpdateButtonText}**.<br><br>If you are unable to perform this ${titleMessageUpdateOrUpgrade:l} now, click **${button2text}** to be reminded again later.<br><br>However, your device **will automatically restart and ${titleMessageUpdateOrUpgrade:l}** on **${ddmEnforcedInstallDateHumanReadable}** if you have not ${titleMessageUpdateOrUpgrade:l}d before the deadline.<br><br>For assistance, please contact **${supportTeamName}** by clicking the (?) button in the bottom, right-hand corner."
setPreferenceValue "message" "${message_managed}" "${message_local}" "${defaultMessage}"
replacePlaceholders "message"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Infobox Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
local defaultInfobox="**Current:** ${installedmacOSVersion}<br><br>**Required:** ${ddmVersionString}<br><br>**Deadline:** ${ddmVersionStringDeadlineHumanReadable}<br><br>**Day(s) Remaining:** ${ddmVersionStringDaysRemaining}"
setPreferenceValue "infobox" "${infobox_managed}" "${infobox_local}" "${defaultInfobox}"
replacePlaceholders "infobox"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Help Message Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
local defaultHelpmessage="For assistance, please contact: **${supportTeamName}**<br>- **Telephone:** ${supportTeamPhone}<br>- **Email:** ${supportTeamEmail}<br>- **Website:** ${supportTeamWebsite}<br>- **Knowledge Base Article:** ${supportKBURL}<br><br>**User Information:**<br>- **Full Name:** {userfullname}<br>- **User Name:** {username}<br><br>**Computer Information:**<br>- **Computer Name:** {computername}<br>- **Serial Number:** {serialnumber}<br>- **macOS:** {osversion}<br><br>**Script Information:**<br>- **Dialog:** $(/usr/local/bin/dialog -v)<br>- **Script:** ${scriptVersion}<br>"
setPreferenceValue "helpmessage" "${helpmessage_managed}" "${helpmessage_local}" "${defaultHelpmessage}"
replacePlaceholders "helpmessage"
local defaultHelpimage="qr=${infobuttonaction}"
setPreferenceValue "helpimage" "${helpimage_managed}" "${helpimage_local}" "${defaultHelpimage}"
replacePlaceholders "helpimage"
…
- Deploy your customized
reminderDialog.zshscript to a test Mac and confirm your edits work as intended - Execute the
zsh Resources/createPlist.zshscript to create a customized.plistand.mobileconfigbased on your changes toreminderDialog.zsh- Carefully validate the file you’ll be deploying
- Deploy your carefully validated
.plistor.mobileconfigto your test Mac - Edit
launchDaemonManagement.zshas desired (which writes your customizedreminderDialog.zshclient-side and creates your customized LaunchDaemon)- Modify
StartCalendarIntervalfor your organization; launchd.info is a great reference
- Modify
cat <<ENDOFLAUNCHDAEMON
<?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>${launchDaemonLabel}</string>
<key>UserName</key>
<string>root</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>${organizationDirectory}/${organizationScriptName}.zsh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/bin:/bin:/usr/sbin:/sbin:/usr/local:/usr/local/bin</string>
</dict>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key>
<integer>8</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<dict>
<key>Hour</key>
<integer>16</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</array>
<key>StandardErrorPath</key>
<string>${scriptLog}</string>
<key>StandardOutPath</key>
<string>${scriptLog}</string>
</dict>
</plist>
ENDOFLAUNCHDAEMON
- Execute
zsh assemble.zshto combine the two scripts into a MDM-deployableResources/ddm-os-reminder-assembled-<timestamp>.zsh.- The
zsh Resources/createSelfExtracting.zshscript creates a self-extracting script (which is easier to deploy for some MDMs)
- The
- Deploy the freshly generated
Resources/ddm-os-reminder-assembled-<timestamp>.zshto your test Mac
4. Upgrading
Author’s Highly Opinionated Thought: There are enough changes in version
2.0.0that it’s probably easier to just start-from-scratch — optionally uninstalling earlier versions — rather than attempting to update from a previous version of DDM OS Reminder.
The following is one method of using Visual Studio Code to upgrade your customized script to the latest version.
- Download the latest assembled version from the Resources directory on GitHub
- You may also wish to review the various branches for pre-release versions
- Make note of
scriptVersionin the freshly downloaded script - Using a backup of your current, known-working version, open both files in VS Code
- Click the Explorer button (Command-Shift-E)
- If necessary, enable “Open Editors” in Explorer’s options
- Individually right-click the files listed in the Open Editors to begin the comparison
- Select for Compare: The freshly downloaded script
- Compare with Selected: Your duplicated, renamed, customized script
- Close the Explorer
- Review each difference — shown in red — then click the right-facing arrow to replace the code in your duplicated, renamed, customized script with the code from the freshly downloaded script
- Notes:
- You can safely ignore any LaunchDaemon-related differences
- Enabling View > Word Wrap can interfere with code comparison
- Notes:
- Test
- Deploy
5. Resources
Resources in the GitHub repository:
Support
Community-supplied, best-effort support is available on the Mac Admins Slack (free, registration required) #ddm-os-reminders channel, or you can open an issue.
Pingback:DDM OS Reminder (1.2.0) - Dan K. Snelson
Pingback:DDM OS Reminder (1.4.0) - Dan K. Snelson