Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 94 additions & 27 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -391,21 +391,48 @@ jobs:
run: |
(cd release && shasum -a 256 "${OUTPUT_NAME}" > "${OUTPUT_NAME}.sha256")

- name: Smoke test PKG
- name: Smoke test PKG install and replacement
shell: bash
env:
OUTPUT_NAME: agentdesktop-macos-${{ matrix.arch }}.pkg
run: |
wait_for_daemon() {
deadline=$((SECONDS + 30))
until sudo "/Applications/agentdesktop.app/Contents/MacOS/agentdesktop" status; do
if (( SECONDS >= deadline )); then
echo "Installed CLI could not reach the Agent Desktop daemon" >&2
return 1
fi
sleep 1
done
}

daemon_pid() {
sudo launchctl print system/dev.agentdesktop.daemon |
awk '$1 == "pid" && $2 == "=" { print $3; exit }'
}

sudo installer -pkg "release/${OUTPUT_NAME}" -target /
sudo launchctl print system/dev.agentdesktop.daemon > /dev/null
deadline=$((SECONDS + 30))
until sudo "/Applications/agentdesktop.app/Contents/MacOS/agentdesktop" status; do
if (( SECONDS >= deadline )); then
echo "Installed CLI could not reach the Agent Desktop daemon" >&2
exit 1
fi
sleep 1
done
wait_for_daemon

first_pid="$(daemon_pid)"
test -n "${first_pid}"
config_hash="$(sudo shasum -a 256 /etc/agentdesktop/config.yaml | awk '{ print $1 }')"
sudo touch /var/lib/agentdesktop/upgrade-smoke-marker

sudo installer -pkg "release/${OUTPUT_NAME}" -target /
sudo launchctl print system/dev.agentdesktop.daemon > /dev/null
wait_for_daemon

second_pid="$(daemon_pid)"
test -n "${second_pid}"
if [ "${first_pid}" = "${second_pid}" ]; then
echo "Agent Desktop daemon was not restarted during package replacement" >&2
exit 1
fi
test "${config_hash}" = "$(sudo shasum -a 256 /etc/agentdesktop/config.yaml | awk '{ print $1 }')"
sudo test -f /var/lib/agentdesktop/upgrade-smoke-marker

- name: Remove smoke-test installation
if: always()
Expand All @@ -415,6 +442,7 @@ jobs:
sudo rm -f /Library/LaunchDaemons/dev.agentdesktop.daemon.plist
sudo rm -rf "/Applications/agentdesktop.app"
sudo rm -rf /etc/agentdesktop /var/lib/agentdesktop /var/run/agentdesktop
sudo rm -f /var/run/agentdesktop-relaunch
sudo pkgutil --forget dev.agentdesktop.installer || true

- name: Upload unsigned PKG
Expand Down Expand Up @@ -503,33 +531,72 @@ jobs:
$hash = (Get-FileHash "release/$env:OUTPUT_NAME" -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash $env:OUTPUT_NAME" | Set-Content "release/$env:OUTPUT_NAME.sha256" -Encoding ascii

- name: Smoke test MSI
- name: Smoke test MSI install and replacement
shell: pwsh
env:
OUTPUT_NAME: agentdesktop-windows-${{ matrix.arch }}.msi
run: |
function Assert-MsiSuccess($process, $operation) {
if ($process.ExitCode -notin @(0, 3010)) {
throw "MSI $operation failed with exit code $($process.ExitCode)"
}
}

function Wait-ForAgentDesktopService {
$service = Get-Service AgentDesktop
$service.WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
$deadline = (Get-Date).AddSeconds(30)
do {
& "$env:ProgramFiles\Agent Desktop\agentdesktop.exe" status
if ($LASTEXITCODE -eq 0) {
return
}
Start-Sleep -Seconds 1
} while ((Get-Date) -lt $deadline)
throw "Installed CLI could not reach the Agent Desktop service"
}

$installer = (Resolve-Path "release/$env:OUTPUT_NAME").Path
$install = Start-Process msiexec.exe -ArgumentList @("/i", "`"$installer`"", "/qn", "/norestart") -Wait -PassThru
if ($install.ExitCode -ne 0) {
throw "MSI installation failed with exit code $($install.ExitCode)"
Assert-MsiSuccess $install "installation"
Wait-ForAgentDesktopService

$configPath = "$env:ProgramData\AgentDesktop\config.yaml"
$stateMarker = "$env:ProgramData\AgentDesktop\state\upgrade-smoke-marker"
$configHash = (Get-FileHash $configPath -Algorithm SHA256).Hash
New-Item -ItemType Directory -Force (Split-Path $stateMarker) | Out-Null
New-Item -ItemType File -Force $stateMarker | Out-Null

$tray = Start-Process "$env:ProgramFiles\Agent Desktop\agentdesktop.exe" -PassThru
Start-Sleep -Seconds 3
if ($tray.HasExited) {
throw "Agent Desktop tray process exited before replacement"
}
$service = Get-Service AgentDesktop
$service.WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
$deadline = (Get-Date).AddSeconds(30)
do {
& "$env:ProgramFiles\Agent Desktop\agentdesktop.exe" status
if ($LASTEXITCODE -eq 0) {
break
}
Start-Sleep -Seconds 1
} while ((Get-Date) -lt $deadline)
if ($LASTEXITCODE -ne 0) {
throw "Installed CLI could not reach the Agent Desktop service"

$replace = Start-Process msiexec.exe -ArgumentList @(
"/i",
"`"$installer`"",
"/qn",
"/norestart",
"REINSTALL=ALL",
"REINSTALLMODE=vomus"
) -Wait -PassThru
Assert-MsiSuccess $replace "replacement"
$tray.Refresh()
if (-not $tray.HasExited) {
throw "Agent Desktop tray process remains after replacement"
}
$uninstall = Start-Process msiexec.exe -ArgumentList @("/x", "`"$installer`"", "/qn", "/norestart") -Wait -PassThru
if ($uninstall.ExitCode -ne 0) {
throw "MSI uninstall failed with exit code $($uninstall.ExitCode)"

Wait-ForAgentDesktopService
if ((Get-FileHash $configPath -Algorithm SHA256).Hash -ne $configHash) {
throw "Agent Desktop configuration changed during replacement"
}
if (-not (Test-Path $stateMarker)) {
throw "Agent Desktop state was removed during replacement"
}

$uninstall = Start-Process msiexec.exe -ArgumentList @("/x", "`"$installer`"", "/qn", "/norestart") -Wait -PassThru
Assert-MsiSuccess $uninstall "uninstall"
if (Get-Service AgentDesktop -ErrorAction SilentlyContinue) {
throw "Agent Desktop service remains after uninstall"
}
Expand Down
39 changes: 38 additions & 1 deletion crates/agentdesktop/macos/scripts/postinstall
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ PLIST_PATH="/Library/LaunchDaemons/${LABEL}.plist"
CONFIG_DIRECTORY="/etc/agentdesktop"
CONFIG_PATH="${CONFIG_DIRECTORY}/config.yaml"
STATE_DIRECTORY="/var/lib/agentdesktop"
APPLICATION="/Applications/agentdesktop.app"
RELAUNCH_MARKER="/var/run/agentdesktop-relaunch"

add_user_to_group() {
username="$1"
Expand All @@ -29,6 +31,39 @@ remove_user_launch_agent() {
fi
}

relaunch_application_for_user() {
uid="$1"
case "${uid}" in
"" | *[!0-9]*)
echo "warning: invalid Agent Desktop relaunch user" >&2
return
;;
esac

username=$(/usr/bin/id -nu "${uid}" 2>/dev/null || true)
if [ -z "${username}" ] || ! /bin/launchctl print "gui/${uid}" >/dev/null 2>&1; then
echo "warning: cannot relaunch Agent Desktop because user ${uid} is no longer logged in" >&2
return
fi

if ! /bin/launchctl asuser "${uid}" /usr/bin/sudo -u "${username}" \
/usr/bin/open -g "${APPLICATION}"; then
echo "warning: could not relaunch Agent Desktop for user ${uid}" >&2
fi
}

relaunch_applications() {
if [ ! -f "${RELAUNCH_MARKER}" ]; then
return
fi

user_ids=$(/bin/cat "${RELAUNCH_MARKER}" 2>/dev/null || true)
/bin/rm -f "${RELAUNCH_MARKER}"
for uid in ${user_ids}; do
relaunch_application_for_user "${uid}"
done
}

if ! /usr/sbin/dseditgroup -q -o read -n . "${GROUP_NAME}" >/dev/null 2>&1; then
/usr/sbin/dseditgroup -q -o create -n . \
-r "Agent Desktop local API users" "${GROUP_NAME}"
Expand Down Expand Up @@ -70,4 +105,6 @@ fi
/bin/launchctl enable "system/${LABEL}"
/bin/launchctl kickstart -k "system/${LABEL}"

exit 0
relaunch_applications

exit 0
120 changes: 120 additions & 0 deletions crates/agentdesktop/macos/scripts/preinstall
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,130 @@
set -eu

LABEL="dev.agentdesktop.daemon"
USER_LAUNCH_AGENT_LABEL="dev.agentdesktop.daemon.user"
APPLICATION="/Applications/agentdesktop.app"
LEGACY_APPLICATION="/Applications/Agent Desktop.app"
RELAUNCH_MARKER="/var/run/agentdesktop-relaunch"

remove_user_launch_agent() {
username="$1"
uid=$(/usr/bin/id -u "${username}" 2>/dev/null || true)
if [ -n "${uid}" ]; then
/bin/launchctl bootout "gui/${uid}/${USER_LAUNCH_AGENT_LABEL}" >/dev/null 2>&1 || true
fi
home_directory=$(/usr/bin/dscl . -read "/Users/${username}" NFSHomeDirectory 2>/dev/null |
/usr/bin/cut -d ' ' -f 2- || true)
if [ -n "${home_directory}" ]; then
/bin/rm -f "${home_directory}/Library/LaunchAgents/${USER_LAUNCH_AGENT_LABEL}.plist"
fi
}

bundle_executable() {
application="$1"
if [ -d "${application}" ]; then
executable_name=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" \
"${application}/Contents/Info.plist" 2>/dev/null || true)
if [ -n "${executable_name}" ]; then
printf '%s/Contents/MacOS/%s\n' "${application}" "${executable_name}"
fi
fi
return 0
}

application_pids() {
executable="$1"
/bin/ps -axo pid=,command= | /usr/bin/awk -v executable="${executable}" '
{
pid = $1
$1 = ""
sub(/^[[:space:]]+/, "", $0)
if ($0 == executable || index($0, executable " -psn_") == 1) print pid
}'
}

application_user_ids() {
executable="$1"
for pid in $(application_pids "${executable}"); do
uid=$(/bin/ps -p "${pid}" -o uid= 2>/dev/null | /usr/bin/awk '{ print $1 }')
case "${uid}" in
"" | *[!0-9]*) ;;
*)
if [ "${uid}" -ge 500 ] && [ "${uid}" -lt 65534 ]; then
printf '%s\n' "${uid}"
fi
;;
esac
done
}

stop_application() {
executable="$1"
pids=$(application_pids "${executable}")
if [ -z "${pids}" ]; then
return
fi

for pid in ${pids}; do
/bin/kill -TERM "${pid}" >/dev/null 2>&1 || true
done

attempt=0
while [ "${attempt}" -lt 10 ]; do
if [ -z "$(application_pids "${executable}")" ]; then
return
fi
/bin/sleep 1
attempt=$((attempt + 1))
done

for pid in $(application_pids "${executable}"); do
/bin/kill -KILL "${pid}" >/dev/null 2>&1 || true
done
}

/bin/rm -f "${RELAUNCH_MARKER}"

console_user=$(/usr/bin/stat -f "%Su" /dev/console)
console_uid=""
case "${console_user}" in
"" | root | loginwindow | _mbsetupuser) ;;
*) console_uid=$(/usr/bin/id -u "${console_user}" 2>/dev/null || true) ;;
esac

relaunch_uids=$(
for application in "${APPLICATION}" "${LEGACY_APPLICATION}"; do
executable=$(bundle_executable "${application}")
if [ -n "${executable}" ]; then
application_user_ids "${executable}"
fi
done | /usr/bin/sort -un
)

if [ -n "${relaunch_uids}" ]; then
(
umask 077
printf '%s\n' "${relaunch_uids}" >"${RELAUNCH_MARKER}"
)
fi

/bin/launchctl bootout "system/${LABEL}" >/dev/null 2>&1 || true

/usr/bin/dscl . -list /Users UniqueID | /usr/bin/awk '$2 >= 500 && $2 < 65534 { print $1 }' |
while IFS= read -r username; do
remove_user_launch_agent "${username}"
done

if [ -n "${console_uid}" ]; then
remove_user_launch_agent "${console_user}"
fi

for application in "${APPLICATION}" "${LEGACY_APPLICATION}"; do
executable=$(bundle_executable "${application}")
if [ -n "${executable}" ]; then
stop_application "${executable}"
fi
done

if [ -d "${LEGACY_APPLICATION}" ]; then
legacy_identifier=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" \
"${LEGACY_APPLICATION}/Contents/Info.plist" 2>/dev/null || true)
Expand Down
14 changes: 13 additions & 1 deletion crates/agentdesktop/windows/installer.wxs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Wix
xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<Fragment>
<DirectoryRef Id="TARGETDIR">
<Directory Id="CommonAppDataFolder">
Expand All @@ -9,6 +11,16 @@
</Fragment>

<Fragment>
<util:CloseApplication
Id="CloseAgentDesktop"
Target="agentdesktop.exe"
Description="Agent Desktop must close before it can be updated."
CloseMessage="yes"
ElevatedCloseMessage="yes"
Timeout="15"
TerminateProcess="1"
RebootPrompt="no" />

<DirectoryRef Id="INSTALLDIR">
<Component Id="AgentDesktopService" Guid="66e82f72-41f7-42f6-9e49-ed43a589ec57" Win64="yes">
<File Id="AgentDesktopServiceExe" Source="$(env.TAURI_AGENTDESKTOP_SERVICE_EXE)" Name="agentdesktop-service.exe" KeyPath="yes" Checksum="yes" />
Expand Down
21 changes: 21 additions & 0 deletions frontend/desktop/MACOS_INSTALLER.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,27 @@ If the application is distributed without the PKG, launching it registers a
per-user LaunchAgent and starts the daemon in user mode. Installing the PKG
later removes that fallback before starting the privileged service.

## Upgrade behavior

The PKG owns process handling for in-place upgrades; MDM does not need a custom
process-kill script. Before replacing the application, `preinstall` stops the
system LaunchDaemon and any per-user fallback LaunchAgents, then asks a running
menu-bar application to terminate. If it does not exit within 10 seconds, the
installer terminates it so the bundle can be replaced safely.

After installing the new payload, `postinstall` starts the system LaunchDaemon.
If the menu-bar application was open before the upgrade and its user is still
logged in, the installer relaunches the new version in that same user session.
This also handles multiple active sessions during fast user switching. An
application that was closed before the upgrade remains closed.

Configuration under `/etc/agentdesktop` and state under `/var/lib/agentdesktop`
are preserved. A silent MDM upgrade can use the same command as installation:

```sh
sudo installer -pkg "Agent Desktop.pkg" -target /
```

## Configure through MDM

Deploy a root-owned configuration to `/etc/agentdesktop/config.yaml`. The
Expand Down
Loading
Loading