diff --git a/scripts/README.md b/scripts/README.md index 5e4ad8b..af64957 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -43,6 +43,10 @@ This folder has small helpers for build, CI, and router install. Keep it simple; - `dev_push.ps1` — Fast dev loop: build + upload to router. Usage: - `./scripts/dev_push.ps1 -Pass [-HostName ] [-RemotePath ] [-Restart] [-Quiet]` - Uploads to `.tmp` then atomically moves into place; `-Restart` restarts the service; `-Quiet` silences MOTD. + - Defaults to both binaries plus init/hooks, with restart enabled. Use `-Restart:$false` to leave the service running, or `-Quiet:$false` for deployment output. Config files are untouched. + - Binary-only agent update: `./scripts/dev_push.ps1 -HostName router -BinariesOnly -Package wakey-agent -Quiet:$false` + - Use `-BinariesOnly` without `-Package` for both binaries, or `-Package wakey` for only the CLI. Cargo still builds the selected package's dependencies. + - The checked-in `remote_deploy_wakey.sh` is streamed over SSH to `sh -s -- --batch ...` in all modes, with the deployment plan passed as quoted arguments. No remote helper is uploaded or required. Its original single-binary invocation remains supported. The shared `Invoke-Ssh -Cmd 'sh -s' -Stdin $script` supports stdin scripts for other callers. - `package_rootfs.ps1` — Produces `dist/wakey-rootfs--.tgz` with `/root/.bin/wakey` and `/etc/init.d/*`. - `update_wakey_cc.sh` — Linux VPS updater for control-plane bundles; extracts into the selected directory (default: current directory) and restarts `wakey-cc.service`. - `update_wakey_cc_from_repo.sh` — Linux VPS source updater; clones repo to temp dir, builds UI + control-plane locally, packages bundle tgz, installs into selected directory, and restarts `wakey-cc.service`. diff --git a/scripts/dev_push.ps1 b/scripts/dev_push.ps1 index 246cd80..342ada1 100755 --- a/scripts/dev_push.ps1 +++ b/scripts/dev_push.ps1 @@ -4,6 +4,9 @@ param( [ValidateSet("cargo", "cross")] [string]$Cargo = "cargo", + [ValidateSet("wakey", "wakey-agent")] + [ValidateNotNullOrEmpty()] + [string[]]$Package = @("wakey", "wakey-agent"), [string]$Pass, [string]$HostName = "192.168.100.1", [int]$Port = 22, @@ -17,6 +20,7 @@ param( [string]$HostKey, [switch]$ForcePassword, [switch]$SkipInitScript, + [switch]$BinariesOnly, [switch]$Restart = $true, [switch]$Quiet = $true ) @@ -26,109 +30,90 @@ try { $PSStyle.OutputRendering = 'Host' } catch {} . "$PSScriptRoot/lib.ps1" -$Pass = Get-DefaultPassword $Pass +function Build-Packages { + param([string[]]$Packages) + $buildArgs = @('build', '--release', '--target', $Target) + foreach ($pkg in $Packages) { $buildArgs += @('-p', $pkg) } + Write-Host "[build] $Cargo $($buildArgs -join ' ')" -ForegroundColor Cyan + & $Cargo @buildArgs + if ($LASTEXITCODE -ne 0) { throw "$Cargo build failed ($LASTEXITCODE)" } +} + +function New-DeploymentPlan { + param([string[]]$Packages) + # Keep CLI first so replacing both binaries restarts the service only once. + $binaries = @( + @{ Package = 'wakey'; Name = $BinName; Destination = $RemotePath } + @{ Package = 'wakey-agent'; Name = $AgentBinName; Destination = $AgentRemotePath } + ) | Where-Object { $_.Package -in $Packages } | ForEach-Object { + [pscustomobject]@{ + Local = Join-Path $repoRoot "target/$Target/release/$($_.Name)" + Temp = "$($_.Destination).tmp" + Destination = $_.Destination + } + } + $support = @( + if (-not $BinariesOnly) { + if (-not $SkipInitScript) { + @{ Source = 'init/openwrt/wakey'; Temp = '/var/tmp/wakey.init.tmp'; Destination = $RemoteInitPath } + } + @{ Source = 'hotplug/openwrt/dhcp/95-wakey'; Temp = '/var/tmp/wakey.hotplug.dhcp.tmp'; Destination = '/etc/hotplug.d/dhcp/95-wakey' } + @{ Source = 'hotplug/openwrt/neigh/95-wakey'; Temp = '/var/tmp/wakey.hotplug.neigh.tmp'; Destination = '/etc/hotplug.d/neigh/95-wakey' } + } + ) | ForEach-Object { + $local = Join-Path $PSScriptRoot $_.Source + if (Test-Path -LiteralPath $local) { + [pscustomobject]@{ Local = $local; Temp = $_.Temp; Destination = $_.Destination } + } + } + foreach ($binary in $binaries) { + if (-not (Test-Path -LiteralPath $binary.Local)) { throw "binary not found: $($binary.Local)" } + } + [pscustomobject]@{ Binaries = @($binaries); Support = @($support) } +} + +function Get-RemoteDeployment { + param($Plan) + $deployArgs = @('--batch') + for ($i = 0; $i -lt $Plan.Binaries.Count; $i++) { + $binary = $Plan.Binaries[$i] + $restartFlag = if ($Restart -and $i -eq $Plan.Binaries.Count - 1) { '1' } else { '0' } + $deployArgs += @('--binary', $binary.Temp, $binary.Destination, $restartFlag) + } + foreach ($file in $Plan.Support) { + $deployArgs += @('--install', $file.Temp, $file.Destination) + } + # Only data needs shell quoting; the checked-in shell program travels as stdin. + $command = 'sh -s -- ' + (($deployArgs | ForEach-Object { Quote-ShArg $_ }) -join ' ') + if ($Quiet) { $command += ' >/dev/null 2>&1' } + [pscustomobject]@{ + Script = Get-Content -Raw -LiteralPath "$PSScriptRoot/remote_deploy_wakey.sh" + Command = $command + } +} + +function Send-DeploymentFiles { + param($Plan) + foreach ($file in @($Plan.Binaries) + @($Plan.Support)) { + Invoke-Scp -Local $file.Local -Dest "$User@${HostName}:$($file.Temp)" -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword + } +} + +$Pass = Get-DefaultPassword $Pass # Backward compatibility: allow HostName in the form host:port. $hostPort = Split-HostPort -HostName $HostName -DefaultPort $Port $HostName = $hostPort.Host $Port = $hostPort.Port - -function Get-DeployScript { - param($DeployPreferred, $DeployTmp, $RemoteTmp, $RemotePath, $RestartFlag) - return @" -DEPLOY=$DeployPreferred; -if [ ! -x "`$DEPLOY" ] && [ -f $DeployTmp ]; then - sed -i "s/\r$//" $DeployTmp 2>/dev/null || true - chmod +x $DeployTmp - DEPLOY=$DeployTmp -fi -sh "`$DEPLOY" $RemoteTmp $RemotePath $RestartFlag -"@ -} - -#---- Main Flow ---- $repoRoot = Split-Path -Parent $PSScriptRoot Push-Location $repoRoot - try { - Write-Host "[build] $Cargo build --release --target $Target -p wakey -p wakey-agent" -ForegroundColor Cyan - . "$Cargo" build --release --target $Target -p wakey -p wakey-agent - if ($LASTEXITCODE -ne 0) { throw "$Cargo build failed ($LASTEXITCODE)" } - - $localBin = Join-Path $repoRoot "target/$Target/release/$BinName" - $localAgentBin = Join-Path $repoRoot "target/$Target/release/$AgentBinName" - if (-not (Test-Path $localBin)) { throw "binary not found: $localBin" } - if (-not (Test-Path $localAgentBin)) { throw "binary not found: $localAgentBin" } - - $remoteTmp = "$RemotePath.tmp" - $agentRemoteTmp = "$AgentRemotePath.tmp" - $destTmp = "$User@${HostName}:$remoteTmp" - $agentDestTmp = "$User@${HostName}:$agentRemoteTmp" - $initTmp = '/var/tmp/wakey.init.tmp' - $initDestTmp = "$User@${HostName}:$initTmp" - $localDeploy = Join-Path $repoRoot 'scripts/remote_deploy_wakey.sh' - $localInit = Join-Path $repoRoot 'scripts/init/openwrt/wakey' - $localDhcpHotplug = Join-Path $repoRoot 'scripts/hotplug/openwrt/dhcp/95-wakey' - $localNeighHotplug = Join-Path $repoRoot 'scripts/hotplug/openwrt/neigh/95-wakey' - $deployTmp = '/var/tmp/remote_deploy_wakey.sh' - $deployPreferred = '/root/.bin/remote_deploy_wakey.sh' - $dhcpHotplugTmp = '/var/tmp/wakey.hotplug.dhcp.tmp' - $neighHotplugTmp = '/var/tmp/wakey.hotplug.neigh.tmp' - - # Push binaries - Invoke-Scp -Local $localBin -Dest $destTmp -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword - Invoke-Scp -Local $localAgentBin -Dest $agentDestTmp -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword - - # Push OpenWrt init script unless explicitly skipped - if (-not $SkipInitScript -and (Test-Path $localInit)) { - Invoke-Scp -Local $localInit -Dest $initDestTmp -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword - } - if (Test-Path $localDhcpHotplug) { - Invoke-Scp -Local $localDhcpHotplug -Dest "$User@${HostName}:$dhcpHotplugTmp" -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword - } - if (Test-Path $localNeighHotplug) { - Invoke-Scp -Local $localNeighHotplug -Dest "$User@${HostName}:$neighHotplugTmp" -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword - } - - # Push deploy helper if exists - if (Test-Path $localDeploy) { - Invoke-Scp -Local $localDeploy -Dest "$User@${HostName}:$deployTmp" -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword - } - - # Build and run remote deploy command - $restartFlag = $(if ($Restart) { '1' } else { '0' }) - $script = @" -$(Get-DeployScript $deployPreferred $deployTmp $remoteTmp $RemotePath 0) -$(Get-DeployScript $deployPreferred $deployTmp $agentRemoteTmp $AgentRemotePath $restartFlag) -$(if (-not $SkipInitScript) { -@" -if [ -f $initTmp ]; then - sed -i "s/\r$//" $initTmp 2>/dev/null || true - if command -v install >/dev/null 2>&1; then - install -m 0755 $initTmp $RemoteInitPath - else - cp -f $initTmp $RemoteInitPath - chmod 0755 $RemoteInitPath - fi -fi -"@ -}) -if [ -f $dhcpHotplugTmp ]; then - sed -i "s/\r$//" $dhcpHotplugTmp 2>/dev/null || true - mkdir -p /etc/hotplug.d/dhcp - cp -f $dhcpHotplugTmp /etc/hotplug.d/dhcp/95-wakey -fi -if [ -f $neighHotplugTmp ]; then - sed -i "s/\r$//" $neighHotplugTmp 2>/dev/null || true - mkdir -p /etc/hotplug.d/neigh - cp -f $neighHotplugTmp /etc/hotplug.d/neigh/95-wakey -fi -"@ - $remoteCmd = "sh -lc '$($script -replace "`r",'')'" - if ($Quiet) { $remoteCmd += " >/dev/null 2>&1" } - - Invoke-Ssh -Cmd $remoteCmd -User $User -Remote $HostName -Pass $Pass -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword - + Build-Packages ($Package | Select-Object -Unique) + $plan = New-DeploymentPlan $Package + # Prepare before uploading so missing local helpers fail without remote changes. + $deployment = Get-RemoteDeployment $plan + Send-DeploymentFiles $plan + Invoke-Ssh -Cmd $deployment.Command -Stdin $deployment.Script -User $User -Remote $HostName -Pass $Pass -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword Write-Host "done ✔" -ForegroundColor Green } finally { diff --git a/scripts/lib.ps1 b/scripts/lib.ps1 index e77f34a..37011b6 100755 --- a/scripts/lib.ps1 +++ b/scripts/lib.ps1 @@ -191,7 +191,7 @@ function Get-SshpassCommand { } function Invoke-Ext { - param($Exe, $Arguments, $Label) + param($Exe, $Arguments, $Label, [string]$Stdin) $displayArgs = $Arguments.Clone() for ($i = 0; $i -lt $displayArgs.Count; $i++) { if ($displayArgs[$i] -eq '-pw' -and ($i + 1) -lt $displayArgs.Count) { @@ -203,7 +203,12 @@ function Invoke-Ext { } Write-Host ("[{0}] {1} {2}" -f $Label, $Exe, ($displayArgs -join ' ')) -ForegroundColor Cyan - $out = & $Exe @Arguments 2>&1 + # Keep stdin optional so interactive callers retain their existing behavior. + if ($PSBoundParameters.ContainsKey('Stdin')) { + $out = $Stdin | & $Exe @Arguments 2>&1 + } else { + $out = & $Exe @Arguments 2>&1 + } if ($LASTEXITCODE -ne 0) { if ($Exe -like '*sshpass*' -and $LASTEXITCODE -eq 6) { Write-Error ("{0} failed: sshpass could not confirm host key automatically (code 6). Use ssh once manually, or enable StrictHostKeyChecking=accept-new." -f $Label) @@ -284,8 +289,14 @@ function Invoke-Scp { function Invoke-Ssh { - param($Cmd, $User, $Remote, $Pass, [int]$Port = 22, [switch]$Quiet, [switch]$ForcePassword) + # Supply Stdin for scripts, e.g. -Cmd 'sh -s' -Stdin $script. This avoids + # embedding a whole script in a remote shell command's quoting layer. + param($Cmd, $User, $Remote, $Pass, [int]$Port = 22, [switch]$Quiet, [switch]$ForcePassword, [string]$Stdin) $Cmd = Normalize-LineEndings $Cmd + $inputArgs = @{} + if ($PSBoundParameters.ContainsKey('Stdin')) { + $inputArgs.Stdin = Normalize-LineEndings $Stdin + } $isWin = [bool]$IsWindows $isWsl = Test-IsWsl @@ -297,7 +308,7 @@ function Invoke-Ssh { if ($Pass) { $arguments += @('-pw', $Pass) } $dest = if ($User) { "$User@$Remote" } else { $Remote } $arguments += $dest, $Cmd - Invoke-Ext -Exe $plink.Path -Arguments $arguments -Label 'ssh' + Invoke-Ext -Exe $plink.Path -Arguments $arguments -Label 'ssh' @inputArgs } else { $arguments = @() @@ -319,7 +330,7 @@ function Invoke-Ssh { $sshpassExe = Get-SshpassCommand if ($sshpassExe) { $wrapped = @('-p', $Pass, 'ssh') + $arguments - Invoke-Ext -Exe $sshpassExe -Arguments $wrapped -Label 'ssh' + Invoke-Ext -Exe $sshpassExe -Arguments $wrapped -Label 'ssh' @inputArgs return } if (-not $script:WarnedNoSshpass) { @@ -338,7 +349,7 @@ function Invoke-Ssh { throw 'ForcePassword requires sshpass' } - Invoke-Ext -Exe 'ssh' -Arguments $arguments -Label 'ssh' + Invoke-Ext -Exe 'ssh' -Arguments $arguments -Label 'ssh' @inputArgs } } diff --git a/scripts/remote_deploy_wakey.sh b/scripts/remote_deploy_wakey.sh index bb20995..5fabf1f 100755 --- a/scripts/remote_deploy_wakey.sh +++ b/scripts/remote_deploy_wakey.sh @@ -1,41 +1,80 @@ #!/bin/sh # Usage: remote_deploy_wakey.sh [restart_flag] # restart_flag: 1 (default) to stop/start, 0 to only replace file. +# Or stream over SSH: sh -s -- --batch [--binary | +# --install ]... set -e -BIN_TMP="$1" -DEST="$2" -RESTART="${3:-1}" -INIT="/etc/init.d/wakey" -KILL="/root/.bin/kill_wakey.sh" -if [ -z "$BIN_TMP" ] || [ -z "$DEST" ]; then - echo "usage: $0 [restart_flag]" >&2 - exit 2 -fi +# A subshell keeps per-binary variables and exit statements local, including +# when several replacements are requested by one dev_push invocation. +deploy_binary() ( + BIN_TMP="$1" + DEST="$2" + RESTART="${3:-1}" + INIT="/etc/init.d/wakey" + KILL="/root/.bin/kill_wakey.sh" -[ -f "$BIN_TMP" ] || { - echo "tmp binary not found: $BIN_TMP" >&2 - exit 1 + if [ -z "$BIN_TMP" ] || [ -z "$DEST" ]; then + echo "usage: $0 [restart_flag]" >&2 + exit 2 + fi + + [ -f "$BIN_TMP" ] || { + echo "tmp binary not found: $BIN_TMP" >&2 + exit 1 + } + chmod +x "$BIN_TMP" || true + + if [ "$RESTART" = "1" ]; then + if [ -x "$INIT" ]; then + "$INIT" stop || true + elif [ -x "$KILL" ]; then + sh "$KILL" || true + fi + fi + + mv -f "$BIN_TMP" "$DEST" + + if [ "$RESTART" = "1" ]; then + if [ -x "$INIT" ]; then + "$INIT" start || "$INIT" restart || true + else + nohup "$DEST" >/dev/null 2>&1 & + fi + fi + + exit 0 +) + +install_support_file() { + sed -i 's/\r$//' "$1" 2>/dev/null || true + mkdir -p "$(dirname "$2")" + cp -f "$1" "$2" + chmod 0755 "$2" } -chmod +x "$BIN_TMP" || true -if [ "$RESTART" = "1" ]; then - if [ -x "$INIT" ]; then - "$INIT" stop || true - elif [ -x "$KILL" ]; then - sh "$KILL" || true - fi +if [ "${1:-}" != '--batch' ]; then + deploy_binary "$@" + exit fi -mv -f "$BIN_TMP" "$DEST" - -if [ "$RESTART" = "1" ]; then - if [ -x "$INIT" ]; then - "$INIT" start || "$INIT" restart || true - else - nohup "$DEST" >/dev/null 2>&1 & - fi -fi - -exit 0 +shift +while [ "$#" -gt 0 ]; do + case "$1" in + --binary) + [ "$#" -ge 4 ] || { echo '--binary requires tmp, destination, restart' >&2; exit 2; } + deploy_binary "$2" "$3" "$4" + shift 4 + ;; + --install) + [ "$#" -ge 3 ] || { echo '--install requires tmp and destination' >&2; exit 2; } + install_support_file "$2" "$3" + shift 3 + ;; + *) + echo "unknown deployment operation: $1" >&2 + exit 2 + ;; + esac +done diff --git a/scripts/tests/dev_push.ps1 b/scripts/tests/dev_push.ps1 new file mode 100644 index 0000000..ac0eb9c --- /dev/null +++ b/scripts/tests/dev_push.ps1 @@ -0,0 +1,115 @@ +#!/usr/bin/env pwsh +# Run with pwsh -NoProfile -File scripts/tests/dev_push.ps1. No router is contacted. +$ErrorActionPreference = 'Stop' +$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +$fixture = Join-Path ([IO.Path]::GetTempPath()) "wakey-dev-push-$([guid]::NewGuid())" + +function Assert($condition, $message) { + if (-not $condition) { throw $message } +} + +try { + New-Item -ItemType Directory -Path "$fixture/scripts" -Force | Out-Null + Copy-Item "$repo/scripts/dev_push.ps1" "$fixture/scripts/dev_push.ps1" + Copy-Item "$repo/scripts/lib.ps1" "$fixture/scripts/actual-lib.ps1" + foreach ($file in @('remote_deploy_wakey.sh', 'init/openwrt/wakey', 'hotplug/openwrt/dhcp/95-wakey', 'hotplug/openwrt/neigh/95-wakey')) { + $dest = "$fixture/scripts/$file" + New-Item -ItemType Directory -Path (Split-Path -Parent $dest) -Force | Out-Null + Copy-Item "$repo/scripts/$file" $dest + } + # Override only external operations; keep path resolution and quoting real. + @' +. "$PSScriptRoot/actual-lib.ps1" +function cargo { + $global:DeployTest.Build = @($args) + $global:LASTEXITCODE = $global:DeployTest.BuildExit +} +function Invoke-Scp { + param($Local, $Dest, $Pass, $HostKey, $Port, [switch]$Quiet, [switch]$ForcePassword) + $global:DeployTest.Uploads.Add([pscustomobject]@{ Local = $Local; Dest = $Dest }) + if ($global:DeployTest.Execute) { + Copy-Item -LiteralPath $Local -Destination ($Dest -split ':', 2)[1] + } +} +function Invoke-Ssh { + param($Cmd, $User, $Remote, $Pass, $Port, [switch]$Quiet, [switch]$ForcePassword, $Stdin) + $global:DeployTest.Command = $Cmd + $global:DeployTest.Body = $Stdin + if (-not $Cmd.StartsWith('sh -s -- ')) { throw 'Deployment must use SSH stdin with sh -s' } + # Parse exactly what would reach the remote shell, without executing it. + $Stdin | & sh -n + if ($LASTEXITCODE -ne 0) { throw 'Generated deployment shell is invalid' } + if ($global:DeployTest.Execute) { + $Stdin | & sh -c $Cmd + if ($LASTEXITCODE -ne 0) { throw 'Local fixture deployment failed' } + } +} +'@ | Set-Content "$fixture/scripts/lib.ps1" + + function Run-Deploy($options = @{}, $binaries = @('wakey', 'wakey-agent'), $buildExit = 0, $execute = $false) { + $global:DeployTest = @{ + Build = @(); BuildExit = $buildExit; Execute = $execute + Uploads = [System.Collections.Generic.List[object]]::new() + Command = $null + } + $output = "$fixture/target/test/release" + if (Test-Path $output) { Remove-Item -Recurse -Force $output } + New-Item -ItemType Directory -Path $output -Force | Out-Null + foreach ($binary in $binaries) { New-Item -ItemType File -Path "$output/$binary" | Out-Null } + & "$fixture/scripts/dev_push.ps1" -Pass test-only -HostName router -Target test -Quiet:$false @options + } + + Run-Deploy + Assert ($global:DeployTest.Uploads.Count -eq 5) 'Default deploy must upload both binaries and three support files' + Assert (($global:DeployTest.Build -join ' ') -eq 'build --release --target test -p wakey -p wakey-agent') 'Default build changed' + Assert ($global:DeployTest.Command.Contains("'/root/.bin/wakey.tmp' '/root/.bin/wakey' '0'")) 'CLI must not restart before agent replacement' + Assert ($global:DeployTest.Command.Contains("'/root/.bin/wakey-agent.tmp' '/root/.bin/wakey-agent' '1'")) 'Agent must restart by default' + Assert ($global:DeployTest.Body -ceq (Get-Content -Raw "$repo/scripts/remote_deploy_wakey.sh")) 'Deployment must stream the checked-in file unchanged' + + Run-Deploy @{ BinariesOnly = $true } + Assert ($global:DeployTest.Uploads.Count -eq 2) 'Binary-only deploy uploaded support files' + Assert (-not $global:DeployTest.Command.Contains('--install')) 'Binary-only deploy contains support installation' + + foreach ($package in @('wakey', 'wakey-agent')) { + Run-Deploy @{ BinariesOnly = $true; Package = @($package, $package); Restart = $false } @($package) + Assert ($global:DeployTest.Uploads.Count -eq 1) 'Single-package deploy uploaded another file' + Assert (($global:DeployTest.Build -join ' ') -eq "build --release --target test -p $package") 'Package selection or deduplication failed' + Assert ($global:DeployTest.Command.Contains("'/root/.bin/$package.tmp' '/root/.bin/$package' '0'")) 'Restart override lost' + } + + Run-Deploy @{ SkipInitScript = $true } + Assert ($global:DeployTest.Uploads.Count -eq 4) 'SkipInitScript must still upload hooks' + + Run-Deploy @{ BinariesOnly = $true; Package = @('wakey-agent'); AgentRemotePath = "/tmp/owner's bin/agent" } @('wakey-agent') + Assert ($global:DeployTest.Command.Contains('owner')) 'Custom path lost' + + # Execute only binary replacement inside the temporary fixture, with restart + # disabled. This also checks that the helper's exit doesn't skip binary two. + $cliPath = "$fixture/installed cli" + $agentPath = "$fixture/owner's agent" + Run-Deploy @{ BinariesOnly = $true; Restart = $false; RemotePath = $cliPath; AgentRemotePath = $agentPath } @('wakey', 'wakey-agent') 0 $true + Assert ((Test-Path -LiteralPath $cliPath) -and (Test-Path -LiteralPath $agentPath)) 'Streamed helper did not install both binaries' + Assert (-not (Test-Path -LiteralPath "$agentPath.tmp")) 'Temporary upload was not moved' + + $supportTemp = "$fixture/support.tmp" + $supportDest = "$fixture/hook dir/owner's hook" + [IO.File]::WriteAllText($supportTemp, "#!/bin/sh`r`necho hook`r`n") + & sh "$fixture/scripts/remote_deploy_wakey.sh" --batch --install $supportTemp $supportDest + Assert ($LASTEXITCODE -eq 0) 'Support installation failed' + Assert (-not (Get-Content -Raw -LiteralPath $supportDest).Contains("`r")) 'Support CRLF was not normalized' + & sh -c 'test -x "$1"' sh $supportDest + Assert ($LASTEXITCODE -eq 0) 'Installed support file is not executable' + + New-Item -ItemType File -Path "$fixture/legacy.tmp" | Out-Null + & sh "$fixture/scripts/remote_deploy_wakey.sh" "$fixture/legacy.tmp" "$fixture/legacy" 0 + Assert ($LASTEXITCODE -eq 0 -and (Test-Path "$fixture/legacy")) 'Original helper invocation broke' + + $failed = $false + try { Run-Deploy @{} @() 1 } catch { $failed = $_.Exception.Message -like '*build failed*' } + Assert $failed 'Build failure did not abort deployment' + Assert ($global:DeployTest.Uploads.Count -eq 0 -and $null -eq $global:DeployTest.Command) 'Failed build attempted deployment' + Write-Host 'dev_push tests passed' +} finally { + Remove-Variable DeployTest -Scope Global -ErrorAction SilentlyContinue + Remove-Item -Recurse -Force $fixture +} diff --git a/scripts/tests/ssh_stdin.ps1 b/scripts/tests/ssh_stdin.ps1 new file mode 100644 index 0000000..5416e8c --- /dev/null +++ b/scripts/tests/ssh_stdin.ps1 @@ -0,0 +1,34 @@ +#!/usr/bin/env pwsh +# Run with pwsh -NoProfile -File scripts/tests/ssh_stdin.ps1 on Linux/WSL. +$ErrorActionPreference = 'Stop' +. "$PSScriptRoot/../lib.ps1" + +# Exercise the real native-command pipeline without contacting a server. +$body = "printf '%s\n' `"owner's directory`"`nprintf '%s\n' 'second line'" +$output = @(Invoke-Ext -Exe sh -Arguments @('-s') -Label test -Stdin $body) +if (($output -join '|') -cne "owner's directory|second line") { throw 'stdin script changed in transit' } +$output = @(Invoke-Ext -Exe sh -Arguments @('-c', 'printf command-only') -Label test) +if (($output -join '') -cne 'command-only') { throw 'Command-only execution changed' } +$failed = $false +try { Invoke-Ext -Exe sh -Arguments @('-s') -Label test -Stdin 'exit 7' } catch { $failed = $true } +if (-not $failed) { throw 'stdin execution swallowed a failure' } + +# Test SSH argument and stdin forwarding independently of native SSH availability. +function Test-IsWsl { return $true } +function Get-SshpassCommand { return $script:Sshpass } +function Invoke-Ext { + param($Exe, $Arguments, $Label, [string]$Stdin) + $script:Call = @{ Exe = $Exe; Arguments = $Arguments; Stdin = $Stdin; HasStdin = $PSBoundParameters.ContainsKey('Stdin') } +} +$script:Sshpass = $null +Invoke-Ssh -Cmd 'sh -s' -User root -Remote test -Stdin "printf ok`r`n" +if ($script:Call.Exe -ne 'ssh' -or $script:Call.Stdin.Contains("`r") -or -not $script:Call.HasStdin) { + throw 'SSH stdin was not normalized and forwarded' +} +if (($script:Call.Arguments[-2..-1] -join '|') -ne 'root@test|sh -s') { throw 'SSH command changed' } +Invoke-Ssh -Cmd 'true' -User root -Remote test +if ($script:Call.HasStdin) { throw 'Command-only SSH unexpectedly redirects stdin' } +$script:Sshpass = '/fake/sshpass' +Invoke-Ssh -Cmd 'sh -s' -User root -Remote test -Pass test-only -Stdin $body +if ($script:Call.Exe -ne '/fake/sshpass' -or $script:Call.Stdin -cne $body) { throw 'sshpass lost stdin' } +Write-Host 'SSH stdin tests passed'