304 lines
8.5 KiB
PowerShell
304 lines
8.5 KiB
PowerShell
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$InlineCommand,
|
|
[int]$TimeoutSeconds = 120,
|
|
[string]$TaskNamePrefix = 'HyperTwist-InteractiveDesktop',
|
|
[string]$ResultPath = '',
|
|
[switch]$KeepArtifacts
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
function Write-Utf8JsonFile {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[Parameter(Mandatory = $true)]
|
|
[object]$Value
|
|
)
|
|
|
|
$ParentPath = Split-Path -Parent $Path
|
|
if (-not [string]::IsNullOrWhiteSpace($ParentPath))
|
|
{
|
|
New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
|
|
}
|
|
|
|
$Json = $Value | ConvertTo-Json -Depth 12
|
|
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
[System.IO.File]::WriteAllText($Path, $Json, $Utf8NoBom)
|
|
}
|
|
|
|
function Read-JsonFile {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path
|
|
)
|
|
|
|
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
|
}
|
|
|
|
function Convert-TaskToken {
|
|
param(
|
|
[string]$Value
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Value))
|
|
{
|
|
return 'unknown'
|
|
}
|
|
|
|
return ($Value -replace '[^A-Za-z0-9_-]', '_')
|
|
}
|
|
|
|
function Get-TaskOwnedProcessIds {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RunnerPath
|
|
)
|
|
|
|
$ProcessSnapshots = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue)
|
|
$OwnedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
|
|
foreach ($ProcessSnapshot in $ProcessSnapshots)
|
|
{
|
|
if (-not [string]::IsNullOrWhiteSpace([string]$ProcessSnapshot.CommandLine) `
|
|
-and ([string]$ProcessSnapshot.CommandLine).IndexOf(
|
|
$RunnerPath,
|
|
[System.StringComparison]::OrdinalIgnoreCase
|
|
) -ge 0)
|
|
{
|
|
[void]$OwnedProcessIds.Add([int]$ProcessSnapshot.ProcessId)
|
|
}
|
|
}
|
|
|
|
$AddedProcess = $true
|
|
while ($AddedProcess)
|
|
{
|
|
$AddedProcess = $false
|
|
foreach ($ProcessSnapshot in $ProcessSnapshots)
|
|
{
|
|
if ($OwnedProcessIds.Contains([int]$ProcessSnapshot.ParentProcessId) `
|
|
-and $OwnedProcessIds.Add([int]$ProcessSnapshot.ProcessId))
|
|
{
|
|
$AddedProcess = $true
|
|
}
|
|
}
|
|
}
|
|
|
|
return @($OwnedProcessIds)
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($InlineCommand))
|
|
{
|
|
throw 'Provide -InlineCommand.'
|
|
}
|
|
|
|
$TaskToken = Convert-TaskToken -Value (
|
|
'{0}-{1}' -f $TaskNamePrefix, (Get-Date).ToString('yyyyMMdd-HHmmss')
|
|
)
|
|
$TaskName = $TaskToken
|
|
$TaskRunnerPath = 'C:\Windows\Temp\{0}.runner.ps1' -f $TaskToken
|
|
$TaskResultPath = 'C:\Windows\Temp\{0}.result.json' -f $TaskToken
|
|
$EncodedInlineCommand = [Convert]::ToBase64String(
|
|
[System.Text.Encoding]::Unicode.GetBytes($InlineCommand)
|
|
)
|
|
|
|
if (Test-Path -LiteralPath $TaskResultPath)
|
|
{
|
|
Remove-Item -LiteralPath $TaskResultPath -Force
|
|
}
|
|
|
|
$RunnerScript = @"
|
|
`$ErrorActionPreference = 'Stop'
|
|
`$Result = [ordered]@{
|
|
succeeded = `$false
|
|
user = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
processId = `$PID
|
|
sessionId = (Get-Process -Id `$PID).SessionId
|
|
explorerSessionIds = @((Get-Process explorer -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SessionId -Unique))
|
|
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
|
|
outputLines = @()
|
|
error = `$null
|
|
}
|
|
try {
|
|
`$CommandSource = [System.Text.Encoding]::Unicode.GetString(
|
|
[Convert]::FromBase64String('$EncodedInlineCommand')
|
|
)
|
|
`$CapturedOutput = @(& ([scriptblock]::Create(`$CommandSource)) 2>&1 | ForEach-Object { `$_.ToString() })
|
|
`$Result.outputLines = @(`$CapturedOutput)
|
|
`$Result.succeeded = `$true
|
|
} catch {
|
|
if (`$null -ne `$CapturedOutput) {
|
|
`$Result.outputLines = @(`$CapturedOutput)
|
|
}
|
|
`$Result.error = `$_.Exception.Message
|
|
}
|
|
[System.IO.File]::WriteAllText(
|
|
'$TaskResultPath',
|
|
(`$Result | ConvertTo-Json -Depth 12),
|
|
(New-Object System.Text.UTF8Encoding(`$false))
|
|
)
|
|
if (-not `$Result.succeeded) {
|
|
exit 1
|
|
}
|
|
"@
|
|
|
|
[System.IO.File]::WriteAllText(
|
|
$TaskRunnerPath,
|
|
$RunnerScript,
|
|
(New-Object System.Text.UTF8Encoding($false))
|
|
)
|
|
|
|
$Result = [ordered]@{
|
|
reportVersion = 'ht-interactive-desktop-task/v1'
|
|
generatedAtUtc = [DateTime]::UtcNow.ToString('o')
|
|
taskName = $TaskName
|
|
taskRunnerPath = $TaskRunnerPath
|
|
taskResultPath = $TaskResultPath
|
|
timeoutSeconds = $TimeoutSeconds
|
|
currentSessionId = (Get-Process -Id $PID).SessionId
|
|
explorerSessionIds = @(
|
|
Get-Process explorer -ErrorAction SilentlyContinue |
|
|
Select-Object -ExpandProperty SessionId -Unique
|
|
)
|
|
createExitCode = $null
|
|
runExitCode = $null
|
|
taskOutput = $null
|
|
terminatedOwnedProcessIds = @()
|
|
result = 'failed'
|
|
error = $null
|
|
}
|
|
$TaskCreated = $false
|
|
$FailureResultPath = $null
|
|
|
|
try
|
|
{
|
|
$CreateArgs = @(
|
|
'/Create', '/F',
|
|
'/TN', $TaskName,
|
|
'/SC', 'ONCE',
|
|
'/ST', '23:59',
|
|
'/TR', ('C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File {0}' -f $TaskRunnerPath),
|
|
'/RU', 'INTERACTIVE'
|
|
)
|
|
& schtasks.exe @CreateArgs | Out-Null
|
|
$Result.createExitCode = $LASTEXITCODE
|
|
if ($Result.createExitCode -ne 0)
|
|
{
|
|
throw "Creating the interactive desktop task failed with exit code $($Result.createExitCode)."
|
|
}
|
|
$TaskCreated = $true
|
|
|
|
& schtasks.exe /Run /TN ('\' + $TaskName) | Out-Null
|
|
$Result.runExitCode = $LASTEXITCODE
|
|
if ($Result.runExitCode -ne 0)
|
|
{
|
|
throw "Running the interactive desktop task failed with exit code $($Result.runExitCode)."
|
|
}
|
|
|
|
$Deadline = [DateTime]::UtcNow.AddSeconds([Math]::Max($TimeoutSeconds, 1))
|
|
while ([DateTime]::UtcNow -lt $Deadline)
|
|
{
|
|
if (Test-Path -LiteralPath $TaskResultPath)
|
|
{
|
|
break
|
|
}
|
|
|
|
Start-Sleep -Milliseconds 500
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $TaskResultPath))
|
|
{
|
|
throw "The interactive desktop task did not write '$TaskResultPath' before timeout."
|
|
}
|
|
|
|
$TaskOutput = Read-JsonFile -Path $TaskResultPath
|
|
if ($null -eq $TaskOutput)
|
|
{
|
|
throw "The interactive desktop task result '$TaskResultPath' could not be parsed."
|
|
}
|
|
|
|
$Result.taskOutput = $TaskOutput
|
|
if (-not [bool]$TaskOutput.succeeded)
|
|
{
|
|
$TaskError = if ([string]::IsNullOrWhiteSpace([string]$TaskOutput.error))
|
|
{
|
|
'The interactive desktop task reported failure without an explicit error message.'
|
|
}
|
|
else
|
|
{
|
|
[string]$TaskOutput.error
|
|
}
|
|
|
|
throw $TaskError
|
|
}
|
|
|
|
$Result.result = 'passed'
|
|
}
|
|
catch
|
|
{
|
|
$Result.error = $_.Exception.Message
|
|
if ($null -eq $Result.taskOutput -and (Test-Path -LiteralPath $TaskResultPath))
|
|
{
|
|
$Result.taskOutput = Read-JsonFile -Path $TaskResultPath
|
|
}
|
|
$FailureResultPath = if ([string]::IsNullOrWhiteSpace($ResultPath))
|
|
{
|
|
$TaskResultPath
|
|
}
|
|
else
|
|
{
|
|
$ResultPath
|
|
}
|
|
Write-Utf8JsonFile -Path $FailureResultPath -Value $Result
|
|
throw
|
|
}
|
|
finally
|
|
{
|
|
if (-not $KeepArtifacts)
|
|
{
|
|
$TaskOwnedProcessIds = if ($TaskCreated)
|
|
{
|
|
@(Get-TaskOwnedProcessIds -RunnerPath $TaskRunnerPath)
|
|
}
|
|
else
|
|
{
|
|
@()
|
|
}
|
|
if ($TaskCreated)
|
|
{
|
|
# A timed-out interactive child must not survive as a hidden,
|
|
# memory-consuming desktop task.
|
|
& schtasks.exe /End /TN ('\' + $TaskName) *> $null
|
|
if ($LASTEXITCODE -eq 0)
|
|
{
|
|
Start-Sleep -Milliseconds 250
|
|
}
|
|
& schtasks.exe /Delete /TN $TaskName /F | Out-Null
|
|
}
|
|
foreach ($TaskOwnedProcessId in @($TaskOwnedProcessIds | Sort-Object -Descending))
|
|
{
|
|
if ($TaskOwnedProcessId -ne $PID `
|
|
-and $null -ne (Get-Process -Id $TaskOwnedProcessId -ErrorAction SilentlyContinue))
|
|
{
|
|
Stop-Process -Id $TaskOwnedProcessId -Force -ErrorAction SilentlyContinue
|
|
$Result.terminatedOwnedProcessIds += [int]$TaskOwnedProcessId
|
|
}
|
|
}
|
|
if (Test-Path -LiteralPath $TaskRunnerPath)
|
|
{
|
|
Remove-Item -LiteralPath $TaskRunnerPath -Force
|
|
}
|
|
if (-not [string]::IsNullOrWhiteSpace($FailureResultPath))
|
|
{
|
|
Write-Utf8JsonFile -Path $FailureResultPath -Value $Result
|
|
}
|
|
}
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ResultPath))
|
|
{
|
|
$ResultPath = $TaskResultPath
|
|
}
|
|
|
|
Write-Utf8JsonFile -Path $ResultPath -Value $Result
|
|
$Result | ConvertTo-Json -Depth 12
|