mirror of
https://github.com/acidanthera/OpenCorePkg.git
synced 2025-12-08 19:25:01 +00:00
453 lines
14 KiB
Bash
Executable File
453 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
#
|
|
# Copyright © 2019-2022 Rodion Shingarev, PMheart, vit9696, mikebeaton.
|
|
#
|
|
# Includes logic to install and run this script as one or more of
|
|
# agent, daemon or logout hook. Can also be run directly for testing,
|
|
# use CTRL+C for script termination.
|
|
#
|
|
# Currently installs just as launch daemon, which is the only one that
|
|
# on shutdown can access NVRAM after macOS installer vars are set.
|
|
#
|
|
# Examine log at /var/log/org.acidanthera.nvramhook.launchd/launchd.log.
|
|
#
|
|
|
|
if [ ! -x /usr/bin/dirname ] ||
|
|
[ ! -x /usr/bin/basename ] ||
|
|
[ ! -x /usr/bin/wc ] ||
|
|
[ ! -x /usr/sbin/diskutil ] ||
|
|
[ ! -x /usr/sbin/nvram ] ; then
|
|
abort "Unix environment is broken!"
|
|
fi
|
|
|
|
# Non-installed script can be run directly as 'agent' or 'daemon', i.e. script
|
|
# is started and runs as if launched that way by launchd, for debugging purposes.
|
|
#
|
|
# Agent is currently a null install, mainly since it seemed a shame to remove
|
|
# that code - i.e. it installs, and stops and starts correctly, but unlike
|
|
# daemon it does not actually do anything at stop and start. (Daemon is sudo
|
|
# so not everything that daemon does can be done by agent, also not everything
|
|
# that daemon does needs to be done, by agent.)
|
|
#
|
|
# Note that agent shutdown is to early to pick up NVRAM changes made by macOS
|
|
# installer before first restart, whereas daemon is not.
|
|
#
|
|
usage() {
|
|
echo "Usage: ${SELFNAME} [install|uninstall|status] [logout|agent|daemon]"
|
|
echo " - [install|uninstall|status] with no type uses recommended type (daemon)."
|
|
echo ""
|
|
}
|
|
|
|
doLog() {
|
|
if [ ! "${PREFIX}" = "" ] ; then
|
|
echo "$(date) (${PREFIX}) ${1}" >> "${LOG}"
|
|
else
|
|
echo "${1}" >> "${LOG}"
|
|
fi
|
|
}
|
|
|
|
abort() {
|
|
doLog "Fatal error: ${1}"
|
|
exit 1
|
|
}
|
|
|
|
NVRAM_DIR="NVRAM"
|
|
|
|
WRITE_HOOK_LOG=0
|
|
|
|
LOG=/dev/stdout
|
|
|
|
OCNVRAMGUID="4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102"
|
|
|
|
# OC generated NVRAM var
|
|
BOOT_PATH="${OCNVRAMGUID}:boot-path"
|
|
|
|
# temp storage name for this hook
|
|
BOOT_NODE="${OCNVRAMGUID}:boot-node"
|
|
|
|
# re-use as unique directory name for mount point when needed
|
|
UNIQUE_DIR="${BOOT_NODE}"
|
|
|
|
PRIVILEGED_HELPER_TOOLS="/Library/PrivilegedHelperTools"
|
|
|
|
ORG="org.acidanthera.nvramhook"
|
|
NVRAMDUMP="/Library/PrivilegedHelperTools/${ORG}.nvramdump.helper"
|
|
|
|
DAEMON_PLIST="/Library/LaunchDaemons/${ORG}.daemon.plist"
|
|
AGENT_PLIST="/Library/LaunchAgents/${ORG}.agent.plist"
|
|
|
|
HELPER="/Library/PrivilegedHelperTools/${ORG}.helper"
|
|
LOGDIR="/var/log/${ORG}.launchd"
|
|
LOGFILE="${LOGDIR}/launchd.log"
|
|
|
|
SELFDIR="$(/usr/bin/dirname "${0}")"
|
|
SELFNAME="$(/usr/bin/basename "${0}")"
|
|
|
|
USERID=$(id -u)
|
|
|
|
for arg;
|
|
do
|
|
case $arg in
|
|
install )
|
|
INSTALL=1
|
|
;;
|
|
uninstall )
|
|
UNINSTALL=1
|
|
;;
|
|
agent )
|
|
AGENT=1
|
|
PREFIX="Agent"
|
|
;;
|
|
daemon )
|
|
DAEMON=1
|
|
PREFIX="Daemon"
|
|
;;
|
|
logout )
|
|
LOGOUT=1
|
|
;;
|
|
status )
|
|
STATUS=1
|
|
;;
|
|
* )
|
|
usage
|
|
exit 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [ ! "$SELFNAME" = "Launchd.command" ] ; then
|
|
USE_NVRAMDUMP="${NVRAMDUMP}"
|
|
INSTALLED=1
|
|
else
|
|
cd "${SELFDIR}" || abort "Failed to enter working directory!"
|
|
|
|
if [ ! -x ./nvramdump ] ; then
|
|
abort "nvramdump is not found!"
|
|
fi
|
|
|
|
USE_NVRAMDUMP="./nvramdump"
|
|
INSTALLED=0
|
|
fi
|
|
|
|
# When installed runs as logout hook - i.e. dump immediately - unless specified otherwise.
|
|
if [ ! "$AGENT" = "1" ] &&
|
|
[ ! "$DAEMON" = "1" ] &&
|
|
[ ! "$LOGOUT" = "1" ] ; then
|
|
if [ "$INSTALL" = "1" ] ||
|
|
[ "$UNINSTALL" = "1" ] ; then
|
|
DAEMON=1
|
|
else
|
|
if [ "$INSTALLED" = "0" ] &&
|
|
[ ! "$STATUS" = "1" ] ; then
|
|
usage
|
|
exit 0
|
|
fi
|
|
LOGOUT=1
|
|
fi
|
|
fi
|
|
|
|
# Install one or more of agent, daemon and logout hook.
|
|
install() {
|
|
FAIL="Failed to install!"
|
|
|
|
if [ ! -d "${PRIVILEGED_HELPER_TOOLS}" ] ; then
|
|
sudo mkdir "${PRIVILEGED_HELPER_TOOLS}" || abort "${FAIL}"
|
|
fi
|
|
|
|
if [ "$LOGOUT" = "1" ] ; then
|
|
# logout hook from more permanent location if available
|
|
if [ "$AGENT" = "1" ] ||
|
|
[ "$DAEMON" = "1" ] ; then
|
|
HOOKPATH="${HELPER}"
|
|
else
|
|
HOOKPATH="$(pwd)/${SELFNAME}"
|
|
fi
|
|
sudo defaults write com.apple.loginwindow LogoutHook "${HOOKPATH}" || abort "${FAIL}"
|
|
fi
|
|
|
|
sudo cp "${SELFNAME}" "${HELPER}" || abort "${FAIL}"
|
|
sudo cp nvramdump "${NVRAMDUMP}" || abort "${FAIL}"
|
|
|
|
# customise Launchd.command.plist for agent
|
|
if [ "$AGENT" = "1" ] ; then
|
|
sed "s/\$LABEL/${ORG}.agent/g;s/\$HELPER/$(sed 's/\//\\\//g' <<< $HELPER)/g;s/\$PARAM/agent/g;s/\$LOGFILE/$(sed 's/\//\\\//g' <<< $LOGFILE)/g" "Launchd.command.plist" > "/tmp/Launchd.command.plist" || abort "${FAIL}"
|
|
sudo cp "/tmp/Launchd.command.plist" "${AGENT_PLIST}" || abort "${FAIL}"
|
|
rm -f /tmp/Launchd.command.plist
|
|
fi
|
|
|
|
# customise Launchd.command.plist for daemon
|
|
if [ "$DAEMON" = "1" ] ; then
|
|
sed "s/\$LABEL/${ORG}.daemon/g;s/\$HELPER/$(sed 's/\//\\\//g' <<< $HELPER)/g;s/\$PARAM/daemon/g;s/\$LOGFILE/$(sed 's/\//\\\//g' <<< $LOGFILE)/g" "Launchd.command.plist" > "/tmp/Launchd.command.plist" || abort "${FAIL}"
|
|
sudo cp "/tmp/Launchd.command.plist" "${DAEMON_PLIST}" || abort "${FAIL}"
|
|
rm -f /tmp/Launchd.command.plist
|
|
fi
|
|
|
|
if [ ! -d "${LOGDIR}" ] ; then
|
|
sudo mkdir "${LOGDIR}" || abort "${FAIL}"
|
|
fi
|
|
|
|
if [ ! -f "${LOGFILE}" ] ; then
|
|
sudo touch "${LOGFILE}" || abort "${FAIL}"
|
|
fi
|
|
|
|
if [ "$AGENT" = "1" ] ; then
|
|
# Allow agent to access log
|
|
sudo chmod 666 "${LOGFILE}" || abort "${FAIL}"
|
|
fi
|
|
|
|
if [ "$AGENT" = "1" ] ; then
|
|
# sudo for agent commands to get better logging of errors
|
|
sudo launchctl bootstrap "gui/${USERID}" "${AGENT_PLIST}" || abort "${FAIL}"
|
|
fi
|
|
|
|
if [ "$DAEMON" = "1" ] ; then
|
|
sudo launchctl load "${DAEMON_PLIST}" || abort "${FAIL}"
|
|
fi
|
|
|
|
echo "Installed."
|
|
}
|
|
|
|
uninstall() {
|
|
UNINSTALLED=1
|
|
|
|
if [ "$LOGOUT" = "1" ] ; then
|
|
sudo defaults delete com.apple.loginwindow LogoutHook || UNINSTALLED=0
|
|
fi
|
|
|
|
if [ "$AGENT" = "1" ] ; then
|
|
sudo launchctl bootout "gui/${USERID}" "${AGENT_PLIST}" || UNINSTALLED=0
|
|
sudo rm -f "${AGENT_PLIST}" || UNINSTALLED=0
|
|
fi
|
|
|
|
if [ "$DAEMON" = "1" ] ; then
|
|
# Special value in saved device node so that nvram.plist is not upadated at uninstall
|
|
sudo /usr/sbin/nvram "${BOOT_NODE}=null" || abort "Failed to save null boot device!"
|
|
sudo launchctl unload "${DAEMON_PLIST}" || UNINSTALLED=0
|
|
sudo rm -f "${DAEMON_PLIST}" || UNINSTALLED=0
|
|
fi
|
|
|
|
sudo rm -f "${HELPER}" || UNINSTALLED=0
|
|
sudo rm -f "${NVRAMDUMP}" || UNINSTALLED=0
|
|
|
|
if [ "$UNINSTALLED" = "1" ] ; then
|
|
echo "Uninstalled."
|
|
else
|
|
echo "Could not uninstall!"
|
|
fi
|
|
}
|
|
|
|
status() {
|
|
if [ ! "$AGENT" = "1" ] &&
|
|
[ ! "$DAEMON" = "1" ] ; then
|
|
# summary info
|
|
echo "Daemon pid = $(sudo launchctl print "system/${ORG}.daemon" 2>/dev/null | sed -n 's/.*pid = *//p')"
|
|
echo "Agent pid = $(launchctl print "gui/${USERID}/${ORG}.agent" 2>/dev/null | sed -n 's/.*pid = *//p')"
|
|
echo "LogoutHook = $(sudo defaults read com.apple.loginwindow LogoutHook 2>/dev/null)"
|
|
else
|
|
# detailed info on whatever is selected
|
|
if [ "$AGENT" = "1" ] ; then
|
|
launchctl print "gui/${USERID}/${ORG}.agent"
|
|
fi
|
|
|
|
if [ "$DAEMON" = "1" ] ; then
|
|
sudo launchctl print "system/${ORG}.daemon"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Save some diskutil info in emulated NVRAM for use at daemon shutdown:
|
|
# - While we can access diskutil normally at agent startup and at logout hook;
|
|
# - We cannot use diskutil at daemon shutdown, because:
|
|
# "Unable to run because unable to use the DiskManagement framework.
|
|
# Common reasons include, but are not limited to, the DiskArbitration
|
|
# framework being unavailable due to being booted in single-user mode."
|
|
# - At daemon startup, diskutil works but the device may not be ready
|
|
# immediately, but macOS restarts us quickly (~5s) and then we can run.
|
|
# Note that saving any info for use at process shutdown if not running as
|
|
# daemon (sudo) would have to go into e.g. a file not nvram.
|
|
saveMount() {
|
|
UUID="$(/usr/sbin/nvram "${BOOT_PATH}" | sed 's/.*GPT,\([^,]*\),.*/\1/')"
|
|
if [ "$(printf '%s' "${UUID}" | /usr/bin/wc -c)" -eq 36 ] && [ -z "$(echo "${UUID}" | sed 's/[-0-9A-F]//g')" ] ; then
|
|
node="$(/usr/sbin/diskutil info "${UUID}" | sed -n 's/.*Device Node: *//p')"
|
|
|
|
# This may randomly fail initially, if so the script gets restarted by
|
|
# launchd and eventually succeeds.
|
|
if [ "${node}" = "" ] ; then
|
|
abort "Cannot access device node!"
|
|
fi
|
|
|
|
doLog "Found boot device at ${node}"
|
|
|
|
if [ "${1}" = "1" ] ; then
|
|
# On earlier macOS (at least Mojave in VMWare) there is an intermittent
|
|
# problem where msdos/FAT kext is occasionally not available when we try
|
|
# to mount the drive on daemon exit; mounting and unmounting on daemon
|
|
# start here fixes this.
|
|
# Doing this introduces a different problem, since this early mount sometimes
|
|
# fails initially, however (as with `diskutil info` above) in that case the
|
|
# script gets restarted by launchd after ~5s, and eventually either succeeds or
|
|
# finds the drive already mounted (when applicable, i.e. not the ESP).
|
|
mount_path=$(mount | sed -n "s:${node} on \(.*\) (.*$:\1:p")
|
|
if [ ! "${mount_path}" = "" ] ; then
|
|
doLog "Early mount not needed, already mounted at ${mount_path}"
|
|
else
|
|
/usr/sbin/diskutil mount "${node}" 1>/dev/null || abort "Early mount failed!"
|
|
/usr/sbin/diskutil unmount "${node}" 1>/dev/null || abort "Early unmount failed!"
|
|
doLog "Early mount/unmount succeeded"
|
|
fi
|
|
|
|
# Use hopefully emulated NVRAM as temporary storage for the boot
|
|
# device node discovered with diskutil.
|
|
# If we are in emulated NVRAM, should not appear at next boot as
|
|
# nvramdump does not write values from OC GUID back to nvram.plist.
|
|
sudo /usr/sbin/nvram "${BOOT_NODE}=${node}" || abort "Failed to store boot device!"
|
|
fi
|
|
else
|
|
abort "Missing or invalid ${BOOT_PATH} value!"
|
|
fi
|
|
}
|
|
|
|
saveNvram() {
|
|
if [ "${1}" = "1" ] ; then
|
|
# . matches tab, note that \t for tab cannot be used in earlier macOS (e.g Mojave)
|
|
node=$(nvram "$BOOT_NODE" | sed -n "s/${BOOT_NODE}.//p")
|
|
if [ "$INSTALLED" = "0" ] ; then
|
|
# don't trash saved value if daemon is live
|
|
launchctl print "system/${ORG}.daemon" 2>/dev/null 1>/dev/null || sudo /usr/sbin/nvram -d "$BOOT_NODE"
|
|
else
|
|
sudo /usr/sbin/nvram -d "$BOOT_NODE"
|
|
fi
|
|
fi
|
|
|
|
if [ "${node}" = "" ] ; then
|
|
abort "Cannot access saved device node!"
|
|
elif [ "${node}" = "null" ] ; then
|
|
sudo /usr/sbin/nvram "${BOOT_NODE}=" || abort "Failed to remove boot node variable!"
|
|
doLog "Uninstalling…"
|
|
return
|
|
fi
|
|
|
|
mount_path=$(mount | sed -n "s:${node} on \(.*\) (.*$:\1:p")
|
|
if [ ! "${mount_path}" = "" ] ; then
|
|
doLog "Already mounted at ${mount_path}"
|
|
else
|
|
# use reasonably assumed unique path
|
|
mount_path="/Volumes/${UNIQUE_DIR}"
|
|
sudo mkdir "${mount_path}" || abort "Failed to make directory!"
|
|
sudo mount -t msdos "${node}" "${mount_path}" || abort "Failed to mount!"
|
|
doLog "Successfully mounted at ${mount_path}"
|
|
fi
|
|
|
|
if [ "${NVRAM_DIR}" = "" ] ; then
|
|
nvram_dir=$mount_path
|
|
else
|
|
nvram_dir="${mount_path}/${NVRAM_DIR}"
|
|
if [ ! -d "${nvram_dir}" ] ; then
|
|
sudo mkdir $nvram_dir || abort "Failed to make directory ${nvram_dir}"
|
|
fi
|
|
fi
|
|
|
|
rm -f /tmp/nvram.plist
|
|
${USE_NVRAMDUMP} || abort "failed to save nvram.plist!"
|
|
|
|
if [ -f "${nvram_dir}/nvram.plist" ] ; then
|
|
cp "${nvram_dir}/nvram.plist" "${nvram_dir}/nvram.fallback" || abort "Failed to create nvram.fallback!"
|
|
doLog "Copied nvram.fallback"
|
|
fi
|
|
|
|
cp /tmp/nvram.plist "${nvram_dir}/nvram.plist" || abort "Failed to copy nvram.plist!"
|
|
doLog "Saved nvram.plist"
|
|
|
|
rm -f /tmp/nvram.plist
|
|
|
|
if [ -f "${nvram_dir}/nvram.used" ] ; then
|
|
rm "${nvram_dir}/nvram.used" || abort "Failed to delete nvram.used!"
|
|
doLog "Deleted nvram.used"
|
|
fi
|
|
|
|
if [ "${WRITE_HOOK_LOG}" = "1" ] ; then
|
|
date >> "${nvram_dir}/${2}.hook.log" || abort "Failed to write to ${2}.hook.log!"
|
|
fi
|
|
|
|
# We would like to unmount here, but umount fails with "Resource busy"
|
|
# and diskutil is not available. This should not cause any problem except
|
|
# that the boot drive will be left mounted at the unique path if the
|
|
# daemon process gets killed (the process would then be restarted by macOS
|
|
# and NVRAM should still be saved at exit).
|
|
}
|
|
|
|
onComplete() {
|
|
doLog "Trap ${1}"
|
|
|
|
if [ "$DAEMON" = "1" ] ; then
|
|
saveNvram 1 "daemon"
|
|
fi
|
|
|
|
doLog "Ended."
|
|
|
|
# Needed if running directly (launchd kills any orphaned child processes by default).
|
|
# ShellCheck ref: https://github.com/koalaman/shellcheck/issues/648#issuecomment-208306771
|
|
# shellcheck disable=SC2046
|
|
kill $(jobs -p)
|
|
|
|
exit 0
|
|
}
|
|
|
|
if [ "$INSTALL" = "1" ] ; then
|
|
# Get root immediately (not required, looks nicer than doing a bunch of stuff and then asking)
|
|
sudo echo -n
|
|
|
|
# Save nvram immediately, will become the fallback after the first daemon shutdown.
|
|
# Do not install if this fails, since this indicates that required boot path from
|
|
# OC is not available, or other fatal error.
|
|
if [ "$DAEMON" = "1" ] ; then
|
|
doLog "Saving initial nvram.plist…"
|
|
saveMount 0
|
|
saveNvram 0 "daemon"
|
|
fi
|
|
|
|
install
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$UNINSTALL" = "1" ] ; then
|
|
uninstall
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$STATUS" = "1" ] ; then
|
|
status
|
|
exit 0
|
|
fi
|
|
|
|
if [ "${LOGOUT}" = "1" ] ; then
|
|
#LOG="${SELFDIR}/error.log"
|
|
saveMount 0
|
|
saveNvram 0 "logout"
|
|
exit 0
|
|
fi
|
|
|
|
# Useful for trapping all signals to see what we get.
|
|
#for s in {1..31} ;do trap "onComplete $s" $s ;done
|
|
|
|
# Trap CTRL+C for testing when running in immediate mode, and trap agent/daemon termination.
|
|
# Separate trap commands so we can log which was caught.
|
|
trap "onComplete SIGINT" SIGINT
|
|
trap "onComplete SIGTERM" SIGTERM
|
|
|
|
doLog "Starting…"
|
|
|
|
if [ "$DAEMON" = "1" ] ; then
|
|
saveMount 1
|
|
fi
|
|
|
|
while true
|
|
do
|
|
doLog "Running…"
|
|
|
|
# https://apple.stackexchange.com/a/126066/113758
|
|
# Only works from Yosemite upwards.
|
|
sleep $RANDOM & wait
|
|
done
|