hypertwist/scripts/Invoke-HyperTwistInteractiveDesktopTask.ps1
2026-07-02 08:02:11 +00:00

233 lines
6.2 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_-]', '_')
}
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
result = 'failed'
error = $null
}
try
{
$CreateArgs = @(
'/Create', '/F',
'/TN', $TaskName,
'/SC', 'ONCE',
'/ST', '23:59',
'/SD', (Get-Date).ToString('MM/dd/yyyy'),
'/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)."
}
& 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
}
$ResolvedFailureResultPath = if ([string]::IsNullOrWhiteSpace($ResultPath))
{
$TaskResultPath
}
else
{
$ResultPath
}
Write-Utf8JsonFile -Path $ResolvedFailureResultPath -Value $Result
throw
}
finally
{
if (-not $KeepArtifacts)
{
& schtasks.exe /Delete /TN $TaskName /F | Out-Null
if (Test-Path -LiteralPath $TaskRunnerPath)
{
Remove-Item -LiteralPath $TaskRunnerPath -Force
}
}
}
if ([string]::IsNullOrWhiteSpace($ResultPath))
{
$ResultPath = $TaskResultPath
}
Write-Utf8JsonFile -Path $ResultPath -Value $Result
$Result | ConvertTo-Json -Depth 12