diff --git a/README.md b/README.md index b0ffd6c..e04b81b 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,56 @@ + # maclaunch -List your macOS startup items and their startup policy. -How does it work? -------------- -Lists plist files in LaunchAgents and LaunchDaemons folders. -When disabling an item, it moves it to .disabled so launchctl does not read them anymore. -It does **not** alter the contents in any way. It does not support JSON plists (for now). +Lists and controls your macOS startup items and their startup policy. + +Take back control of your macOS system! + +```shell +% maclaunch.sh list microsoft +> com.microsoft.update.agent + Type : user + User : hazcod + Launch: disabled + File : /Library/LaunchAgents/com.microsoft.update.agent.plist +> com.microsoft.teams.TeamsUpdaterDaemon + Type : system + User : root + Launch: disabled + File : /Library/LaunchDaemons/com.microsoft.teams.TeamsUpdaterDaemon.plist +> com.microsoft.office.licensingV2.helper + Type : system + User : root + Launch: disabled + File : /Library/LaunchDaemons/com.microsoft.office.licensingV2.helper.plist +> com.microsoft.autoupdate.helper + Type : system + User : root + Launch: disabled + File : /Library/LaunchDaemons/com.microsoft.autoupdate.helper.plist +``` + +## How does it work? + +Lists XML/json/binary plist files in LaunchAgents and LaunchDaemons folders which are loaded by launchctl. +When disabling an item, it uses launchctl to natively stop loading that service. +It does **not** alter the contents in any way or moves the file, so it should work with practically any service. + +The name you provide can either be specific to that service or function as a filter to work on multiple services simultaneously. + +## Installation + +Installation can be done straight from [my Homebrew tap](https://github.com/hazcod/homebrew-hazcod) via `brew install hazcod/homebrew-hazcod/maclaunch` or just copy `maclaunch.sh` to your filesystem. + +## Usage + +`Usage: maclaunch (filter|system)` + +To list all your services: `maclaunch list` -Installation -------------- -Installation can be done straight from [my Homebrew tap](https://github.com/hazcod/homebrew-hazcod): `brew install hazcod/homebrew-hazcod/maclaunch`. +To list all your services including system services: `maclaunch list system` -Usage -------------- +To list all microsoft services: `maclaunch list microsoft` -`Usage: maclaunch (item name|system)` +To enable plex player-helper: `maclaunch enable tv.plex.player-helper` -![Example output](https://i.imgur.com/VhHTJXJ.png) +To disable everything related to plex: `maclaunch disable plex` diff --git a/maclaunch.sh b/maclaunch.sh index d4a099c..c76c8f0 100755 --- a/maclaunch.sh +++ b/maclaunch.sh @@ -15,58 +15,53 @@ BOLD='\033[1m' function join_by { local IFS="$1"; shift; echo "$*"; } function usage { + # show command cli usage help echo "Usage: $0 (item name|system)" exit 1 } function error { + # show an error message and exit echo -e "${RED}ERROR:${N} ${1}${NC}" exit 1 } -function findStartupPath { - local name - local found - name="$1" - found="" - for path in "${startup_dirs[@]}"; do - if [ -f "${path}/${name}.plist" ] || [ -f "${path}/${name}.plist.disabled" ]; then - if [ -n "$found" ]; then - error "${name}.plist exists in multiple startup directories" - fi - found="${path}/${name}.plist" - break - fi - done - echo "$found" -} - function isSystem { + # if it's in /System, it's part of the (protected) system partition [[ $1 == /System/* ]] } function getScriptUser { local scriptPath="$1" + # if it's in LaunchAgents, it's ran as the user if echo "$scriptPath" | grep -q "LaunchAgent"; then whoami return fi + # if there is no UserName key, it's ran as root if ! grep -q 'UserName' "$scriptPath"; then echo "root" return fi - echo "custom" + # if UserName key is present, return the custom user + grep 'UserName' -C1 "$scriptPath" | tail -n1 | cut -d '>' -f 2 | cut -d '<' -f 1 } function listItems { + local filter="$2" + itemDirectories=("${startup_dirs[@]}") - # add system dirs if necessary - if [ "$2" == "system" ]; then + # get disabled services + disabled_services="$(launchctl print-disabled user/"$(id -u)")" + + # add system dirs too if we supplied the system parameter + if [ "$filter" == "system" ]; then itemDirectories=("${itemDirectories[@]}" "${system_dirs[@]}") + filter="" fi # login hooks @@ -78,125 +73,186 @@ function listItems { echo fi - # regular startup directories - for dir in "${itemDirectories[@]}"; do + # for every plist found + while IFS= read -r -d '' f; do - if [ ! -d "$dir" ]; then + # check if file is readable + if ! [ -r "$f" ]; then + echo -e "\nSkipping unreadable file: $f\n" continue fi + + # convert plist to XML if it is binary + if ! content=$(plutil -convert xml1 "${f}" -o -); then + error "Unparseable file: $f" + fi + + # detect the process type + type="system" ; [[ "$f" =~ .*LaunchAgents.* ]] && type="user" + + # extract the service name + startup_name="$(basename "$f" | sed -E 's/\.plist(\.disabled)*$//')" + + if [ -n "$filter" ] && [ "$filter" != "enabled" ] && [ "$filter" != "disabled" ]; then + if [[ "$startup_name" != *"$filter"* ]]; then + continue + fi + fi + + local load_items=() - while IFS= read -r -d '' f; do + # check for legacy behavior + if [[ $f =~ \.disabled$ ]]; then + # skip it if we only want enabled items + if [ -n "$filter" ] && [ "$filter" == "enabled" ]; then + continue + fi - # check if file is readable - if ! [ -r "$f" ]; then - echo -e "\nSkipping unreadable file: $f\n" + load_items=("${GREEN}${BOLD}disabled${NC}${YELLOW} (legacy)") + + # check if it's disabled natively via launchctl + elif echo "$disabled_services" | grep -iF "$startup_name" | grep -qi true; then + # skip it if we only want enabled items + if [ -n "$filter" ] && [ "$filter" == "enabled" ]; then continue fi - # convert plist to XML if it is binary - if ! content=$(plutil -convert xml1 "${f}" -o -); then - error "Unparseable file: $f" + load_items=("${GREEN}${BOLD}disabled") + + # if it's not disabled, list the startup triggers + else + # skip it if we only want disabled items + if [ -n "$filter" ] && [ "$filter" == "disabled" ]; then + continue fi - type="system" ; [[ "$f" =~ .*LaunchAgents.* ]] && type="user" - - startup_name=$(basename "$f" | sed -E 's/\.plist(\.disabled)*$//') - - local load_items=() - if [[ $f =~ \.disabled$ ]]; then - load_items=("${GREEN}${BOLD}disabled") - else - if echo "$content" | awk '/Disabled<\/key>/{ getline; if ($0 ~ //) { f = 1; exit } } END {exit(!f)}'; then - load_items+=("${GREEN}disabled") - else - if echo "$content" | grep -q 'OnDemand'; then - load_items+=("${GREEN}OnDemand") - fi - if echo "$content" | grep -q 'RunAtLoad'; then - load_items+=("${RED}OnStartup") - fi - if echo "$content" | grep -q 'KeepAlive'; then - load_items+=("${RED}Always") - fi - if echo "$content" | grep -q 'StartOnMount'; then - load_items+=("${YELLOW}OnFilesystemMount") - fi - if echo "$content" | grep -q 'StartInterval'; then - load_items+=("${RED}Periodically") - fi - if echo "$content" | grep -q "MachServices"; then - load_items+=("${RED}MachService") - fi - if echo "$content" | grep -q "WatchPaths"; then - load_items+=("${YELLOW}WatchPaths") - fi - fi + # --- + + if echo "$content" | grep -q 'OnDemand'; then + load_items+=("${GREEN}OnDemand") fi - if [ ${#load_items[@]} == 0 ]; then - load_str="${YELLOW}Unknown" - else - load_str=$(join_by ',' "${load_items[@]}") + if echo "$content" | grep -q 'RunAtLoad'; then + load_items+=("${RED}OnStartup") fi - if isSystem "$f"; then - startup_type=" (core)" + if echo "$content" | grep -q 'KeepAlive'; then + load_items+=("${RED}Always") fi - runAsUser="$(getScriptUser "$f")" - if [ "$runAsUser" = "root" ]; then - runAsUser="${RED}root${NC}" - elif [ "$runAsUser" = "custom" ]; then - runAsUser="${YELLOW}custom${NC}" + if echo "$content" | grep -q 'StartOnMount'; then + load_items+=("${YELLOW}OnFilesystemMount") fi - echo -e "${BOLD}> ${startup_name}${NC}${startup_type}" - echo " Type : ${type}" - echo -e " User : ${runAsUser}" - echo -e " Launch: ${load_str}${NC}" - echo " File : $f" + if echo "$content" | grep -q 'StartInterval'; then + load_items+=("${RED}Periodically") + fi - done < <(find "${dir}" -name '*.plist*' -type f -print0) - done -} + if echo "$content" | grep -q "MachServices"; then + load_items+=("${RED}MachService") + fi -function enableItem { - startupFile=$(findStartupPath "$1") - disabledFile="${startupFile}.disabled" + if echo "$content" | grep -q "WatchPaths"; then + load_items+=("${YELLOW}WatchPaths") + fi + fi - if [ ! -f "$disabledFile" ]; then - if [ -f "$startupFile" ]; then - error "This item is already enabled${NC}" + # if we did not detect anything, something weird happened + if [ ${#load_items[@]} == 0 ]; then + load_str="${YELLOW}Unknown" else - error "$1 does not exist" + load_str=$(join_by ',' "${load_items[@]}") fi - fi - if mv "$disabledFile" "$startupFile" && [ -f "$startupFile" ]; then - echo -e "${GREEN}Enabled ${STRONG}$1${NC}" - else - error "Could not enable ${STRONG}$1${NC}" - fi + # set the type to core if it's a system process (e.g. protected by SIP) + if isSystem "$f"; then + startup_type=" (core)" + fi + + # print what user this process is run as + runAsUser="$(getScriptUser "$f")" + if [ "$runAsUser" = "root" ]; then + runAsUser="${RED}root${NC}" + elif [ "$runAsUser" = "custom" ]; then + runAsUser="${YELLOW}custom${NC}" + fi + + echo -e "${BOLD}> ${startup_name}${NC}${startup_type}" + echo " Type : ${type}" + echo -e " User : ${runAsUser}" + echo -e " Launch: ${load_str}${NC}" + echo " File : $f" + + done< <(find "${itemDirectories[@]}" -type f -iname '*.plist*' -print0 2>/dev/null) } -function disableItem { - startupFile=$(findStartupPath "$1") - - if [ ! -f "$startupFile" ]; then - if [ -f "${startupFile}.disabled" ]; then - error "This item is already disabled${NC}" - else - error "$1 does not exist" +function enableItems { + disabled_items="$(launchctl print-disabled user/"$(id -u)")" + + while IFS= read -r -d '' startupFile; do + + # error out if we didn't find a plist + if [ -z "$startupFile" ]; then + error "Could not read $1" fi - fi - if mv "$startupFile" "${startupFile}.disabled" && [ -f "${startupFile}.disabled" ]; then - echo -e "${GREEN}Disabled ${STRONG}$1${NC}" - else - error "Could not disable ${STRONG}$1${NC}" - fi + # fix legacy .disabled behavior + startupFile="$(echo "$startupFile" | sed -E 's/(\.disabled)$//')" + disabledFile="${startupFile}.disabled" + if [ -f "$disabledFile" ] && ! mv "$disabledFile" "$startupFile"; then + error "could not move '$startupFile' to '$disabledFile'. Try to run with sudo?" + fi + + name="$(basename "$startupFile" | sed -E 's/\.plist(\.disabled)*$//')" + + # check if it's disabled + if ! echo "$disabled_items" | grep -iF "$name" | grep -q true; then + error "$name is already enabled" + fi + + # try to enable it + if ! launchctl enable user/"$(id -u)"/"${name}"; then + error "Could not enable ${STRONG}$name${NC}" + fi + + echo -e "${GREEN}Enabled ${STRONG}${name}${NC}" + + done< <(find "${startup_dirs[@]}" "${system_dirs[@]}" \( -iname "*$1*.plist" -o -iname "*$1*.plist.disabled" \) -print0 2>/dev/null) } +function disableItems { + disabled_items="$(launchctl print-disabled user/"$(id -u)")" + + while IFS= read -r -d '' startupFile; do + + # error out if we didn't find a plist + if [ -z "$startupFile" ]; then + error "Could not find plist for $1" + fi + + # fix legacy .disabled behavior + startupFile="$(echo "$startupFile" | sed -E 's/(\.disabled)$//')" + disabledFile="${startupFile}.disabled" + if [ -f "$disabledFile" ] && ! mv "$disabledFile" "$startupFile"; then + error "could not move '$startupFile' to '$disabledFile'. Try to run with sudo?" + fi + + name="$(basename "$startupFile" | sed -E 's/\.plist(\.disabled)*$//')" + + # check if it's enabled + if echo "$disabled_items" | grep -iF "$name" | grep -q true; then + error "$name is already disabled" + fi + + # try to disable it + if ! launchctl disable user/"$(id -u)"/"${name}"; then + error "Could not disable ${STRONG}$name${NC}" + fi + + echo -e "${GREEN}Disabled ${STRONG}${name}${NC}" + + done< <(find "${startup_dirs[@]}" "${system_dirs[@]}" \( -iname "*$1*.plist" -o -iname "*$1*.plist.disabled" \) -print0 2>/dev/null) +} if [ $# -lt 1 ] || [ $# -gt 2 ]; then usage @@ -205,7 +261,7 @@ fi case "$1" in "list") if [ $# -ne 1 ]; then - if [ $# -ne 2 ] || [ "$2" != "system" ]; then + if [ $# -ne 2 ]; then usage fi fi @@ -215,13 +271,13 @@ case "$1" in if [ $# -ne 2 ]; then usage fi - disableItem "$2" + disableItems "$2" ;; "enable") if [ $# -ne 2 ]; then usage fi - enableItem "$2" + enableItems "$2" ;; *) usage