#!/bin/sh ########################################################################################## # # File: licensed_repair.sh # # Contains: This script forcibly repairs the installed LicenseD and Agent plists by # copying the reference copies from the same directory, fixing # ownership/permissions, and unloading & reloading them. # # Usage: licensed_repair.sh [logFilePath] # # If logFilePath is provided, all output is timestamped and appended to that file. # Otherwise, the script outputs to stdout/stderr (still timestamped). # # If a failure happens when trying to repair, we will alert the user, but only once # per boot *or* after 10 minutes have passed since the last failure, whichever comes # first. # # We also implement a minimal log rotation scheme if the log file grows larger than # a certain size. We keep a certain number of older logs around and delete the rest. # # Copyright: 2025 by PACE Anti-Piracy, Inc., all rights reserved. # ########################################################################################## #----------------------------------------------------------------------------------------- # Log rotation constants #----------------------------------------------------------------------------------------- LOG_MAX_SIZE=$((1024 * 1024 * 2)) # 2 MB default size limit LOG_MAX_FILES=10 # Keep up to 10 old log files DAEMON_PLIST_NAME="com.paceap.eden.licensed.plist" AGENT_PLIST_NAME="com.paceap.eden.licensed.agent.plist" DAEMON_INSTALL_PATH="/Library/LaunchDaemons/${DAEMON_PLIST_NAME}" AGENT_INSTALL_PATH="/Library/LaunchAgents/${AGENT_PLIST_NAME}" LICENSED_UNLOAD_SCRIPT="licensed_unload_v2.sh" LICENSED_LOAD_SCRIPT="licensed_load.sh" AGENT_UNLOAD_SCRIPT="/Library/Frameworks/PACEEdenExperience.framework/Versions/A/Resources/agent_unload.sh" AGENT_LOAD_SCRIPT="/Library/Frameworks/PACEEdenExperience.framework/Versions/A/Resources/agent_load.sh" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REFERENCE_DAEMON_PLIST="${SCRIPT_DIR}/${DAEMON_PLIST_NAME}" REFERENCE_AGENT_PLIST="${SCRIPT_DIR}/${AGENT_PLIST_NAME}" SENTINEL_FILE="/tmp/com.paceap.eden.licensed_repair_failed" SENTINEL_FILE_THRESHOLD=10 # Delete the sentinel once every 10 minutes #SENTINEL_FILE_THRESHOLD=1 # For testing purposes. Don't commit with this on. SEPARATOR="------------------------------------------------------------------------------------------" #----------------------------------------------------------------------------------------- # rotateLogIfNeeded # # If a log file is given, and its size exceeds LOG_MAX_SIZE, we rotate the logs, # keeping up to LOG_MAX_FILES historical logs. The newest rename is .1.log, then .2.log, # etc. #----------------------------------------------------------------------------------------- rotateLogIfNeeded() { if [ -n "$LOGFILE" ]; then if [ -f "$LOGFILE" ]; then local currentSize="$(wc -c < "$LOGFILE" 2>/dev/null)" if [ -n "$currentSize" ] && [ "$currentSize" -ge "$LOG_MAX_SIZE" ]; then # We'll assume .log is the file extension and insert .1, .2, etc. before .log. local baseName="${LOGFILE%.log}" # remove trailing ".log" local extName=".log" # Rotate older logs: e.g. LicenseD_repair.(LOG_MAX_FILES-1).log -> LicenseD_repair.LOG_MAX_FILES.log local i=$((LOG_MAX_FILES - 1)) while [ "$i" -ge 1 ]; do local prev="${baseName}.${i}${extName}" if [ -f "$prev" ]; then local next="${baseName}.$((i+1))${extName}" mv "$prev" "$next" 2>/dev/null fi i=$((i - 1)) done # Move the current log to .1 mv "$LOGFILE" "${baseName}.1${extName}" 2>/dev/null # Create a new empty log touch "$LOGFILE" fi fi fi } #----------------------------------------------------------------------------------------- # If a log file path was passed in, create the parent directory if needed, # then rotate if needed, then redirect output (append). #----------------------------------------------------------------------------------------- LOGFILE="$1" if [ -n "$LOGFILE" ]; then dirName="$(dirname "${LOGFILE}")" mkdir -p "${dirName}" 2>/dev/null rotateLogIfNeeded exec >> "$LOGFILE" 2>&1 fi #----------------------------------------------------------------------------------------- # logMsg MESSAGE # # Prints a timestamped MESSAGE to stdout (which may be redirected). # We do everything in Perl, generating UTC year-month-day hour:minute:second.microseconds. #----------------------------------------------------------------------------------------- logMsg() { local ts="$(perl -MTime::HiRes=gettimeofday -e ' my ($sec, $usec) = gettimeofday(); my @t = gmtime($sec); # @t = (sec, min, hour, mday, mon, year, wday, yday, isdst) printf "%04d-%02d-%02d %02d:%02d:%02d.%06d", $t[5] + 1900, # year $t[4] + 1, # month $t[3], # day $t[2], # hour $t[1], # minute $t[0], # second $usec; # microseconds ')" echo "${ts} $*" } #----------------------------------------------------------------------------------------- # removeOldSentinel # # Possibly remove sentinel if it is >= 10 minutes old, allowing a new alert. #----------------------------------------------------------------------------------------- removeOldSentinel() { if [ ! -f "$SENTINEL_FILE" ]; then return fi # If the sentinel is older than the threshold, remove it. find "$SENTINEL_FILE" -mmin +$SENTINEL_FILE_THRESHOLD -exec rm -f {} \; 2>/dev/null } #----------------------------------------------------------------------------------------- # fatalError MESSAGE # # Prints a timestamped error message, then exits with status 1. #----------------------------------------------------------------------------------------- fatalError() { logMsg "Error: $1" # Don't alert the user if a sentinel file still exists (meaning we failed under the # threshold number of minutes). if [ -f "$SENTINEL_FILE" ]; then logMsg "Repair was previously attempted and failed (less than $SENTINEL_FILE_THRESHOLD minutes ago)." logMsg "Failing without alerting the user." exit 1 fi # Attempt a GUI alert for the currently logged-in user: loggedInUser=$(stat -f %Su /dev/console) uidOfUser=$(id -u "$loggedInUser") # Construct AppleScript to display a dialog with our failure message /bin/launchctl asuser "$uidOfUser" /usr/bin/osascript -e \ 'tell application "System Events" to display dialog "There is a problem with your License Support installation that cannot be corrected automatically.\n\nPlease go to ilok.com, download the latest License Support software, then uninstall and reinstall it." with title "License Support Repair Failure" buttons {"OK"} default button 1 with icon 0' >/dev/null 2>&1 # Create the sentinel file to mark that a failure occurred. touch "$SENTINEL_FILE" 2>/dev/null # Make it world-writable and owned by group admin. # This ensures from a file-permissions perspective, any admin user can remove it, # but the sticky bit in /tmp means they'll generally need to 'sudo' anyway. chown root:admin "$SENTINEL_FILE" chmod 666 "$SENTINEL_FILE" exit 1 } #----------------------------------------------------------------------------------------- # runScriptOrFail SCRIPT_PATH # # Runs SCRIPT_PATH, capturing all output line by line, prepending a timestamp. # If SCRIPT_PATH fails (non-zero exit code), calls fatalError. We also check that # SCRIPT_PATH is executable first, or else we fail immediately. #----------------------------------------------------------------------------------------- runScriptOrFail() { script_path="$1" if [ ! -x "${script_path}" ]; then fatalError "Script not found or not executable: ${script_path}" fi tmp_exit_file="$(mktemp -t licensed_repair_exit.XXXXXX)" || fatalError "mktemp failed" { "${script_path}" echo "$?" > "${tmp_exit_file}" } 2>&1 | while IFS= read -r line do logMsg "$line" done rc="$(cat "${tmp_exit_file}" 2>/dev/null || echo 127)" rm -f "${tmp_exit_file}" 2>/dev/null if [ "$rc" -ne 0 ]; then fatalError "Sub-script failed with code $rc: ${script_path}" fi } #----------------------------------------------------------------------------------------- # copyFile SOURCE DEST # # Logs a message, then copies SOURCE to DEST. If copy fails, calls fatalError. #----------------------------------------------------------------------------------------- copyFile() { local source="$1" local dest="$2" logMsg "Copying ${source} to ${dest}" cp "${source}" "${dest}" 2>/dev/null || fatalError "Failed to copy ${source}" } #----------------------------------------------------------------------------------------- # setLaunchDPlistPermissions FILEPATH # # Logs a message, then sets ownership to root:wheel and permissions to 644 on FILEPATH. # If either command fails, calls fatalError. #----------------------------------------------------------------------------------------- setLaunchDPlistPermissions() { local fpath="$1" logMsg "Setting launchd plist ownership and permissions for ${fpath}" chown root:wheel "${fpath}" 2>/dev/null || fatalError "Failed to chown ${fpath}" chmod 644 "${fpath}" 2>/dev/null || fatalError "Failed to chmod ${fpath}" } #----------------------------------------------------------------------------------------- # Main script logic #----------------------------------------------------------------------------------------- logMsg "$SEPARATOR" logMsg "Forcibly repairing our License Support launchd plists..." # Possibly remove sentinel if it is older than the allowed threshold removeOldSentinel # Force a failure for testing. Don't commit with this enabled. #fatalError "Test" copyFile "${REFERENCE_DAEMON_PLIST}" "${DAEMON_INSTALL_PATH}" copyFile "${REFERENCE_AGENT_PLIST}" "${AGENT_INSTALL_PATH}" setLaunchDPlistPermissions "${DAEMON_INSTALL_PATH}" setLaunchDPlistPermissions "${AGENT_INSTALL_PATH}" # Unload and reload the Agent runScriptOrFail "${AGENT_UNLOAD_SCRIPT}" runScriptOrFail "${AGENT_LOAD_SCRIPT}" # Unload and reload LicenseD. It's critical that we do this last because at the time this # script is running, LicenseD is waiting for a signal to exit and restart. If we hit an # error running this script, we want LicenseD to stay blocked rather than the possibility # of it looping and failing the install repair over and over (launched persistently by # launchd). runScriptOrFail "${SCRIPT_DIR}/${LICENSED_UNLOAD_SCRIPT}" runScriptOrFail "${SCRIPT_DIR}/${LICENSED_LOAD_SCRIPT}" # If we had a previous error, delete any sentinel file becase we've successfully repaired. rm -f "$SENTINEL_FILE" logMsg "Repair complete."