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
8 changes: 8 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ jobs:
run: |
uv run pwsh scripts/windows/build-binary.ps1

- name: Test install.ps1 end-to-end (Windows)
if: matrix.platform == 'windows'
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/windows/test-install-script.ps1

- name: Upload binary as workflow artifact
uses: actions/upload-artifact@v4
with:
Expand Down
24 changes: 24 additions & 0 deletions docs/src/content/docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,30 @@ $env:APM_DEBUG = "1"
apm install <package>
```

### `Access is denied` running apm.exe on Windows (AppLocker / App Control for Business)

If the installer (or `apm self-update`) fails at the `Testing binary...` step with `Access is denied` / HRESULT `0x80070005`, an enterprise application control policy ([AppLocker](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/applocker/applocker-overview) or [App Control for Business / WDAC](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/)) is blocking execution of `apm.exe` from a user-writable path.

The installer stages the binary under `%LOCALAPPDATA%\Programs\apm\releases\<tag>` **before** invoking it, so a single allow-list rule for that path is enough.

Ask your endpoint admin to add one of:

- **Path rule:** `%LOCALAPPDATA%\Programs\apm\*`
- **Publisher / hash rule** for the released `apm.exe`

If you cannot change policy, set `APM_TEMP_DIR` to a directory your policy allows and retry:

```powershell
$env:APM_TEMP_DIR = "$env:LOCALAPPDATA\Programs\apm\tmp"
irm https://aka.ms/apm-windows | iex
```

As a last resort, install via pip (runs from your Python user site):

```powershell
pip install --user apm-cli
```

## Next steps

See the [Quick Start](../quick-start/) to set up your first project.
168 changes: 150 additions & 18 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,72 @@ function Write-ManualInstallHelp {
Write-Host "Need help? Create an issue at: $GithubUrl/$ApmRepo/issues"
}

function Get-Sha256Hex {
# Stream-based SHA256 that works even when Get-FileHash is unavailable
# (hardened hosts, $PSModuleAutoLoadingPreference='None', restricted sessions).
# System.Security.Cryptography is a core .NET type allowed in ConstrainedLanguage.
param([string]$Path)
$cmd = Get-Command Get-FileHash -ErrorAction SilentlyContinue
if (-not $cmd) {
try {
Import-Module Microsoft.PowerShell.Utility -ErrorAction Stop
$cmd = Get-Command Get-FileHash -ErrorAction SilentlyContinue
} catch {
}
}
if ($cmd) {
return (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLower()
}
$stream = $null
$hasher = $null
try {
$stream = [System.IO.File]::OpenRead($Path)
$hasher = [System.Security.Cryptography.SHA256]::Create()
$bytes = $hasher.ComputeHash($stream)
$sb = New-Object System.Text.StringBuilder
foreach ($b in $bytes) { [void]$sb.Append($b.ToString("x2")) }
return $sb.ToString()
} finally {
if ($hasher) { $hasher.Dispose() }
if ($stream) { $stream.Dispose() }
}
}

function Test-AccessDeniedError {
# AppLocker / WDAC / App Control for Business denies CreateProcess on EXEs
# under user-writable paths (e.g. %TEMP%, %LOCALAPPDATA%\Temp) with HRESULT
# 0x80070005 (E_ACCESSDENIED), surfaced by PowerShell as "Access is denied".
param([string]$Text)
if (-not $Text) { return $false }
return ($Text -match 'Access is denied' -or $Text -match '0x80070005')
}

function Write-AppControlGuidance {
param(
[string]$Path,
[string]$TargetInstallDir
)
Write-Host ""
Write-ErrorText "The OS denied execution of $Path."
Write-Host "This is the standard signature of an enterprise application control policy"
Write-Host "(AppLocker or App Control for Business / WDAC) denying an unsigned binary"
Write-Host "from a user-writable path."
Write-Host ""
Write-Info "Options to unblock:"
if ($TargetInstallDir) {
Write-Host " 1. Ask your endpoint admin to allow-list the final install path"
Write-Host " ($TargetInstallDir) via an AppLocker/WDAC Path or Publisher rule."
} else {
Write-Host " 1. Ask your endpoint admin to allow-list apm.exe via an"
Write-Host " AppLocker/WDAC Path or Publisher rule."
}
Write-Host " 2. Set APM_TEMP_DIR to a directory your policy permits, then retry:"
Write-Host " `$env:APM_TEMP_DIR = `"`$env:LOCALAPPDATA\Programs\apm\tmp`""
Write-Host " 3. Install via pip into your user site:"
Write-Host " pip install --user apm-cli"
Write-Host ""
}

# ---------------------------------------------------------------------------
# Banner
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -487,7 +553,7 @@ try {
if ($fetched -and (Test-Path $sha256Path)) {
try {
$expectedHash = (Get-Content $sha256Path -Raw).Trim().Split(" ")[0]
$actualHash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLower()
$actualHash = Get-Sha256Hex -Path $zipPath
if ($actualHash -ne $expectedHash) {
Write-ErrorText "Checksum verification FAILED."
Write-Host " Expected: $expectedHash"
Expand Down Expand Up @@ -515,49 +581,115 @@ try {
}

# ------------------------------------------------------------------
# Extract
# Extract + stage + binary test + promote
#
# Order matters: AppLocker / App Control for Business commonly block
# executable launch from %TEMP%. We move the extracted bundle to the
# final per-user install root ($releasesDir, default
# %LOCALAPPDATA%\Programs\apm\releases\<tag>) BEFORE invoking
# apm.exe --version, so the binary test runs from the allow-listed
# path that the shim will keep pointing at. Until promotion succeeds
# we stage to a sibling `.new-<guid>` directory so a failed install
# never destroys the currently working release. See issue #1389.
# ------------------------------------------------------------------

Write-Info "Extracting package..."
Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force

$packageDir = Join-Path $tempDir "apm-windows-x86_64"
$exePath = Join-Path $packageDir "apm.exe"
if (-not (Test-Path $exePath)) {
Write-ErrorText "Extracted package is missing apm.exe."
if (-not (Test-Path $packageDir)) {
Write-ErrorText "Extracted package is missing the apm-windows-x86_64 directory."
Write-Info "Attempting automatic fallback to pip..."
if (Install-ViaPip) { exit 0 }
Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo
exit 1
}

# ------------------------------------------------------------------
# Binary test
# ------------------------------------------------------------------
$stagingDir = "$releaseDir.new-" + [System.Guid]::NewGuid().ToString("N")
if (Test-Path $stagingDir) {
Remove-Item -Recurse -Force $stagingDir
}
try {
Move-Item -Path $packageDir -Destination $stagingDir -Force
} catch {
$stageError = "$_"
Write-ErrorText "Failed to stage release at ${stagingDir}: $stageError"
if (Test-AccessDeniedError -Text $stageError) {
Write-AppControlGuidance -Path $stagingDir -TargetInstallDir $releaseDir
}
Write-Info "Attempting automatic fallback to pip..."
if (Install-ViaPip) { exit 0 }
Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo
exit 1
}

$stagedExe = Join-Path $stagingDir "apm.exe"
if (-not (Test-Path $stagedExe)) {
Write-ErrorText "Staged package is missing apm.exe."
Remove-Item -Recurse -Force $stagingDir -ErrorAction SilentlyContinue
Write-Info "Attempting automatic fallback to pip..."
if (Install-ViaPip) { exit 0 }
Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo
exit 1
}

Write-Info "Testing binary..."
$testFailure = $null
try {
$testOutput = & $exePath --version 2>&1
if ($LASTEXITCODE -ne 0) { throw "exit code $LASTEXITCODE" }
$testOutput = & $stagedExe --version 2>&1
if ($LASTEXITCODE -ne 0) { throw "exit code $LASTEXITCODE - $testOutput" }
Write-Success "Binary test successful: $testOutput"
} catch {
Write-ErrorText "Downloaded binary failed to run: $_"
Write-Host ""
$testFailure = "$_"
}

if ($testFailure) {
$denied = Test-AccessDeniedError -Text $testFailure
Write-ErrorText "Downloaded binary failed to run: $testFailure"
if ($denied) {
Write-AppControlGuidance -Path $stagedExe -TargetInstallDir $releaseDir
}
Remove-Item -Recurse -Force $stagingDir -ErrorAction SilentlyContinue
Write-Info "Attempting automatic fallback to pip..."
if (Install-ViaPip) { exit 0 }
Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo
exit 1
}

# ------------------------------------------------------------------
# Install
# ------------------------------------------------------------------

# Promote: rename the existing release aside, then rename the staged
# tree into place. Win32 has no truly atomic directory replacement, so
# there is still a small gap where neither path exists; doing it this
# way minimizes that gap and lets us roll back if the second rename
# fails. Concurrent apm invocations during that window will fail and
# need a retry — acceptable for an install/self-update operation.
$backupDir = $null
if (Test-Path $releaseDir) {
Remove-Item -Recurse -Force $releaseDir
$backupDir = "$releaseDir.old-" + [System.Guid]::NewGuid().ToString("N")
try {
Move-Item -Path $releaseDir -Destination $backupDir -Force
} catch {
Write-ErrorText "Failed to move existing release aside: $_"
Remove-Item -Recurse -Force $stagingDir -ErrorAction SilentlyContinue
Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo
exit 1
}
}

try {
Move-Item -Path $stagingDir -Destination $releaseDir -Force
} catch {
Write-ErrorText "Failed to promote staged release: $_"
if ($backupDir -and (Test-Path $backupDir)) {
Move-Item -Path $backupDir -Destination $releaseDir -Force -ErrorAction SilentlyContinue
}
Remove-Item -Recurse -Force $stagingDir -ErrorAction SilentlyContinue
Write-ManualInstallHelp -GithubUrl $githubUrl -ApmRepo $apmRepo
exit 1
}

Move-Item -Path $packageDir -Destination $releaseDir
if ($backupDir -and (Test-Path $backupDir)) {
Remove-Item -Recurse -Force $backupDir -ErrorAction SilentlyContinue
}

$shimPath = Join-Path $binDir "apm.cmd"
$shimContent = "@echo off`r`n`"$releaseDir\apm.exe`" %*`r`n"
Expand Down
Loading
Loading