Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.

Starting in this release, 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.
158 changes: 140 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,105 @@ 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 {
Write-ErrorText "Failed to stage release at ${stagingDir}: $_"
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: replace any existing release atomically (rename the old one
# aside first so concurrent shim invocations can't see a missing dir).
$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
}
}

Move-Item -Path $packageDir -Destination $releaseDir
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
}

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