Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
automerge
bbwe
denelon
dkbennett
Freeware
Expand All @@ -11,4 +12,5 @@ nullsoft
stephengillie
img
UTF
wekyb
wix
70 changes: 68 additions & 2 deletions Tools/SandboxTest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# 2 = Unable to kill a running process
# 3 = WinGet is not installed
# 4 = Manifest validation error
# 5 = Unattended run did not complete within the timeout
###

[CmdletBinding()]
Expand Down Expand Up @@ -68,6 +69,10 @@ param(
return $true
})]
[string] $WinGetOptions,
# Unattended
[Parameter(HelpMessage = 'Wait up to this many minutes for the sandbox run to complete and write its output to the host')]
[ValidateRange(1, [int]::MaxValue)]
[int] $Unattended,
# Switches
[switch] $SkipManifestValidation,
[switch] $Prerelease,
Expand All @@ -84,8 +89,11 @@ enum DependencySources {
$ProgressPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Stop' # This gets overridden most places, but is set explicitly here to help catch errors
if ($PSBoundParameters.Keys -notcontains 'InformationAction') { $InformationPreference = 'Continue' } # If the user didn't explicitly set an InformationAction, Override their preference
$script:IsUnattended = $PSBoundParameters.Keys -contains 'Unattended'
if ($PSBoundParameters.Keys -contains 'WarningAction') {
$script:OnMappedFolderWarning = $PSBoundParameters.WarningAction
} elseif ($script:IsUnattended) {
$script:OnMappedFolderWarning = 'Continue'
} else {
$script:OnMappedFolderWarning = 'Inquire'
}
Expand Down Expand Up @@ -783,6 +791,14 @@ function Get-ARPTable {
}

Push-Location $($script:SandboxTestDataFolder)
`$Unattended = $([int]$script:IsUnattended)
`$ResultsFolder = Join-Path '$($script:SandboxTestDataFolder)' 'Results'
`$metadata = [ordered]@{ StartTime = (Get-Date).ToString('o') }
if (`$Unattended) {
New-Item -Path `$ResultsFolder -ItemType Directory -Force | Out-Null
Start-Transcript -Path (Join-Path `$ResultsFolder 'transcript.log') | Out-Null
}
try {
Write-Host @'
--> Installing WinGet
'@
Expand Down Expand Up @@ -875,7 +891,14 @@ if (`$manifestFolder) {

`"@
`$originalARP = Get-ARPTable
winget install -m `$manifestFolder --accept-package-agreements --verbose-logs --ignore-local-archive-malware-scan --dependency-source winget $WinGetOptions
`$metadata.Manifest = `$manifestFolder | Split-Path -Leaf
if (`$Unattended) {
winget install -m `$manifestFolder --accept-package-agreements --verbose-logs --ignore-local-archive-malware-scan --dependency-source winget $WinGetOptions | Out-Host
} else {
winget install -m `$manifestFolder --accept-package-agreements --verbose-logs --ignore-local-archive-malware-scan --dependency-source winget $WinGetOptions
}
`$metadata.InstallExitCode = `$LASTEXITCODE
`$metadata.InstallExitCodeHex = '0x{0:X8}' -f `$LASTEXITCODE

Write-Host @'

Expand All @@ -899,7 +922,28 @@ if (`$BoundParameterScript) {
}

'@
& `$BoundParameterScript.FullName
`$global:LASTEXITCODE = 0
try {
& `$BoundParameterScript.FullName
`$metadata.ScriptSucceeded = `$?
} catch {
`$metadata.ScriptSucceeded = `$false
`$metadata.ScriptError = "`$_"
throw
} finally {
`$metadata.ScriptExitCode = `$LASTEXITCODE
}
}
} finally {
if (`$Unattended) {
`$metadata.EndTime = (Get-Date).ToString('o')
if (Get-Command winget -ErrorAction SilentlyContinue) { `$metadata.WinGetVersion = winget --version }
`$metadata.OSVersion = '{0}.{1}' -f [Environment]::OSVersion.Version.ToString(3), (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').UBR
`$metadata | ConvertTo-Json | Out-File -FilePath (Join-Path `$ResultsFolder 'metadata.json') -Encoding utf8
Copy-Item -Path (Join-Path `$env:LOCALAPPDATA 'Packages\$($script:AppInstallerPFN)\LocalState\DiagOutputDir') -Destination (Join-Path `$ResultsFolder 'WinGetLogs') -Recurse -ErrorAction SilentlyContinue
Stop-Transcript | Out-Null
New-Item -Path (Join-Path `$ResultsFolder 'done') -ItemType File -Force | Out-Null
}
}

Pop-Location
Expand Down Expand Up @@ -966,4 +1010,26 @@ $Script

Write-Verbose "Invoking the sandbox using $script:ConfigurationFile"
WindowsSandbox $script:ConfigurationFile

if ($script:IsUnattended) {
$resultsFolder = Join-Path -Path $script:TestDataFolder -ChildPath 'Results'
$doneFile = Join-Path -Path $resultsFolder -ChildPath 'done'
$deadline = (Get-Date).AddMinutes($Unattended)
Write-Information "--> Waiting up to $Unattended minutes for the sandbox run to complete"
while (!(Test-Path -Path $doneFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 5 }

Get-Content -Path (Join-Path -Path $resultsFolder -ChildPath 'transcript.log') -ErrorAction SilentlyContinue | ForEach-Object { Write-Information $_ }
Write-Information @"
--> Sandbox results:
- Transcript: $(Join-Path -Path $resultsFolder -ChildPath 'transcript.log')
- Metadata: $(Join-Path -Path $resultsFolder -ChildPath 'metadata.json')
- WinGet Logs: $(Join-Path -Path $resultsFolder -ChildPath 'WinGetLogs')
"@
if (!(Test-Path -Path $doneFile)) {
Write-Error -Category OperationTimeout "The sandbox run did not complete within $Unattended minutes" -ErrorAction Continue
Invoke-CleanExit -ExitCode 5
}
Get-Content -Path (Join-Path -Path $resultsFolder -ChildPath 'metadata.json') -ErrorAction SilentlyContinue | ForEach-Object { Write-Information $_ }
}

Invoke-CleanExit -ExitCode 0
30 changes: 30 additions & 0 deletions doc/tools/SandboxTest.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Run the script with the full path to the manifest as an argument. This validates
| **-MapFolder** | Folder to map in Sandbox (default: current directory) |
| **-WinGetVersion** | Specify the version of WinGet to use in Sandbox |
| **-WinGetOptions** | Additional options for the `winget install` command |
| **-Unattended** | Wait up to the specified number of minutes for the Sandbox run to finish, then write its output to the host |
| **-SkipManifestValidation** | Skip `winget validate -m <manifest>` if already validated |
| **-Prerelease** | Use preview release versions of WinGet |
| **-EnableExperimentalFeatures** | Enable WinGet experimental features |
Expand Down Expand Up @@ -80,6 +81,35 @@ Run the script with the full path to the manifest as an argument. This validates

---

## Unattended Mode

By default the script starts the Sandbox and returns immediately; the output of the session running inside the Sandbox stays inside the Sandbox. The `-Unattended` option makes the script wait for that session to finish and then write its output to the host, which is useful for automation.

`-Unattended` requires a value, which is the number of minutes to wait before giving up. 15 is a good starting point.

```powershell
.\SandboxTest.ps1 <path-to-manifest> -Unattended 15
```

The Sandbox is left open after the run finishes.

### Results

The Sandbox session writes its results to `%LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\SandboxTest\Results`:

| Item | Description |
|------------------|--------------------------------------------------------------------------|
| `transcript.log` | Full PowerShell transcript of the Sandbox session |
| `metadata.json` | WinGet version, OS build, manifest folder name, `winget install` exit code (decimal and hexadecimal), `-Script` exit status, and start/end timestamps |
| `WinGetLogs\` | WinGet diagnostic logs copied from the Sandbox |
| `done` | Marker file written once the Sandbox session has finished |

This folder is deleted at the start of every run, so copy anything you want to keep before running the script again.

The transcript and the metadata are also written to the host output once the run finishes. If the run does not finish within the given number of minutes, the script writes whatever the transcript contains so far and exits with code 5.

---

## Integration with Other Scripts

- **[YamlCreate Script](../../Tools/YamlCreate.ps1):** Uses `SandboxTest.ps1` to test newly created manifests.
Expand Down