Last active
June 9, 2024 05:19
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This makes a lot of assumption that may not be true for your environment.
installs
array.ManagedInstallDir
preference.