Skip to content

Instantly share code, notes, and snippets.

@weswhet
Last active June 9, 2024 05:19
Show Gist options
  • Save weswhet/a75849c475c1f200348499f4b0b7b9f1 to your computer and use it in GitHub Desktop.
Save weswhet/a75849c475c1f200348499f4b0b7b9f1 to your computer and use it in GitHub Desktop.
quit, update, and reopen all apps that have an update available in Managed Software Center.
#!/usr/local/munki/munki-python
import argparse
import os
import pathlib
import plistlib
import pprint
import pwd
import subprocess
from time import sleep
CONSOLE_UID = os.stat("/dev/console").st_uid
CONSOLE_USER = pwd.getpwuid(os.stat("/dev/console")[4])[0]
def get_console_user() -> str:
"""Returns the current console user."""
return pwd.getpwuid(os.stat("/dev/console")[4])[0]
def get_munki_pending_update_paths(install_report: str) -> list[str]:
"""Returns a list of paths of pending updates in Munki."""
ret = []
# check if install report exits
if not os.path.exists(install_report):
print(f"File not found: {install_report}")
return ret
# read the plist file
plist = plistlib.loads(pathlib.Path(install_report).read_bytes())
# Checking if 'ItemsToInstall' key exists
if "ItemsToInstall" not in plist:
print("Found no pending updates...")
return ret
# Loop the list of dictionaries in 'ItemsToInstall'
for pending_updates in plist["ItemsToInstall"]:
# Checking if 'installs' key exists
display_name = pending_updates["display_name"]
if "installs" not in pending_updates:
print(f"Found no information for item: {display_name}...")
continue
# Loop the list of dictionaries in 'installs'
for install_info in pending_updates["installs"]:
# Checking if 'path' key exists
if "path" not in install_info:
print(f"Found no App path for item: {display_name}...")
continue
# Fetching the path
path = install_info["path"]
print(f"Found Pending Update for: {path}.")
ret.append(path)
return ret
def checkNSQuitAlwaysKeepsWindows() -> bool:
"""Checks the NSQuitAlwaysKeepsWindows setting."""
cmd = [
"/usr/bin/su",
"-l",
CONSOLE_USER,
"-c",
"/usr/bin/defaults read NSGlobalDomain NSQuitAlwaysKeepsWindows",
]
try:
output = (
subprocess.check_output(args=cmd, stderr=subprocess.DEVNULL)
.decode(encoding="utf-8")
.strip()
)
except subprocess.CalledProcessError as e:
print(f"Error: {e}")
return False
return output == "1"
def check_if_quit_always_keeps_windows_was_off() -> bool:
"""Sets the NSQuitAlwaysKeepsWindows setting."""
enabled = checkNSQuitAlwaysKeepsWindows()
if enabled:
print("NSQuitAlwaysKeepsWindows is already enabled.")
return False
setNSQuitAlwaysKeepsWindows(True)
print("Waiting for change to take effect...\n")
sleep(10)
return True
def setNSQuitAlwaysKeepsWindows(value: bool) -> None:
"""Sets the NSQuitAlwaysKeepsWindows setting."""
stringValue = str(value).lower()
cmd = [
"/usr/bin/su",
"-l",
CONSOLE_USER,
"-c",
f"/usr/bin/defaults write NSGlobalDomain\
NSQuitAlwaysKeepsWindows -bool {stringValue}",
]
subprocess.call(args=cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
return
def main() -> None:
parser = argparse.ArgumentParser(
description="mscupdate will quit, update, and reopen all apps that have an update available in Managed Software Center."
)
parser.add_argument(
"--force",
action="store_true",
help="Skip the prompt and force the updates. Use with caution.",
)
args = parser.parse_args()
if os.geteuid() != 0:
print('Please run this script with sudo. "sudo mscupdate"')
exit(code=1)
if not args.force:
print(
"This script will QUIT, update, and reopen all apps (INCLUDING iTERM2) that have an update available in Managed Software Center."
)
print("To skip this prompt, use the '--force' flag. 'sudo mscupdate --force'")
print("Are you sure you want to continue? (y/n)")
response = input()
if response.lower() != "y":
print("Exiting...")
exit(code=0)
print(
"Written and maintained by #corpeng-cpe-team. Issues or feedback contact us on Slack.\n"
)
print("Refreshing available updates in Managed Software Center...\n")
subprocess.call(
args=["/usr/local/munki/managedsoftwareupdate", "--checkonly"],
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT,
)
print("Check Complete...\n")
# plist where we can find the pending updates
munki_report = "/Library/Managed Installs/ManagedInstallReport.plist"
# get the paths of all pending update apps.
pending_updates = get_munki_pending_update_paths(install_report=munki_report)
if not pending_updates:
print("No pending updates found...")
exit(0)
print(
"Checking if NSQuitAlwaysKeepsWindows is enabled for apps to restore after updates..."
)
windowsStatus = check_if_quit_always_keeps_windows_was_off()
# close all pending update apps using pkill
for path in pending_updates:
# send SIGINT to the process this is the same as pressing cmd+q
# may take longer to close apps but it's a cleaner exit
if "iTerm" in path:
print("iTerm is open, we'll close it later...")
continue
cmd = ["/usr/bin/pkill", "-2", "-f", path]
print(f"Closing Application: {path}")
subprocess.call(args=cmd)
print("All pending update applications have been closed...")
cmd = ["/usr/local/munki/managedsoftwareupdate", "--installonly"]
print("Running Managed Software Center to update applications...\n")
subprocess.call(args=cmd)
user = get_console_user()
windowsStatus = False
for path in pending_updates:
# skip any installs that are not applications
if "Applications" not in path:
print(f"Skipping: {path}")
continue
if "iTerm" in path:
# kill iTerm in a separate process
print("Restarting iTerm...")
subprocess.call(args=["/usr/bin/killall", "-SIGKILL", "iTerm2"])
cmd = ["/usr/bin/su", "-l", user, "-c", f"/usr/bin/open '{path}'"]
print(f"Opening Application: {path}")
subprocess.call(args=cmd)
# check if we changed the NSQuitAlwaysKeepsWindows setting
if windowsStatus:
# set NSQuitAlwaysKeepsWindows back to false
print("Setting NSQuitAlwaysKeepsWindows back to false...")
setNSQuitAlwaysKeepsWindows(value=False)
print(
"Checking Managed Software Center for available updates to make sure we were successful..."
)
cmd = ["/usr/local/munki/managedsoftwareupdate", "--checkonly"]
subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
# Open the plist file
second_read = get_munki_pending_update_paths(install_report=munki_report)
if not second_read:
print("All applications have been updated. Have a good day!")
exit(code=0)
print("The below applications have not been updated...")
pprint.pprint(object=second_read)
if __name__ == "__main__":
main()
@weswhet
Copy link
Author

weswhet commented Mar 5, 2024

This makes a lot of assumption that may not be true for your environment.

  1. That all pending updates that are blocked have an installs array.
  2. You haven't changed the ManagedInstallDir preference.
  3. You will never have an OS upgrade as one of the pending update items. I can't say for certain what would happen if you run this with a pending OS upgrade.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment