diff --git a/.github/workflows/README.md b/.github/workflows/README.md index d96af3064..24449d15f 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -58,6 +58,20 @@ The workflow will display a notice if `run` was not built from the main branch. - The `Codesign` job uses a matrix to code-sign and repackage in parallel. It's important to ensure that individual matrix jobs do not upload the same artifacts; otherwise the result may be unexpected. For example: each platform downloads the `jetsocat` artifact, which contains builds for several operating systems. The Windows job must only sign and upload the Windows builds. If it were allowed to upload the macOS builds, they will be unsigned and *may* overwrite the signed builds uploaded by the macOS job. - The workflow makes a checkout of the repository at the commit that `run` was built from. This ensures consistency with the [CI](#ci) workflow when repackaging. Remember that tools invoked by the workflow (e.g. [tlk.ps1](../../ci/tlk.ps1)) will be from that commit too. +For macOS, make these GitHub Actions secrets available to the `publish-prod` environment: + +| Secret | Purpose | +| --- | --- | +| `APPLE_APP_DEV_ID_APP_CERTIFICATE` | Developer ID Application certificate | +| `APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD` | Application certificate export password | +| `APPLE_DEV_ID_INSTALLER_CERTIFICATE` | Developer ID Installer certificate for the Agent package | +| `APPLE_DEV_ID_INSTALLER_CERTIFICATE_PASSWORD` | Installer certificate export password | +| `APPLE_BOT_PASSWORD` | App-specific Apple ID password for notarization | + +Both certificates must be base64-encoded PKCS #12 bundles containing their private keys. +The Agent package requires a separate Developer ID Installer certificate. +Production packaging fails if the Installer certificate or notarization password is missing. + ##### TODO - `jetsocat` builds for macOS are signed, but should be notarized as well diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bf7d723c..9c112f7f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,8 +65,8 @@ jobs: $Jobs = @() - $Platforms | ForEach-Object { - $Runner = switch ($_) { 'windows' { 'windows-2022' } 'linux' { 'ubuntu-22.04' } } + $Platforms + 'macos' | ForEach-Object { + $Runner = switch ($_) { 'windows' { 'windows-2022' } 'macos' { 'macos-15' } 'linux' { 'ubuntu-22.04' } } foreach ($Arch in $Archs) { $Jobs += @{ arch = $Arch @@ -1136,6 +1136,17 @@ jobs: with: ref: ${{ needs.preflight.outputs.ref }} + # See the jetsocat job: the macos-15 image's ~/.cargo/bin shims are broken. + - name: Repair Rust shims (macOS) + if: ${{ matrix.os == 'macos' }} + run: | + set -eux + rm -rf "$HOME/.cargo/bin" + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | bash -s -- -y --default-toolchain none --no-modify-path --profile minimal + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + shell: bash + - name: Setup Rust cache uses: ./.github/actions/setup-rust-cache with: @@ -1280,6 +1291,10 @@ jobs: rustup target add aarch64-pc-windows-msvc shell: pwsh + - name: Configure macOS (intel) runner + if: ${{ matrix.os == 'macos' && matrix.arch == 'x64' }} + run: rustup target add x86_64-apple-darwin + - name: Add msbuild to PATH if: ${{ matrix.os == 'windows' }} uses: microsoft/setup-msbuild@v3 @@ -1376,6 +1391,11 @@ jobs: DAGENT_EXECUTABLE: ${{ steps.load-variables.outputs.dagent-executable }} TARGET_OUTPUT_PATH: ${{ steps.load-variables.outputs.target-output-path }} + # The macos-15 runner is arm64, so it can only install the arm64 package. + - name: Test macOS package installation + if: ${{ matrix.os == 'macos' && matrix.arch == 'arm64' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + run: ./package/AgentMacOS/Test-Package.sh "${{ steps.load-variables.outputs.target-output-path }}/devolutions-agent.pkg" + shell: bash - name: Clean up architecture-agnostic files before upload run: | diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index fe3d25946..354dc7f82 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -193,8 +193,6 @@ jobs: exclude: - project: devolutions-gateway os: macos - - project: devolutions-agent - os: macos steps: - name: Checkout ${{ github.repository }} @@ -238,11 +236,23 @@ jobs: security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH security import $DEVELOPER_ID_CERTIFICATE_PATH -P "$DEVELOPER_ID_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + + # Installer packages need a separate Developer ID Installer identity. + # UNTESTED: productbuild needs prompt-free access to the Installer key for CI; + # verify a signed package after changing the keychain setup. + if [ '${{ matrix.project }}' = 'devolutions-agent' ] && [ -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE" ]; then + DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH=$RUNNER_TEMP/dev_id_installer_cert.p12 + echo -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE" | base64 --decode --output $DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH + security import $DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH -P "$DEVELOPER_ID_INSTALLER_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + fi + security list-keychain -d user -s $KEYCHAIN_PATH shell: bash env: DEVELOPER_ID_CERTIFICATE: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE }} DEVELOPER_ID_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD }} + DEVELOPER_ID_INSTALLER_CERTIFICATE: ${{ secrets.APPLE_DEV_ID_INSTALLER_CERTIFICATE }} + DEVELOPER_ID_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEV_ID_INSTALLER_CERTIFICATE_PASSWORD }} - name: Configure runner (Windows) if: ${{ matrix.os == 'windows' }} @@ -333,15 +343,15 @@ jobs: run: | $IncludePattern = @(switch ('${{ matrix.project }}') { 'devolutions-gateway' { @('DevolutionsGateway.exe') } - 'devolutions-agent' { @('DevolutionsAgent.exe', 'DevolutionsAgentUpdater.exe', 'DevolutionsPedmShellExt.dll', 'DevolutionsPedmShellExt.msix', 'DevolutionsDesktopAgent.exe') } + 'devolutions-agent' { @('DevolutionsAgent.exe', 'DevolutionsAgentUpdater.exe', 'DevolutionsPedmShellExt.dll', 'DevolutionsPedmShellExt.msix', 'DevolutionsDesktopAgent.exe', 'devolutions-agent') } 'jetsocat' { @('jetsocat.exe', 'jetsocat') } }) $ExcludePattern = "*.pdb" - Get-ChildItem -Path ${{ runner.temp }} -Recurse -Include $IncludePattern -Exclude $ExcludePattern | % { + Get-ChildItem -Path $Env:RUNNER_TEMP -Recurse -File -Include $IncludePattern -Exclude $ExcludePattern | % { if ('${{ matrix.os }}' -Eq 'windows') { if ($_.Name -Eq 'DevolutionsPedmShellExt.msix') { $PackagePublisher = '${{ secrets.CODE_SIGNING_APPX_PUBLISHER }}' - $UnpackedMsix = Join-Path ${{ runner.temp }} "unpacked-context-menu-msix" + $UnpackedMsix = Join-Path $Env:RUNNER_TEMP "unpacked-context-menu-msix" $AppxManifest = Join-Path $UnpackedMsix "AppxManifest.xml" $PackedMsix = $_.FullName @@ -370,16 +380,10 @@ jobs: '-v') AzureSignTool @Params $_.FullName } elseif ('${{ matrix.os }}' -Eq 'macos') { - $SignCmd = $(@( - 'codesign', - '--timestamp', - '--options=runtime', - '-s', '"Developer ID Application: Devolutions inc. (N592S9ASDB)"', - '-v', - $_.FullName - )) -Join ' ' - Write-Host $SignCmd - Invoke-Expression $SignCmd + codesign --force --timestamp --options=runtime -s 'Developer ID Application: Devolutions inc. (N592S9ASDB)' -v $_.FullName + if ($LastExitCode -Ne 0) { + throw "failed to sign $($_.FullName)" + } } else { echo "::debug::nothing to do for ${{ matrix.os }}" } @@ -604,6 +608,71 @@ jobs: } shell: pwsh + - name: Repackage and notarize Agent (macOS) + if: ${{ matrix.project == 'devolutions-agent' && matrix.os == 'macos' }} + run: | + $PackageRoot = Join-Path $Env:RUNNER_TEMP devolutions-agent macos + $IsProduction = '${{ needs.preflight.outputs.package-env }}' -Eq 'publish-prod' + $CanSign = -Not [string]::IsNullOrEmpty($Env:DEVELOPER_ID_INSTALLER_CERTIFICATE) + $CanNotarize = $CanSign -And -Not [string]::IsNullOrEmpty($Env:APPLE_BOT_PASSWORD) + + if (-Not $CanNotarize) { + $Message = 'the Developer ID Installer certificate or the notarization credentials are missing; the Agent macOS package is not notarized' + if ($IsProduction) { throw $Message } + echo "::warning::$Message" + } + + if ($CanSign) { + $Env:DAGENT_PKG_SIGNING_IDENTITY = 'Developer ID Installer: Devolutions inc. (N592S9ASDB)' + } + + if ($CanNotarize) { + xcrun notarytool store-credentials 'notarytool-profile' --apple-id "$Env:APPLE_BOT_ID" --team-id "$Env:APPLE_BOT_TEAM_ID" --password "$Env:APPLE_BOT_PASSWORD" + if ($LastExitCode -Ne 0) { throw 'notarytool store-credentials failed' } + } + + foreach ($Arch in @('x64', 'arm64')) { + $ArchRoot = Join-Path $PackageRoot $Arch + $Env:DAGENT_EXECUTABLE = Join-Path $ArchRoot 'devolutions-agent' + $Env:TARGET_OUTPUT_PATH = $ArchRoot + + Write-Host "=== Repackage Agent ($Arch) ===" + ./ci/tlk.ps1 package -Product agent -Platform macos -Architecture $Arch + + if ($CanNotarize) { + $PackageFile = Join-Path $ArchRoot 'devolutions-agent.pkg' + xcrun notarytool submit $PackageFile --keychain-profile 'notarytool-profile' --wait + if ($LastExitCode -Ne 0) { throw "notarization failed for $PackageFile" } + xcrun stapler staple $PackageFile + if ($LastExitCode -Ne 0) { throw "stapling failed for $PackageFile" } + } + } + shell: pwsh + env: + DEVELOPER_ID_INSTALLER_CERTIFICATE: ${{ secrets.APPLE_DEV_ID_INSTALLER_CERTIFICATE }} + APPLE_BOT_PASSWORD: ${{ secrets.APPLE_BOT_PASSWORD }} + APPLE_BOT_ID: bot@devolutions.net + APPLE_BOT_TEAM_ID: N592S9ASDB + + - name: Verify macOS Agent signatures and notarization + if: ${{ matrix.project == 'devolutions-agent' && matrix.os == 'macos' && needs.preflight.outputs.package-env == 'publish-prod' }} + run: | + $PackageRoot = Join-Path $Env:RUNNER_TEMP devolutions-agent macos + foreach ($Arch in @('x64', 'arm64')) { + $Executable = Join-Path $PackageRoot $Arch 'devolutions-agent' + $Package = Join-Path $PackageRoot $Arch 'devolutions-agent.pkg' + + codesign --verify --strict --verbose=2 $Executable + if ($LastExitCode -Ne 0) { throw "failed to verify the signature of $Executable" } + + pkgutil --check-signature $Package + if ($LastExitCode -Ne 0) { throw "failed to verify the signature of $Package" } + + xcrun stapler validate $Package + if ($LastExitCode -Ne 0) { throw "failed to validate the notarization ticket of $Package" } + } + shell: pwsh + - name: Verification if: ${{ (matrix.os == 'windows' || matrix.os == 'macos') && env.package-env == 'publish-prod' }} run: | @@ -617,7 +686,7 @@ jobs: } } } elseif ('${{ matrix.os }}' -Eq 'macos') { - Get-ChildItem -Path $RootPath -Recurse -Include 'jetsocat' | % { + Get-ChildItem -Path $RootPath -Recurse -File -Include 'jetsocat' | % { codesign -dvvv "$($_.FullName)" if ($LastExitCode -Ne 0) { echo "::error::failed to verify the signature of $($_.FullName)" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef534a073..1f9a57224 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -370,6 +370,11 @@ jobs: Move-Item "$Dl/devolutions-agent/linux/${Arch}/devolutions-agent.changes" "$Release/devolutions-agent-${Version}-${Arch}.changes" } + # Agent macOS PKG + foreach ($Arch in @('x64', 'arm64')) { + Move-Item "$Dl/devolutions-agent/macos/${Arch}/devolutions-agent.pkg" "$Release/devolutions-agent-${Version}-${Arch}.pkg" + } + # Jetsocat archives: .tar.xz for Linux, .zip for Windows and macOS foreach ($P in @( @{ OS = "windows"; Arch = "x64"; Ext = ".exe"; Archive = "zip" }, @@ -658,6 +663,10 @@ jobs: Move-Item -Path "./devolutions-agent/linux/${Arch}/devolutions-agent.rpm" -Destination "$destinationFolder/devolutions-agent-${versionFull}-${Arch}.rpm" Move-Item -Path "./devolutions-agent/linux/${Arch}/devolutions-agent.changes" -Destination "$destinationFolder/devolutions-agent-${versionFull}-${Arch}.changes" } + + foreach ($Arch in @('x64', 'arm64')) { + Move-Item -Path "./devolutions-agent/macos/${Arch}/devolutions-agent.pkg" -Destination "$destinationFolder/devolutions-agent-${versionFull}-${Arch}.pkg" + } shell: pwsh - name: Attest build provenance diff --git a/ci/tlk.ps1 b/ci/tlk.ps1 index fda2c53fa..8a8768423 100755 --- a/ci/tlk.ps1 +++ b/ci/tlk.ps1 @@ -919,6 +919,89 @@ class TlkRecipe Pop-Location } + [void] Package_MacOS() { + if ($this.Product -Ne 'agent') { + throw "macOS packaging for $($this.Product) is not supported" + } + + if (Test-Path Env:DAGENT_EXECUTABLE) { + $Executable = $Env:DAGENT_EXECUTABLE + } else { + throw ("Specify DAGENT_EXECUTABLE environment variable") + } + + $HostArchitecture = switch ($this.Target.Architecture) { + "x64" { "x86_64" } + "arm64" { "arm64" } + default { throw "unsupported macOS architecture: $($this.Target.Architecture)" } + } + + $Identifier = "com.devolutions.agent" + $InputPackagePath = Join-Path $this.SourcePath "package/AgentMacOS" + $OutputPath = Join-Path $this.SourcePath "output/agent-macos-$($this.Target.Architecture)" + $PayloadPath = Join-Path $OutputPath "payload" + $ScriptsPath = Join-Path $OutputPath "scripts" + $BinPath = Join-Path $PayloadPath "usr/local/bin" + $LaunchDaemonsPath = Join-Path $PayloadPath "Library/LaunchDaemons" + + Remove-Item -Path $OutputPath -Recurse -Force -ErrorAction SilentlyContinue + @($BinPath, $LaunchDaemonsPath, $ScriptsPath) | % { + New-Item -Path $_ -ItemType 'Directory' -Force | Out-Null + } + + Copy-Item $Executable "$BinPath/devolutions-agent" + Copy-Item "$InputPackagePath/devolutions-agent-uninstall" "$BinPath/devolutions-agent-uninstall" + Copy-Item "$InputPackagePath/$Identifier.plist" "$LaunchDaemonsPath/$Identifier.plist" + Copy-Item "$InputPackagePath/scripts/*" $ScriptsPath + + & 'chmod' '755' "$BinPath/devolutions-agent" "$BinPath/devolutions-agent-uninstall" "$ScriptsPath/preinstall" "$ScriptsPath/postinstall" + & 'chmod' '644' "$LaunchDaemonsPath/$Identifier.plist" + + & 'plutil' '-lint' "$LaunchDaemonsPath/$Identifier.plist" | Out-Host + if ($LASTEXITCODE -Ne 0) { + throw "invalid launchd plist" + } + + $ComponentPackageName = "devolutions-agent-component.pkg" + + & 'pkgbuild' '--root' $PayloadPath ` + '--scripts' $ScriptsPath ` + '--identifier' $Identifier ` + '--version' $this.Version ` + '--install-location' '/' ` + '--ownership' 'recommended' ` + "$OutputPath/$ComponentPackageName" | Out-Host + if ($LASTEXITCODE -Ne 0) { + throw "pkgbuild failed" + } + + $DistributionFile = Join-Path $OutputPath "distribution.xml" + Merge-Tokens -TemplateFile "$InputPackagePath/distribution.xml" -Tokens @{ + host_arch = $HostArchitecture + version = $this.Version + component_pkg = $ComponentPackageName + } -OutputFile $DistributionFile + + $ProductBuildArgs = @('--distribution', $DistributionFile, '--package-path', $OutputPath) + + # The installer is signed here because a signature cannot be added to a built product archive. + if (Test-Path Env:DAGENT_PKG_SIGNING_IDENTITY) { + $ProductBuildArgs += @('--sign', $Env:DAGENT_PKG_SIGNING_IDENTITY, '--timestamp') + } + + $PackageFile = Join-Path $OutputPath "devolutions-agent.pkg" + & 'productbuild' @ProductBuildArgs $PackageFile | Out-Host + if ($LASTEXITCODE -Ne 0) { + throw "productbuild failed" + } + + if (Test-Path Env:TARGET_OUTPUT_PATH) { + $TargetOutputPath = $Env:TARGET_OUTPUT_PATH + New-Item -Path $TargetOutputPath -ItemType 'Directory' -Force | Out-Null + Copy-Item $PackageFile "$TargetOutputPath/devolutions-agent.pkg" -Force + } + } + [void] Package([string]$PackageOption) { if ($this.Product -Eq 'jetsocat') { throw "Packaging for $($this.Product) is not supported" @@ -943,6 +1026,8 @@ class TlkRecipe } } elseif ($this.Target.IsLinux()) { $this.Package_Linux() + } elseif ($this.Target.IsMacOS()) { + $this.Package_MacOS() } } diff --git a/devolutions-agent/src/cli.rs b/devolutions-agent/src/cli.rs index 64bfa8d7d..06f21855f 100644 --- a/devolutions-agent/src/cli.rs +++ b/devolutions-agent/src/cli.rs @@ -13,7 +13,13 @@ use devolutions_agent::AgentServiceEvent; use devolutions_agent::config::ConfHandle; use devolutions_agent::enrollment::parse_enrollment_jwt; -use crate::service::{DESCRIPTION, DISPLAY_NAME, SERVICE_NAME}; +use crate::service::{DESCRIPTION, DISPLAY_NAME}; + +/// The launchd label, which must match the LaunchDaemon plist that the macOS package installs. +#[cfg(target_os = "macos")] +const CONTROLLER_NAME: &str = "com.devolutions.agent"; +#[cfg(not(target_os = "macos"))] +const CONTROLLER_NAME: &str = crate::service::SERVICE_NAME; #[derive(Debug, PartialEq, Eq)] struct UpCommand { @@ -97,7 +103,7 @@ fn parse_up_command_args_with_reader(args: &[String], mut stdin_read } pub(crate) fn run() { - let mut controller = Controller::new(SERVICE_NAME, DISPLAY_NAME, DESCRIPTION); + let mut controller = Controller::new(CONTROLLER_NAME, DISPLAY_NAME, DESCRIPTION); if let Some(cmd) = env::args().nth(1) { match cmd.as_str() { diff --git a/package/AgentMacOS/README.md b/package/AgentMacOS/README.md new file mode 100644 index 000000000..01a295e28 --- /dev/null +++ b/package/AgentMacOS/README.md @@ -0,0 +1,48 @@ +# Devolutions Agent for macOS + +The installer package runs Devolutions Agent as the `com.devolutions.agent` launchd daemon, as root. +There is one package per architecture; install the one that matches the Mac. + +```sh +sudo installer -pkg devolutions-agent--arm64.pkg -target / +``` + +The package installs these files: + +| Path | Content | +| --- | --- | +| `/usr/local/bin/devolutions-agent` | Agent executable | +| `/usr/local/bin/devolutions-agent-uninstall` | Uninstall script | +| `/Library/LaunchDaemons/com.devolutions.agent.plist` | launchd daemon definition | +| `/Library/Application Support/Devolutions Agent` | Configuration and credentials, readable by root only | +| `/Library/Logs/Devolutions Agent` | Standard output and error of the daemon | + +The Agent writes its own log files to the configuration directory. +Installing creates a default `agent.json` when none exists, then starts the daemon; launchd restarts it after a crash and at boot. +Upgrading keeps the existing configuration. + +Start and stop the daemon with the Agent itself or with `launchctl`: + +```sh +sudo devolutions-agent stop +sudo devolutions-agent start +sudo launchctl kickstart -k system/com.devolutions.agent +``` + +PowerShell Universal jobs need PowerShell 7, found through the daemon's `PATH`, which includes `/usr/local/bin` and `/opt/homebrew/bin`. + +Uninstall the Agent and keep its configuration, or remove everything with `--purge`: + +```sh +sudo devolutions-agent-uninstall +sudo devolutions-agent-uninstall --purge +``` + +Build a package from a built executable on macOS: + +```sh +DAGENT_EXECUTABLE=target/release/devolutions-agent pwsh ./ci/tlk.ps1 package -Product agent -Platform macos -Architecture arm64 +``` + +Set `DAGENT_PKG_SIGNING_IDENTITY` to a Developer ID Installer identity to sign the package. +`Test-Package.sh` installs a package, checks the daemon, upgrades, and uninstalls it; run it only on a disposable machine. diff --git a/package/AgentMacOS/Test-Package.sh b/package/AgentMacOS/Test-Package.sh new file mode 100755 index 000000000..be1226ce6 --- /dev/null +++ b/package/AgentMacOS/Test-Package.sh @@ -0,0 +1,64 @@ +#!/bin/sh + +# Installs, upgrades, and uninstalls the Devolutions Agent macOS package, checking the launchd service at each step. +# Changes the system it runs on, so only run it on a disposable machine such as a CI runner. + +set -eu + +PKG="$1" +LABEL="com.devolutions.agent" +PLIST="/Library/LaunchDaemons/$LABEL.plist" +DATA_DIR="/Library/Application Support/Devolutions Agent" +LOG_DIR="/Library/Logs/Devolutions Agent" + +fail() { + echo "FAIL: $*" >&2 + sudo /bin/launchctl print "system/$LABEL" >&2 || true + sudo cat "$LOG_DIR/agent.stderr.log" >&2 || true + exit 1 +} + +# Polls because launchd starts and stops the job asynchronously. +wait_for_state() { + expected="$1" + for _ in $(seq 1 30); do + state="$(sudo /bin/launchctl print "system/$LABEL" 2>/dev/null | awk '/^\tstate = / { sub(/^\tstate = /, ""); print; exit }')" + [ "$state" = "$expected" ] && return 0 + sleep 1 + done + fail "expected the service to be $expected, got '${state:-not loaded}'" +} + +install_pkg() { + sudo /usr/sbin/installer -pkg "$PKG" -target / || fail "installer failed" +} + +echo "Installing $PKG" +install_pkg + +[ -x /usr/local/bin/devolutions-agent ] || fail "binary not installed" +[ -x /usr/local/bin/devolutions-agent-uninstall ] || fail "uninstall script not installed" +[ "$(stat -f '%Su:%Sg %Lp' "$PLIST")" = "root:wheel 644" ] || fail "unexpected plist ownership or mode" +[ "$(sudo stat -f '%Su %Lp' "$DATA_DIR")" = "root 700" ] || fail "unexpected data directory ownership or mode" +sudo test -f "$DATA_DIR/agent.json" || fail "configuration not initialized" +wait_for_state running + +echo "Stopping and starting through the agent command line" +sudo /usr/local/bin/devolutions-agent stop +wait_for_state "not running" +sudo /usr/local/bin/devolutions-agent start +wait_for_state running + +echo "Upgrading over the installed package" +install_pkg +wait_for_state running + +echo "Uninstalling" +sudo /usr/local/bin/devolutions-agent-uninstall --purge +sudo /bin/launchctl print "system/$LABEL" >/dev/null 2>&1 && fail "service still loaded after uninstall" +for path in /usr/local/bin/devolutions-agent /usr/local/bin/devolutions-agent-uninstall "$PLIST" "$DATA_DIR" "$LOG_DIR"; do + [ -e "$path" ] && fail "$path still present after uninstall" +done +/usr/sbin/pkgutil --pkg-info "$LABEL" >/dev/null 2>&1 && fail "package receipt still present after uninstall" + +echo "PASS" diff --git a/package/AgentMacOS/com.devolutions.agent.plist b/package/AgentMacOS/com.devolutions.agent.plist new file mode 100644 index 000000000..73b96bad3 --- /dev/null +++ b/package/AgentMacOS/com.devolutions.agent.plist @@ -0,0 +1,30 @@ + + + + + Label + com.devolutions.agent + ProgramArguments + + /usr/local/bin/devolutions-agent + + EnvironmentVariables + + PATH + /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin + + RunAtLoad + + KeepAlive + + SuccessfulExit + + + ExitTimeOut + 45 + StandardOutPath + /Library/Logs/Devolutions Agent/agent.stdout.log + StandardErrorPath + /Library/Logs/Devolutions Agent/agent.stderr.log + + diff --git a/package/AgentMacOS/devolutions-agent-uninstall b/package/AgentMacOS/devolutions-agent-uninstall new file mode 100755 index 000000000..f27c3d9d6 --- /dev/null +++ b/package/AgentMacOS/devolutions-agent-uninstall @@ -0,0 +1,39 @@ +#!/bin/sh + +# Removes Devolutions Agent. +# The configuration, credentials, and logs are kept unless --purge is given. + +set -eu + +LABEL="com.devolutions.agent" +PLIST="/Library/LaunchDaemons/$LABEL.plist" +DATA_DIR="/Library/Application Support/Devolutions Agent" +LOG_DIR="/Library/Logs/Devolutions Agent" + +PURGE=0 +case "${1:-}" in + "") ;; + --purge) PURGE=1 ;; + *) + echo "usage: $0 [--purge]" >&2 + exit 2 + ;; +esac + +if [ "$(/usr/bin/id -u)" -ne 0 ]; then + echo "$0 must run as root, for example with sudo" >&2 + exit 1 +fi + +if /bin/launchctl print "system/$LABEL" >/dev/null 2>&1; then + /bin/launchctl bootout "system/$LABEL" +fi + +/bin/rm -f "$PLIST" /usr/local/bin/devolutions-agent +/usr/sbin/pkgutil --forget "$LABEL" >/dev/null 2>&1 || true + +if [ "$PURGE" -eq 1 ]; then + /bin/rm -rf "$DATA_DIR" "$LOG_DIR" +fi + +/bin/rm -f /usr/local/bin/devolutions-agent-uninstall diff --git a/package/AgentMacOS/distribution.xml b/package/AgentMacOS/distribution.xml new file mode 100644 index 000000000..43515fef4 --- /dev/null +++ b/package/AgentMacOS/distribution.xml @@ -0,0 +1,20 @@ + + + Devolutions Agent + com.devolutions + + + + + + + + + + + + + + + {{ component_pkg }} + diff --git a/package/AgentMacOS/scripts/postinstall b/package/AgentMacOS/scripts/postinstall new file mode 100755 index 000000000..2387ecd19 --- /dev/null +++ b/package/AgentMacOS/scripts/postinstall @@ -0,0 +1,24 @@ +#!/bin/sh + +set -e + +LABEL="com.devolutions.agent" +PLIST="/Library/LaunchDaemons/$LABEL.plist" +DATA_DIR="/Library/Application Support/Devolutions Agent" +LOG_DIR="/Library/Logs/Devolutions Agent" + +# The data directory holds the configuration and credentials, so only root may read it. +if [ ! -d "$DATA_DIR" ]; then + /bin/mkdir -p "$DATA_DIR" + /bin/chmod 700 "$DATA_DIR" +fi + +/bin/mkdir -p "$LOG_DIR" + +# A broken existing configuration must not block the upgrade that may fix it. +/usr/local/bin/devolutions-agent config init >/dev/null || echo "devolutions-agent: failed to initialize the configuration" >&2 + +/bin/launchctl enable "system/$LABEL" +/bin/launchctl bootstrap system "$PLIST" + +exit 0 diff --git a/package/AgentMacOS/scripts/preinstall b/package/AgentMacOS/scripts/preinstall new file mode 100755 index 000000000..fdfa45b60 --- /dev/null +++ b/package/AgentMacOS/scripts/preinstall @@ -0,0 +1,18 @@ +#!/bin/sh + +# Stops a running Agent so the payload can replace its binary. + +LABEL="com.devolutions.agent" + +if /bin/launchctl print "system/$LABEL" >/dev/null 2>&1; then + /bin/launchctl bootout "system/$LABEL" || true + + # postinstall cannot bootstrap the label again until launchd has fully unloaded it. + i=0 + while /bin/launchctl print "system/$LABEL" >/dev/null 2>&1 && [ "$i" -lt 60 ]; do + /bin/sleep 1 + i=$((i + 1)) + done +fi + +exit 0