From 7d527ca23cb261ce2015e34e3146b9ae6b4d2caf Mon Sep 17 00:00:00 2001 From: Allen Houchins <32207388+allenhouchins@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:31:55 -0600 Subject: [PATCH] Add quit and relaunch logic to macOS FMAs (#37670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request enhances the macOS app installation process by improving how running applications are handled during install and update, and also updates the metadata and scripts for Docker Desktop. The main improvements are the introduction of quit/relaunch logic for pkg-based FMAs, and the renaming and updating of Docker Desktop’s identifiers and scripts. **App install/relaunch improvements:** * Added new shell functions `quit_and_track_application` and `relaunch_application` to the generated install scripts. These functions ensure that if an app (or pkg) is running before installation, it is quit and then automatically relaunched after installation, preserving user state. The logic tracks whether the app was running via an environment variable. [[1]](diffhunk://#diff-a9df2db484fcbb560d62c43f94c4bcc2d26dcf68066c9e7cc2bffad6f124ce97L22-R41) [[2]](diffhunk://#diff-a9df2db484fcbb560d62c43f94c4bcc2d26dcf68066c9e7cc2bffad6f124ce97R53-R59) [[3]](diffhunk://#diff-a9df2db484fcbb560d62c43f94c4bcc2d26dcf68066c9e7cc2bffad6f124ce97R72-R73) [[4]](diffhunk://#diff-a9df2db484fcbb560d62c43f94c4bcc2d26dcf68066c9e7cc2bffad6f124ce97R571-R648) * Removed the previous simpler `quit_application` logic from the install script generation, as the new functions supersede it. **Docker Desktop metadata and script updates:** * Renamed the Docker Desktop input and updated its `slug` and `unique_identifier` to match the new bundle identifier (`com.electron.dockerdesktop`), reflecting the current packaging. * Updated the output app metadata in `apps.json` to use the new slug and unique identifier for Docker Desktop. * Added a new output file for Docker Desktop (`docker-desktop/darwin.json`) with the updated install and uninstall scripts, including the new quit/relaunch logic and references. --- .../ingesters/homebrew/scripts.go | 110 ++++++++++++++++-- .../{docker.json => docker-desktop.json} | 4 +- ee/maintained-apps/outputs/apps.json | 4 +- .../outputs/docker-desktop/darwin.json | 21 ++++ .../SoftwarePage/components/icons/Docker.tsx | 12 +- .../app-icon-docker-desktop-60x60@2x.png | Bin 0 -> 6214 bytes 6 files changed, 131 insertions(+), 20 deletions(-) rename ee/maintained-apps/inputs/homebrew/{docker.json => docker-desktop.json} (60%) create mode 100644 ee/maintained-apps/outputs/docker-desktop/darwin.json create mode 100644 website/assets/images/app-icon-docker-desktop-60x60@2x.png diff --git a/ee/maintained-apps/ingesters/homebrew/scripts.go b/ee/maintained-apps/ingesters/homebrew/scripts.go index c59bd340ce..d0d142fa98 100644 --- a/ee/maintained-apps/ingesters/homebrew/scripts.go +++ b/ee/maintained-apps/ingesters/homebrew/scripts.go @@ -19,16 +19,26 @@ func installScriptForApp(app inputApp, cask *brewCask) (string, error) { sb.Extract(app.InstallerFormat) - var includeQuitFunc bool + // Add quit/relaunch functions if we have App or Pkg artifacts + var needsQuitRelaunch bool + for _, artifact := range cask.Artifacts { + if len(artifact.App) > 0 || len(artifact.Pkg) > 0 { + needsQuitRelaunch = true + break + } + } + + if needsQuitRelaunch { + sb.AddFunction("quit_and_track_application", quitAndTrackApplicationFunc) + sb.AddFunction("relaunch_application", relaunchApplicationFunc) + } + for _, artifact := range cask.Artifacts { switch { case len(artifact.App) > 0: sb.Write("# copy to the applications folder") - sb.Writef("quit_application '%s'", app.UniqueIdentifier) - if cask.Token == "docker" { - sb.Writef("quit_application 'com.electron.dockerdesktop'") - } - includeQuitFunc = true + // Quit the app before installing if it's running, and track state for relaunch + sb.Writef("quit_and_track_application '%s'", app.UniqueIdentifier) for _, appItem := range artifact.App { // Only process string values (skip objects with target, those are handled by custom scripts) if appItem.String == "" { @@ -40,9 +50,13 @@ func installScriptForApp(app inputApp, cask *brewCask) (string, error) { fi`, appPath) sb.Copy(appPath, "$APPDIR") } + // Relaunch the app if it was running before installation + sb.Writef("relaunch_application '%s'", app.UniqueIdentifier) case len(artifact.Pkg) > 0: sb.Write("# install pkg files") + // Quit the app before installing if it's running, and track state for relaunch + sb.Writef("quit_and_track_application '%s'", app.UniqueIdentifier) switch len(artifact.Pkg) { case 1: if err := sb.InstallPkg(artifact.Pkg[0].String); err != nil { @@ -55,6 +69,8 @@ fi`, appPath) default: return "", fmt.Errorf("application %s has unknown directive format for pkg", app.Token) } + // Relaunch the app if it was running before installation + sb.Writef("relaunch_application '%s'", app.UniqueIdentifier) case len(artifact.Binary) > 0: if len(artifact.Binary) == 2 { @@ -69,10 +85,6 @@ fi`, appPath) } } - if includeQuitFunc { - sb.AddFunction("quit_application", quitApplicationFunc) - } - return sb.String(), nil } @@ -556,6 +568,84 @@ const quitApplicationFunc = `quit_application() { } ` +// quitAndTrackApplicationFunc quits a running application and tracks whether it was running +// so it can be relaunched after installation. Sets APP_WAS_RUNNING_ environment variable. +const quitAndTrackApplicationFunc = `quit_and_track_application() { + local bundle_id="$1" + local var_name="APP_WAS_RUNNING_$(echo "$bundle_id" | tr '.-' '__')" + local timeout_duration=10 + + # check if the application is running + if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then + eval "export $var_name=0" + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." + eval "export $var_name=0" + return + fi + + # App was running, mark it for relaunch + eval "export $var_name=1" + echo "Application '$bundle_id' was running; will relaunch after installation." + + echo "Quitting application '$bundle_id'..." + + # try to quit the application within the timeout period + local quit_success=false + SECONDS=0 + while (( SECONDS < timeout_duration )); do + if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then + if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then + echo "Application '$bundle_id' quit successfully." + quit_success=true + break + fi + fi + sleep 1 + done + + if [[ "$quit_success" = false ]]; then + echo "Application '$bundle_id' did not quit." + fi +} +` + +// relaunchApplicationFunc relaunches an application if it was running before installation. +// Checks the APP_WAS_RUNNING_ environment variable set by quitAndTrackApplicationFunc. +const relaunchApplicationFunc = `relaunch_application() { + local bundle_id="$1" + local var_name="APP_WAS_RUNNING_$(echo "$bundle_id" | tr '.-' '__')" + local was_running + + # Check if the app was running before installation + eval "was_running=\$$var_name" + if [[ "$was_running" != "1" ]]; then + return + fi + + local console_user + console_user=$(stat -f "%Su" /dev/console) + if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then + echo "Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'." + return + fi + + echo "Relaunching application '$bundle_id'..." + + # Try to launch the application + if osascript -e "tell application id \"$bundle_id\" to activate" >/dev/null 2>&1; then + echo "Application '$bundle_id' relaunched successfully." + else + echo "Failed to relaunch application '$bundle_id'." + fi +} +` + const trashFunc = `trash() { local logged_in_user="$1" local target_file="$2" diff --git a/ee/maintained-apps/inputs/homebrew/docker.json b/ee/maintained-apps/inputs/homebrew/docker-desktop.json similarity index 60% rename from ee/maintained-apps/inputs/homebrew/docker.json rename to ee/maintained-apps/inputs/homebrew/docker-desktop.json index 6b10ff7a36..9dec9c50e3 100644 --- a/ee/maintained-apps/inputs/homebrew/docker.json +++ b/ee/maintained-apps/inputs/homebrew/docker-desktop.json @@ -1,7 +1,7 @@ { "name": "Docker Desktop", - "slug": "docker/darwin", - "unique_identifier": "com.docker.docker", + "slug": "docker-desktop/darwin", + "unique_identifier": "com.electron.dockerdesktop", "token": "docker-desktop", "installer_format": "dmg", "default_categories": ["Developer tools"] diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index 20cd27c859..84068beff7 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -570,9 +570,9 @@ }, { "name": "Docker Desktop", - "slug": "docker/darwin", + "slug": "docker-desktop/darwin", "platform": "darwin", - "unique_identifier": "com.docker.docker", + "unique_identifier": "com.electron.dockerdesktop", "description": "Docker Desktop provides a seamless environment for building, sharing, and running containerized applications and microservices." }, { diff --git a/ee/maintained-apps/outputs/docker-desktop/darwin.json b/ee/maintained-apps/outputs/docker-desktop/darwin.json new file mode 100644 index 0000000000..621776e2a2 --- /dev/null +++ b/ee/maintained-apps/outputs/docker-desktop/darwin.json @@ -0,0 +1,21 @@ +{ + "versions": [ + { + "version": "4.55.0", + "queries": { + "exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'com.electron.dockerdesktop';" + }, + "installer_url": "https://desktop.docker.com/mac/main/arm64/213807/Docker.dmg", + "install_script_ref": "2fd921da", + "uninstall_script_ref": "876e899c", + "sha256": "c1a9d2ecccc226ce10d4ec9bcd2defac574e2bbb2c2f42ae6a639e4a57c89ee5", + "default_categories": [ + "Developer tools" + ] + } + ], + "refs": { + "2fd921da": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath $INSTALLER_PATH)\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Try to launch the application\n if osascript -e \"tell application id \\\"$bundle_id\\\" to activate\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nhdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\"\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\"\n# copy to the applications folder\nquit_and_track_application 'com.electron.dockerdesktop'\nif [ -d \"$APPDIR/Docker.app\" ]; then\n\tsudo mv \"$APPDIR/Docker.app\" \"$TMPDIR/Docker.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Docker.app\" \"$APPDIR\"\nrelaunch_application 'com.electron.dockerdesktop'\nmkdir -p /usr/local/cli-plugins\n/bin/ln -h -f -s -- \"$APPDIR/Docker.app/Contents/Resources/cli-plugins/docker-compose\" \"/usr/local/cli-plugins/docker-compose\"\nmkdir -p /usr/local/bin\n/bin/ln -h -f -s -- \"$APPDIR/Docker.app/Contents/Resources/bin/hub-tool\" \"/usr/local/bin/hub-tool\"\n/bin/ln -h -f -s -- \"$APPDIR/Docker.app/Contents/Resources/bin/kubectl\" \"/usr/local/bin/kubectl.docker\"\n/bin/ln -h -f -s -- \"$APPDIR/Docker.app/Contents/Resources/bin/docker\" \"/usr/local/bin/docker\"\n/bin/ln -h -f -s -- \"$APPDIR/Docker.app/Contents/Resources/bin/docker-credential-desktop\" \"/usr/local/bin/docker-credential-desktop\"\n/bin/ln -h -f -s -- \"$APPDIR/Docker.app/Contents/Resources/bin/docker-credential-ecr-login\" \"/usr/local/bin/docker-credential-ecr-login\"\n/bin/ln -h -f -s -- \"$APPDIR/Docker.app/Contents/Resources/bin/docker-credential-osxkeychain\" \"/usr/local/bin/docker-credential-osxkeychain\"\n", + "876e899c": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.docker.helper'\nremove_launchctl_service 'com.docker.socket'\nremove_launchctl_service 'com.docker.vmnetd'\nquit_application 'com.docker.docker'\nquit_application 'com.electron.dockerdesktop'\nsudo rm -rf '/Library/PrivilegedHelperTools/com.docker.socket'\nsudo rm -rf '/Library/PrivilegedHelperTools/com.docker.vmnetd'\nsudo rmdir '~/.docker/bin'\nsudo rm -rf \"$APPDIR/Docker.app\"\nsudo rm -rf '/usr/local/cli-plugins/docker-compose'\nsudo rm -rf '/usr/local/bin/hub-tool'\nsudo rm -rf '/usr/local/bin/kubectl.docker'\nsudo rm -rf '/usr/local/bin/docker'\nsudo rm -rf '/usr/local/bin/docker-credential-desktop'\nsudo rm -rf '/usr/local/bin/docker-credential-ecr-login'\nsudo rm -rf '/usr/local/bin/docker-credential-osxkeychain'\nsudo rmdir '~/Library/Caches/com.plausiblelabs.crashreporter.data'\nsudo rmdir '~/Library/Caches/KSCrashReports'\ntrash $LOGGED_IN_USER '/usr/local/bin/docker-compose.backup'\ntrash $LOGGED_IN_USER '/usr/local/bin/docker.backup'\ntrash $LOGGED_IN_USER '~/.docker'\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/com.docker.helper'\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/group.com.docker'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.docker.helper.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.electron.dockerdesktop.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.bugsnag.Bugsnag/com.docker.docker'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Docker Desktop'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.docker.docker'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.plausiblelabs.crashreporter.data/com.docker.docker'\ntrash $LOGGED_IN_USER '~/Library/Caches/KSCrashReports/Docker'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.docker.docker'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.docker.helper'\ntrash $LOGGED_IN_USER '~/Library/Group Containers/group.com.docker'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.docker.docker'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.docker.docker.binarycookies'\ntrash $LOGGED_IN_USER '~/Library/Logs/Docker Desktop'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.docker.docker.plist'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.electron.docker-frontend.plist'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.electron.dockerdesktop.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.electron.docker-frontend.savedState'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.electron.dockerdesktop.savedState'\n" + } +} diff --git a/frontend/pages/SoftwarePage/components/icons/Docker.tsx b/frontend/pages/SoftwarePage/components/icons/Docker.tsx index 0510385110..411e0c9a20 100644 --- a/frontend/pages/SoftwarePage/components/icons/Docker.tsx +++ b/frontend/pages/SoftwarePage/components/icons/Docker.tsx @@ -1,13 +1,13 @@ -import React from "react"; +import * as React from "react"; import type { SVGProps } from "react"; const Docker = (props: SVGProps) => ( - - - + ); diff --git a/website/assets/images/app-icon-docker-desktop-60x60@2x.png b/website/assets/images/app-icon-docker-desktop-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..730eee9f55fc139959760386de23a0a307b47b61 GIT binary patch literal 6214 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