aboutsummaryrefslogtreecommitdiff
path: root/scripts/windows_input/Common.ps1
blob: d944e87b135188d51a42545e0a0186a7b4e4f75f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# Shared test helpers. Dot-sourcing does not touch the desktop or console.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Write-GauntletJson($Path, $Value) {
    $temporary = "$Path.$([guid]::NewGuid().ToString('N')).tmp"
    [IO.File]::WriteAllText($temporary, ($Value | ConvertTo-Json -Depth 30), [Text.UTF8Encoding]::new($false))
    for ($attempt = 0; ; $attempt++) {
        try { [IO.File]::Move($temporary, $Path, $true); return }
        catch [IO.IOException], [UnauthorizedAccessException] {
            if ($attempt -ge 99) { throw }
            Start-Sleep -Milliseconds 10
        }
    }
}

function Read-GauntletJson($Path) {
    if (Test-Path -LiteralPath $Path) {
        $stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, ([IO.FileShare]::ReadWrite -bor [IO.FileShare]::Delete))
        try {
            $reader = [IO.StreamReader]::new($stream)
            try { return $reader.ReadToEnd() | ConvertFrom-Json -AsHashtable }
            finally { $reader.Dispose() }
        } finally { $stream.Dispose() }
    }
    return $null
}

function Quote-GauntletPS([string] $Text) { return "'" + $Text.Replace("'", "''") + "'" }

function New-GauntletProcess($Exe, $Arguments, $Plan, [switch] $Capture) {
    $info = [Diagnostics.ProcessStartInfo]::new($Exe)
    $info.UseShellExecute = $false
    foreach ($argument in $Arguments) { $info.ArgumentList.Add([string]$argument) }
    foreach ($name in @($info.Environment.Keys)) {
        if ($name -like 'HERDR_*' -or $name -in @('SSH_CONNECTION', 'SSH_TTY')) { $null = $info.Environment.Remove($name) }
    }
    if ($null -ne $Plan) {
        $info.Environment['HERDR_CONFIG_PATH'] = $Plan.config
        $info.Environment['XDG_CONFIG_HOME'] = $Plan.config_home
        $info.Environment['HERDR_SESSION'] = $Plan.session
        if ($Plan.profile -ne 'default') { $info.Environment['HERDR_WINDOWS_INPUT_PROBE'] = $Plan.profile }
        if ($Plan.path -in @('herdr', 'herdr-remote')) { $info.Environment['HERDR_WINDOWS_INPUT_TRACE_FILE'] = $Plan.input_trace }
        if ($Plan.path -eq 'herdr-remote') {
            $info.Environment['HERDR_REMOTE_KEYBINDINGS'] = 'local'
        }
    }
    $info.RedirectStandardOutput = $Capture.IsPresent
    $info.RedirectStandardError = $Capture.IsPresent
    $info.CreateNoWindow = $Capture.IsPresent
    return [Diagnostics.Process]::Start($info)
}

function Invoke-GauntletProcess($Exe, $Arguments, $Plan = $null, [int] $Timeout = 30) {
    $process = New-GauntletProcess $Exe $Arguments $Plan -Capture
    try {
        $stdout = $process.StandardOutput.ReadToEndAsync()
        $stderr = $process.StandardError.ReadToEndAsync()
        $timer = [Diagnostics.Stopwatch]::StartNew()
        if (-not $process.WaitForExit($Timeout * 1000)) {
            $process.Kill($true)
            throw "Command timed out: $Exe $($Arguments -join ' ')"
        }
        $remaining = [Math]::Max(0, $Timeout * 1000 - [int]$timer.ElapsedMilliseconds)
        if (-not [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($stdout, $stderr), $remaining)) {
            throw "Output streams stayed open after exit: $Exe $($Arguments -join ' ')"
        }
        if ($process.ExitCode -ne 0) { throw "Command failed ($($process.ExitCode)): $Exe`n$($stderr.GetAwaiter().GetResult())" }
        return $stdout.GetAwaiter().GetResult()
    } finally { $process.Dispose() }
}

function Update-GauntletLease($Root) {
    [IO.File]::WriteAllText((Join-Path $Root 'lease'), [DateTime]::UtcNow.ToString('O'))
}

function Test-GauntletLease($Root) {
    $lease = Get-Item -LiteralPath (Join-Path $Root 'lease') -ErrorAction SilentlyContinue
    return $null -ne $lease -and ([DateTime]::UtcNow - $lease.LastWriteTimeUtc).TotalSeconds -lt 90
}

function Wait-GauntletJson($Path, $Root, $Predicate, [int] $Timeout = 20) {
    $deadline = [DateTime]::UtcNow.AddSeconds($Timeout)
    do {
        Update-GauntletLease $Root
        $value = Read-GauntletJson $Path
        if ($null -ne $value -and (& $Predicate $value)) { return $value }
        Start-Sleep -Milliseconds 100
    } while ([DateTime]::UtcNow -lt $deadline)
    throw "Timed out waiting for fresh acknowledgement: $Path"
}