mirror of
https://github.com/royal32/Frame-HEV-Power-Level-SFX.git
synced 2026-10-17 14:01:14 +02:00
100 lines
5.0 KiB
PowerShell
100 lines
5.0 KiB
PowerShell
# Run in PowerShell from an extracted release on Windows.
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$HostName = 'steamos@frame.local',
|
|
[switch]$Offline,
|
|
[string]$SourceDir = '',
|
|
[switch]$Help
|
|
)
|
|
$ErrorActionPreference = 'Stop'
|
|
if ($Help) {
|
|
@'
|
|
Usage: .\install.ps1 [-HostName USER@HOST] [-Offline] [-SourceDir PATH]
|
|
|
|
Uploads and installs HEV announcements on your awake Steam Frame using Windows
|
|
OpenSSH and tar. The default target is steamos@frame.local. SSH/scp may each ask
|
|
for your Frame password; no password is stored. -SourceDir is an audio directory
|
|
on the Frame, not on this computer. -Offline disables audio downloading.
|
|
Use Windows Terminal or a normal PowerShell window. PowerShell ISE cannot
|
|
handle the interactive SSH prompts.
|
|
'@
|
|
exit 0
|
|
}
|
|
if ($Host.Name -eq 'Windows PowerShell ISE Host' -or [Console]::IsInputRedirected) {
|
|
throw 'SSH needs an interactive console. Open Windows Terminal or press Win+R, type powershell, and press Enter. Run this installer there, without piping or redirecting its input. PowerShell ISE does not support SSH keyboard prompts.'
|
|
}
|
|
if ($HostName -notmatch '^([A-Za-z0-9_][A-Za-z0-9_.-]*@)?[A-Za-z0-9][A-Za-z0-9.-]*$') {
|
|
throw 'Use a hostname or IPv4 address, optionally preceded by USER@ (no SSH options or spaces).'
|
|
}
|
|
foreach ($command in @('ssh', 'scp', 'tar')) {
|
|
if (-not (Get-Command $command -ErrorAction SilentlyContinue)) {
|
|
throw "$command is required. Enable the Windows OpenSSH Client if SSH/scp are missing."
|
|
}
|
|
}
|
|
$files = @(
|
|
'VERSION', 'README.md', 'LICENSE', 'frame_hev.py', 'assets/README.md', 'assets/manifest.json',
|
|
'config/environment', 'systemd/frame-hev.service', 'tools/install.sh', 'tools/install.py',
|
|
'tools/uninstall.sh', 'tools/control.sh', 'tools/fetch_sounds.py'
|
|
)
|
|
foreach ($file in $files) {
|
|
$item = Get-Item -LiteralPath (Join-Path $PSScriptRoot $file) -ErrorAction SilentlyContinue
|
|
if (-not $item -or $item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
|
throw "Missing release file: $file. Extract the whole download first."
|
|
}
|
|
}
|
|
$token = [Guid]::NewGuid().ToString('N')
|
|
$archive = Join-Path ([IO.Path]::GetTempPath()) "frame-hev-$token.tar"
|
|
$remoteArchive = "/tmp/frame-hev-$token.tar"
|
|
$remoteWork = "/tmp/frame-hev-$token"
|
|
$offlineValue = if ($Offline) { '1' } else { '0' }
|
|
$remoteScript = @'
|
|
set -euo pipefail
|
|
work=$1
|
|
archive=$2
|
|
offline=$3
|
|
audio_source=$4
|
|
trap 'rm -f -- "$archive"' EXIT
|
|
mkdir -m 700 -- "$work"
|
|
trap 'rm -rf -- "$work"; rm -f -- "$archive"' EXIT
|
|
tar -xf "$archive" -C "$work"
|
|
args=()
|
|
[[ "$offline" == 0 ]] || args+=(--offline)
|
|
[[ -z "$audio_source" ]] || args+=(--source-dir "$audio_source")
|
|
bash "$work/tools/install.sh" "${args[@]}" </dev/null
|
|
'@
|
|
# Windows here-strings can contain CRLF, which must not reach the Linux shell.
|
|
$remoteScript = $remoteScript.Replace("`r", '')
|
|
# PowerShell 5.1 can drop embedded double quotes in native command arguments.
|
|
# Send the exact Bash argument array as base64 JSON; the Frame's existing Python
|
|
# decodes it and launches Bash without any shell interpretation of user options.
|
|
$remoteArguments = @('bash', '-c', $remoteScript, '--', $remoteWork, $remoteArchive, $offlineValue, $SourceDir)
|
|
$argumentJson = ConvertTo-Json -InputObject $remoteArguments -Compress
|
|
$argumentData = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($argumentJson))
|
|
$remoteCommand = "python3 -c 'import base64,json,subprocess,sys;sys.exit(subprocess.call(json.loads(base64.b64decode(sys.argv[1]))))' " + $argumentData
|
|
$uploadAttempted = $false
|
|
$remoteAttempted = $false
|
|
try {
|
|
# Do not pipe archive bytes through PowerShell's text pipeline.
|
|
& tar -cf $archive -C $PSScriptRoot @files
|
|
if ($LASTEXITCODE -ne 0) { throw 'Could not create the installation archive.' }
|
|
Write-Host "Installing on $HostName. scp and SSH may ask for your Frame password."
|
|
Write-Host 'On first connection, SSH may ask you to confirm the host fingerprint with yes and Enter.'
|
|
Write-Host 'Password typing is invisible. If the yes/no prompt ignores keys, use a normal PowerShell window or Windows Terminal.'
|
|
$uploadAttempted = $true
|
|
& scp -- $archive "${HostName}:$remoteArchive"
|
|
if ($LASTEXITCODE -ne 0) { throw 'Upload failed. Check that the Frame is awake and SSH is enabled.' }
|
|
$remoteAttempted = $true
|
|
& ssh -T -- $HostName $remoteCommand
|
|
if ($LASTEXITCODE -ne 0) { throw 'Installation failed. See the Frame installer output above.' }
|
|
} finally {
|
|
Remove-Item -LiteralPath $archive -Force -ErrorAction SilentlyContinue
|
|
# A failed scp can leave a partial file. Remove only this invocation's GUID path.
|
|
# Once SSH starts, the remote shell's EXIT trap owns cleanup.
|
|
if ($uploadAttempted -and -not $remoteAttempted) {
|
|
# Never open another password/host-key prompt while handling a failed
|
|
# upload: it can look like the original prompt is stuck in a loop.
|
|
& ssh -T -o BatchMode=yes -o ConnectTimeout=5 -o ConnectionAttempts=1 -- $HostName ('rm -f -- ' + $remoteArchive)
|
|
if ($LASTEXITCODE -ne 0) { Write-Warning "Could not remove temporary upload $remoteArchive on the Frame." }
|
|
}
|
|
}
|