forgedock 1.32.1 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/outcome-codes.mjs +7 -3
- package/bin/engine/phases.mjs +3 -3
- package/bin/engine-cli.mjs +56 -11
- package/bin/forgedock.mjs +21 -1
- package/bin/lean/basesync.mjs +98 -0
- package/bin/lean/basesync.test.mjs +151 -0
- package/bin/lean/batch.test.mjs +103 -2
- package/bin/lean/budget.mjs +7 -1
- package/bin/lean/budget.test.mjs +9 -1
- package/bin/lean/conformance.mjs +46 -10
- package/bin/lean/conformance.test.mjs +50 -2
- package/bin/lean/depcache.mjs +23 -3
- package/bin/lean/depcache.test.mjs +42 -1
- package/bin/lean/effects.mjs +1 -1
- package/bin/lean/execution.mjs +102 -12
- package/bin/lean/execution.test.mjs +101 -1
- package/bin/lean/hostkill.mjs +8 -0
- package/bin/lean/lean.mjs +49 -21
- package/bin/lean/lean.test.mjs +133 -7
- package/bin/lean/lightlane.mjs +38 -13
- package/bin/lean/lightlane.test.mjs +35 -1
- package/bin/lean/orchestrate.mjs +63 -17
- package/bin/lean/prompts.mjs +19 -9
- package/bin/lean/recovery.test.mjs +26 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +74 -1
- package/bin/lean/sandbox-remote.mjs +40 -7
- package/bin/lean/seqlease.mjs +49 -2
- package/bin/lean/seqlease.test.mjs +14 -0
- package/bin/lean/sequence.mjs +54 -6
- package/bin/lean/sequence.test.mjs +83 -1
- package/bin/pool/agent.mjs +435 -0
- package/bin/pool/cli.mjs +110 -0
- package/bin/pool/docker.mjs +56 -0
- package/bin/pool/http.mjs +55 -0
- package/bin/pool/router.mjs +299 -0
- package/bin/pool/store.mjs +25 -0
- package/bin/pool/version.mjs +44 -0
- package/bin/review-staging.mjs +14 -2
- package/bin/runner.mjs +1 -0
- package/bin/stats/aggregate.mjs +6 -0
- package/bin/stats/efficiency.mjs +103 -17
- package/bin/stats/efficiency.test.mjs +103 -4
- package/bin/stats/index.mjs +40 -6
- package/bin/stats/local-source.mjs +1 -1
- package/bin/stats/pool.mjs +51 -0
- package/bin/stats/pool.test.mjs +39 -0
- package/bin/stats/query.mjs +2 -2
- package/bin/stats/stats.test.mjs +15 -1
- package/commands/orchestrate.md +3 -3
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-staging.md +1 -1
- package/docs/CONFIG.md +23 -9
- package/docs/OUTCOME-CODES.md +5 -3
- package/docs/STATS.md +36 -1
- package/package.json +2 -1
- package/pool/README.md +13 -0
- package/pool/image/Dockerfile +46 -0
- package/pool/image/claude-shim +8 -0
- package/pool/image/cs-cred +12 -0
- package/pool/image/entrypoint.sh +61 -0
- package/pool/image/gh-shim +6 -0
- package/pool/install/install-agent-linux.sh +53 -0
- package/pool/install/install-agent.ps1 +225 -0
- package/pool/install/install-agent.sh +101 -0
- package/pool/install/install-router.sh +67 -0
- package/pool/install/keepalive.ps1 +36 -0
- package/pool/install/uninstall-agent.ps1 +20 -0
- package/scripts/hotspots.sh +33 -21
- package/scripts/hotspots.test.sh +22 -3
- package/scripts/review-finalize.sh +17 -1
- package/scripts/review-finalize.test.sh +41 -0
- package/scripts/review-pr-domain-map.test.sh +2 -2
- package/scripts/review-pr-staging-scope.test.sh +4 -2
- package/scripts/review-prepare.sh +62 -27
- package/scripts/review-prepare.test.sh +53 -0
- package/scripts/review-staging-pre.sh +29 -9
- package/scripts/review-staging-pre.test.sh +18 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
# Native Linux (systemd) agent install: the Windows installer without the VM layer. Resolves host settings, then runs install-agent.sh,
|
|
5
|
+
# which installs docker, node, enrolls the agent, builds the lane image and installs the computeshare-agent systemd unit.
|
|
6
|
+
# Usage: install-agent-linux.sh --router URL --token CODE [--name N] [--port P] [--cpus N] [--mem GB] [--bundle FILE] [--dry-run]
|
|
7
|
+
# Env equivalents: ROUTER_URL ENROLL_TOKEN AGENT_NAME AGENT_PORT ALLOW_CPUS ALLOW_MEM_GB BUNDLE AGENT_URL
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
DRY=0
|
|
10
|
+
while [ $# -gt 0 ]; do
|
|
11
|
+
case "$1" in
|
|
12
|
+
--router) ROUTER_URL="${2:?}"; shift 2 ;;
|
|
13
|
+
--token) ENROLL_TOKEN="${2:?}"; shift 2 ;;
|
|
14
|
+
--name) AGENT_NAME="${2:?}"; shift 2 ;;
|
|
15
|
+
--port) AGENT_PORT="${2:?}"; shift 2 ;;
|
|
16
|
+
--cpus) ALLOW_CPUS="${2:?}"; shift 2 ;;
|
|
17
|
+
--mem) ALLOW_MEM_GB="${2:?}"; shift 2 ;;
|
|
18
|
+
--bundle) BUNDLE="${2:?}"; shift 2 ;;
|
|
19
|
+
--dry-run) DRY=1; shift ;;
|
|
20
|
+
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
21
|
+
esac
|
|
22
|
+
done
|
|
23
|
+
: "${ROUTER_URL:?--router or ROUTER_URL required}" "${ENROLL_TOKEN:?--token or ENROLL_TOKEN required}"
|
|
24
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
25
|
+
PORT="${AGENT_PORT:-7711}"
|
|
26
|
+
AGENT_NAME="${AGENT_NAME:-$(hostname | tr 'A-Z' 'a-z')}"
|
|
27
|
+
# Same defaults as install-agent.ps1: logical CPUs minus 4 (min 2); half of physical RAM (min 4 GB).
|
|
28
|
+
CPUS="${ALLOW_CPUS:-0}"
|
|
29
|
+
if [ "$CPUS" -le 0 ]; then CPUS=$(( $(nproc) - 4 )); [ "$CPUS" -ge 2 ] || CPUS=2; fi
|
|
30
|
+
MEM="${ALLOW_MEM_GB:-0}"
|
|
31
|
+
if [ "$MEM" -le 0 ]; then MEM=$(( $(awk '/MemTotal/ {print int($2/1024/1024)}' /proc/meminfo) / 2 )); [ "$MEM" -ge 4 ] || MEM=4; fi
|
|
32
|
+
if [ -z "${AGENT_URL:-}" ]; then
|
|
33
|
+
if [ "$DRY" = 1 ] && ! command -v tailscale >/dev/null; then TS_IP="<tailscale-ip>"; else
|
|
34
|
+
TS_IP="$(tailscale ip -4 | head -1)"
|
|
35
|
+
[[ "$TS_IP" == 100.* ]] || { echo "no Tailscale IPv4 found (got '$TS_IP')" >&2; exit 1; }
|
|
36
|
+
fi
|
|
37
|
+
AGENT_URL="http://${TS_IP}:${PORT}"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ "$DRY" = 1 ]; then
|
|
41
|
+
echo "router=$ROUTER_URL name=$AGENT_NAME url=$AGENT_URL port=$PORT cpus=$CPUS mem_gb=$MEM bundle=${BUNDLE:-<download>} token=<redacted>"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
[ "$(id -u)" = 0 ] || { echo "run as root" >&2; exit 1; }
|
|
45
|
+
|
|
46
|
+
if [ -z "${BUNDLE:-}" ]; then
|
|
47
|
+
BUNDLE="$(mktemp /tmp/computeshare-bundle.XXXXXX.tar.gz)"
|
|
48
|
+
trap 'rm -f "$BUNDLE"' EXIT
|
|
49
|
+
curl -fsS "${ROUTER_URL}/bootstrap/bundle.tar.gz?token=${ENROLL_TOKEN}" -o "$BUNDLE"
|
|
50
|
+
fi
|
|
51
|
+
export ROUTER_URL ENROLL_TOKEN AGENT_NAME AGENT_URL BUNDLE
|
|
52
|
+
export AGENT_PORT="$PORT" ALLOW_CPUS="$CPUS" ALLOW_MEM_GB="$MEM"
|
|
53
|
+
bash "$HERE/install-agent.sh"
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
<#
|
|
4
|
+
.SYNOPSIS
|
|
5
|
+
Installs a computeshare agent on a Windows host: a dedicated WSL2 distro ("computeshare") running Docker and the agent,
|
|
6
|
+
reachable on this machine's Tailscale IP, with a CPU/RAM ceiling enforced by the WSL VM itself.
|
|
7
|
+
.NOTES
|
|
8
|
+
Run in an elevated PowerShell. Normally started by the router's bootstrap one-liner (computeshare install-command).
|
|
9
|
+
#>
|
|
10
|
+
[CmdletBinding()]
|
|
11
|
+
param(
|
|
12
|
+
[Parameter(Mandatory)] [string] $RouterUrl,
|
|
13
|
+
[Parameter(Mandatory)] [string] $EnrollToken,
|
|
14
|
+
[Parameter(Mandatory)] [string] $BundlePath,
|
|
15
|
+
[string] $AgentName = $env:COMPUTERNAME.ToLower(),
|
|
16
|
+
[int] $Port = 7711,
|
|
17
|
+
[int] $AllowCpus = 0, # 0 = logical CPUs minus 4 (min 2)
|
|
18
|
+
[int] $AllowMemGb = 0, # 0 = half of physical RAM (min 4)
|
|
19
|
+
[string] $Distro = 'computeshare',
|
|
20
|
+
[switch] $Force # kept for compatibility; the install never prompts
|
|
21
|
+
)
|
|
22
|
+
$ErrorActionPreference = 'Stop'
|
|
23
|
+
$env:WSL_UTF8 = '1'
|
|
24
|
+
function Say($m) { Write-Host "[computeshare] $m" -ForegroundColor Cyan }
|
|
25
|
+
function Fail($m) { Write-Host "[computeshare] $m" -ForegroundColor Red; exit 1 }
|
|
26
|
+
|
|
27
|
+
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
|
|
28
|
+
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Fail 'Run this from an elevated (Administrator) PowerShell.' }
|
|
29
|
+
|
|
30
|
+
$installDir = Join-Path $env:ProgramData 'computeshare'
|
|
31
|
+
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
|
|
32
|
+
|
|
33
|
+
# --- Tailscale ---------------------------------------------------------------------------------------------------
|
|
34
|
+
$ts = (Get-Command tailscale.exe -ErrorAction SilentlyContinue).Source
|
|
35
|
+
if (-not $ts) { $ts = Join-Path $env:ProgramFiles 'Tailscale\tailscale.exe' }
|
|
36
|
+
if (-not (Test-Path $ts)) { Fail 'Tailscale is not installed.' }
|
|
37
|
+
$tsIp = (& $ts ip -4 | Select-Object -First 1).Trim()
|
|
38
|
+
if ($tsIp -notmatch '^100\.') { Fail "Could not read this machine's Tailscale IPv4 (got '$tsIp')." }
|
|
39
|
+
Say "Tailscale IP $tsIp"
|
|
40
|
+
|
|
41
|
+
# --- Resource ceiling --------------------------------------------------------------------------------------------
|
|
42
|
+
$threads = [Environment]::ProcessorCount
|
|
43
|
+
Add-Type -AssemblyName Microsoft.VisualBasic
|
|
44
|
+
$ramGb = [math]::Floor((New-Object Microsoft.VisualBasic.Devices.ComputerInfo).TotalPhysicalMemory / 1GB)
|
|
45
|
+
if ($AllowCpus -le 0) { $AllowCpus = [math]::Max(2, $threads - 4) }
|
|
46
|
+
if ($AllowMemGb -le 0) { $AllowMemGb = [math]::Max(4, [math]::Floor($ramGb / 2)) }
|
|
47
|
+
$vmMemGb = $AllowMemGb + 2 # docker + agent overhead inside the VM
|
|
48
|
+
Say "Host: $threads threads, $ramGb GB RAM. Lanes get $AllowCpus CPUs / $AllowMemGb GB (WSL VM capped at $AllowCpus CPUs / $vmMemGb GB)."
|
|
49
|
+
|
|
50
|
+
# --- WSL ---------------------------------------------------------------------------------------------------------
|
|
51
|
+
# wsl.exe can hang when the WSL service is wedged. Every call runs in a job with a timeout; on a hang the service is
|
|
52
|
+
# restarted once and the call retried, so the install never needs a manual fix.
|
|
53
|
+
function Reset-WslService {
|
|
54
|
+
Say 'WSL service is not responding: restarting it'
|
|
55
|
+
Get-Process wsl, wslhost, wslrelay, wslservice -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
56
|
+
Start-Sleep -Seconds 2
|
|
57
|
+
Start-Service WSLService -ErrorAction SilentlyContinue
|
|
58
|
+
Start-Sleep -Seconds 3
|
|
59
|
+
}
|
|
60
|
+
function Invoke-Wsl([string[]] $WslArgs, [int] $TimeoutSec = 120, [switch] $Optional) {
|
|
61
|
+
for ($try = 1; $try -le 2; $try++) {
|
|
62
|
+
$job = Start-Job -ScriptBlock { param($a) $env:WSL_UTF8 = '1'; $o = & wsl.exe @a 2>&1 | Out-String; [pscustomobject]@{ rc = $LASTEXITCODE; out = $o } } -ArgumentList (, $WslArgs)
|
|
63
|
+
if (Wait-Job $job -Timeout $TimeoutSec) { $r = Receive-Job $job; Remove-Job $job -Force; return $r }
|
|
64
|
+
Stop-Job $job -ErrorAction SilentlyContinue; Remove-Job $job -Force -ErrorAction SilentlyContinue
|
|
65
|
+
if ($Optional) { return $null }
|
|
66
|
+
if ($try -eq 1) { Reset-WslService }
|
|
67
|
+
}
|
|
68
|
+
Fail "wsl $($WslArgs -join ' ') still hangs after restarting the WSL service. Reboot this PC, then run a fresh install command."
|
|
69
|
+
}
|
|
70
|
+
Say 'Checking WSL'
|
|
71
|
+
$v = Invoke-Wsl @('--version') 60
|
|
72
|
+
if ($v.rc -ne 0) {
|
|
73
|
+
Say 'WSL is missing or the old inbox version: installing it. Reboot when it finishes, then run a fresh install command.'
|
|
74
|
+
$r = Invoke-Wsl @('--install', '--no-distribution', '--web-download') 1200
|
|
75
|
+
Write-Host $r.out
|
|
76
|
+
exit 3010
|
|
77
|
+
}
|
|
78
|
+
$wslVer = [version]'0.0'
|
|
79
|
+
if ($v.out -match 'WSL[^:\r\n]*:\s*([0-9]+(\.[0-9]+)+)') { $wslVer = [version]$Matches[1] }
|
|
80
|
+
Say "WSL $wslVer"
|
|
81
|
+
if ($wslVer -lt [version]'2.0') {
|
|
82
|
+
# mirrored networking and the current --import behaviour need WSL 2.0+; only then is an update worth the wait
|
|
83
|
+
Say 'Updating WSL (up to 10 min; skipped if it stalls)'
|
|
84
|
+
$u = Invoke-Wsl @('--update', '--web-download') 600 -Optional
|
|
85
|
+
if ($null -eq $u) { Say 'wsl --update stalled; continuing with the installed version.' }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# WSL 2 needs the Virtual Machine Platform feature and CPU virtualisation enabled in firmware (wsl.exe alone is not enough).
|
|
89
|
+
$cpu = Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
90
|
+
$hvPresent = [bool](Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue).HypervisorPresent
|
|
91
|
+
if (-not $hvPresent -and $cpu -and $cpu.VirtualizationFirmwareEnabled -eq $false) {
|
|
92
|
+
Fail 'CPU virtualisation is disabled in the BIOS/UEFI (Intel VT-x / AMD SVM). Enable it in the firmware setup, boot Windows, then run a fresh install command.'
|
|
93
|
+
}
|
|
94
|
+
$needReboot = $false
|
|
95
|
+
foreach ($f in 'VirtualMachinePlatform', 'Microsoft-Windows-Subsystem-Linux') {
|
|
96
|
+
$st = (Get-WindowsOptionalFeature -Online -FeatureName $f -ErrorAction SilentlyContinue).State
|
|
97
|
+
if ($st -and $st -ne 'Enabled') {
|
|
98
|
+
Say "Enabling Windows feature $f"
|
|
99
|
+
$r = Enable-WindowsOptionalFeature -Online -FeatureName $f -All -NoRestart -ErrorAction Stop
|
|
100
|
+
if ($r.RestartNeeded) { $needReboot = $true }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if ($needReboot) {
|
|
104
|
+
Say 'Windows features for WSL 2 were enabled. Reboot this PC, then run a fresh install command (computeshare install-command).'
|
|
105
|
+
exit 3010
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
$build = [int](Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').CurrentBuildNumber
|
|
109
|
+
$mirrored = $build -ge 22621
|
|
110
|
+
Say ("Networking: " + ($(if ($mirrored) { 'mirrored (agent listens on the Tailscale IP directly)' } else { 'NAT + portproxy (Windows 10 / older 11)' })))
|
|
111
|
+
|
|
112
|
+
# .wslconfig is per Windows user and applies to every distro: merge our keys, keep everything else.
|
|
113
|
+
$wslConfig = Join-Path $env:USERPROFILE '.wslconfig'
|
|
114
|
+
$lines = @(); if (Test-Path $wslConfig) { $lines = Get-Content $wslConfig }
|
|
115
|
+
$want = [ordered]@{ processors = "$AllowCpus"; memory = "${vmMemGb}GB"; vmIdleTimeout = '-1' }
|
|
116
|
+
if ($mirrored) { $want['networkingMode'] = 'mirrored' }
|
|
117
|
+
$out = New-Object System.Collections.Generic.List[string]; $inWsl2 = $false; $seen = @{}
|
|
118
|
+
foreach ($l in $lines) {
|
|
119
|
+
if ($l -match '^\s*\[(.+)\]\s*$') {
|
|
120
|
+
if ($inWsl2) { foreach ($k in $want.Keys) { if (-not $seen[$k]) { $out.Add("$k=$($want[$k])"); $seen[$k] = $true } } }
|
|
121
|
+
$inWsl2 = ($Matches[1].Trim().ToLower() -eq 'wsl2')
|
|
122
|
+
$out.Add($l); continue
|
|
123
|
+
}
|
|
124
|
+
if ($inWsl2 -and $l -match '^\s*([A-Za-z]+)\s*=') {
|
|
125
|
+
$k = $want.Keys | Where-Object { $_ -ieq $Matches[1] } | Select-Object -First 1
|
|
126
|
+
if ($k) { $out.Add("$k=$($want[$k])"); $seen[$k] = $true; continue }
|
|
127
|
+
}
|
|
128
|
+
$out.Add($l)
|
|
129
|
+
}
|
|
130
|
+
if ($inWsl2) { foreach ($k in $want.Keys) { if (-not $seen[$k]) { $out.Add("$k=$($want[$k])"); $seen[$k] = $true } } }
|
|
131
|
+
if (-not ($out | Where-Object { $_ -match '^\s*\[wsl2\]\s*$' })) { $out.Add('[wsl2]'); foreach ($k in $want.Keys) { $out.Add("$k=$($want[$k])") } }
|
|
132
|
+
$changed = (($lines -join "`n") -ne ($out -join "`n"))
|
|
133
|
+
if ($changed) {
|
|
134
|
+
if (Test-Path $wslConfig) { Copy-Item $wslConfig "$wslConfig.computeshare-backup" -Force }
|
|
135
|
+
Set-Content -Path $wslConfig -Value $out -Encoding ascii
|
|
136
|
+
Say "Updated $wslConfig (backup: .wslconfig.computeshare-backup). NOTE: these limits apply to all your WSL distros."
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# --- Dedicated distro -------------------------------------------------------------------------------------------
|
|
140
|
+
$distros = (Invoke-Wsl @('-l', '-q') 60).out -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
|
141
|
+
if ($distros -notcontains $Distro) {
|
|
142
|
+
$base = 'https://cloud-images.ubuntu.com/wsl/releases/24.04/current'
|
|
143
|
+
$file = 'ubuntu-noble-wsl-amd64-wsl.rootfs.tar.gz'
|
|
144
|
+
$tar = Join-Path $installDir $file
|
|
145
|
+
# SHA256SUMS is served without a content type, so PowerShell 5.1 would hand back bytes: save it and read it as text.
|
|
146
|
+
$sumsFile = Join-Path $installDir 'SHA256SUMS'
|
|
147
|
+
Invoke-WebRequest -UseBasicParsing "$base/SHA256SUMS" -OutFile $sumsFile
|
|
148
|
+
$expected = ((Get-Content $sumsFile | Where-Object { $_ -match [regex]::Escape($file) } | Select-Object -First 1) -split '\s+')[0]
|
|
149
|
+
if (-not $expected) { Fail "no checksum for $file in SHA256SUMS" }
|
|
150
|
+
$actual = if (Test-Path $tar) { (Get-FileHash -Algorithm SHA256 $tar).Hash.ToLower() } else { '' }
|
|
151
|
+
if ($actual -ne $expected.ToLower()) {
|
|
152
|
+
Say "Downloading Ubuntu 24.04 WSL rootfs"
|
|
153
|
+
Invoke-WebRequest -UseBasicParsing "$base/$file" -OutFile $tar
|
|
154
|
+
$actual = (Get-FileHash -Algorithm SHA256 $tar).Hash.ToLower()
|
|
155
|
+
if ($actual -ne $expected.ToLower()) { Remove-Item $tar -Force; Fail "rootfs checksum mismatch ($actual vs $expected)" }
|
|
156
|
+
} else { Say 'Reusing the already downloaded rootfs (checksum verified)' }
|
|
157
|
+
Say "Importing WSL distro '$Distro'"
|
|
158
|
+
$r = Invoke-Wsl @('--import', $Distro, (Join-Path $installDir 'wsl'), $tar, '--version', '2') 900
|
|
159
|
+
if ($r.rc -ne 0) {
|
|
160
|
+
if ($r.out -match 'HCS_E_HYPERV_NOT_INSTALLED|Virtual Machine Platform') { Fail "WSL 2 cannot start a VM here: enable virtualisation (Intel VT-x / AMD SVM) in the BIOS/UEFI, reboot, and run a fresh install command. Details: $($r.out.Trim())" }
|
|
161
|
+
Fail "wsl --import failed: $($r.out)"
|
|
162
|
+
}
|
|
163
|
+
Remove-Item $tar -Force
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# systemd for docker + the agent; no Windows PATH or Windows binaries inside the lane distro.
|
|
167
|
+
$wslConf = "[boot]\nsystemd=true\n\n[network]\nhostname=$AgentName-cs\n\n[interop]\nappendWindowsPath=false\n"
|
|
168
|
+
$before = (Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'cat', '/etc/wsl.conf') 120).out
|
|
169
|
+
[void](Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'sh', '-c', "printf '$wslConf' > /etc/wsl.conf") 120)
|
|
170
|
+
$after = (Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'cat', '/etc/wsl.conf') 120).out
|
|
171
|
+
$confChanged = ($before -ne $after)
|
|
172
|
+
|
|
173
|
+
if ($changed -or $confChanged -or -not ((Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'ps', '-p', '1', '-o', 'comm=') 120).out -match 'systemd')) {
|
|
174
|
+
Say 'Restarting WSL to apply the settings (this briefly stops every running WSL distro)'
|
|
175
|
+
[void](Invoke-Wsl @('--shutdown') 120)
|
|
176
|
+
Start-Sleep -Seconds 3
|
|
177
|
+
$p1 = (Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'ps', '-p', '1', '-o', 'comm=') 180).out
|
|
178
|
+
if ($p1 -notmatch 'systemd') { Fail "systemd did not start in the '$Distro' distro (PID 1: $($p1.Trim()))." }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# --- Firewall ------------------------------------------------------------------------------------------------------
|
|
182
|
+
# Idempotent: re-runs replace the rules (a leftover rule must never stop the install).
|
|
183
|
+
Remove-NetFirewallRule -Name 'computeshare-agent' -ErrorAction SilentlyContinue
|
|
184
|
+
New-NetFirewallRule -Name 'computeshare-agent' -DisplayName 'computeshare agent (Tailscale only)' -Direction Inbound -Protocol TCP -LocalPort $Port -InterfaceAlias 'Tailscale' -Action Allow | Out-Null
|
|
185
|
+
if ($mirrored -and (Get-Command New-NetFirewallHyperVRule -ErrorAction SilentlyContinue)) {
|
|
186
|
+
$wslCreator = '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}'
|
|
187
|
+
# Hyper-V rules are not always addressable by the -Name they were created with: match ours by display name or port.
|
|
188
|
+
Get-NetFirewallHyperVRule -ErrorAction SilentlyContinue |
|
|
189
|
+
Where-Object { $_.DisplayName -like 'computeshare*' -or $_.Name -like '*computeshare*' -or ("$($_.LocalPorts)" -eq "$Port" -and "$($_.VMCreatorId)" -eq $wslCreator) } |
|
|
190
|
+
ForEach-Object { Remove-NetFirewallHyperVRule -Name $_.Name -ErrorAction SilentlyContinue }
|
|
191
|
+
try {
|
|
192
|
+
New-NetFirewallHyperVRule -Name "computeshare-agent-$Port" -DisplayName 'computeshare agent' -Direction Inbound -VMCreatorId $wslCreator -Protocol TCP -LocalPorts $Port -Action Allow -ErrorAction Stop | Out-Null
|
|
193
|
+
} catch {
|
|
194
|
+
Say "Hyper-V firewall rule not created ($($_.Exception.Message.Trim())); continuing, reachability is checked at the end."
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# --- Agent inside the distro --------------------------------------------------------------------------------------
|
|
199
|
+
$bundleWsl = (Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'wslpath', '-a', ($BundlePath -replace '\\', '/')) 60).out.Trim()
|
|
200
|
+
$r = Invoke-Wsl @('-d', $Distro, '-u', 'root', '--', 'sh', '-c', "tar -xzf '$bundleWsl' -C /tmp pool/install/install-agent.sh && cp '$bundleWsl' /tmp/computeshare-bundle.tar.gz") 120
|
|
201
|
+
if ($r.rc -ne 0) { Fail "could not stage the bundle in the distro: $($r.out)" }
|
|
202
|
+
$envs = "ROUTER_URL='$RouterUrl' ENROLL_TOKEN='$EnrollToken' AGENT_NAME='$AgentName' AGENT_URL='http://${tsIp}:$Port' AGENT_PORT='$Port' ALLOW_CPUS='$AllowCpus' ALLOW_MEM_GB='$AllowMemGb' BUNDLE=/tmp/computeshare-bundle.tar.gz"
|
|
203
|
+
Say 'Installing Docker, Node and the agent inside the distro (first run builds the lane image; this takes a while)'
|
|
204
|
+
& wsl.exe -d $Distro -u root -- bash -c "$envs bash /tmp/pool/install/install-agent.sh"
|
|
205
|
+
if ($LASTEXITCODE -ne 0) { Fail 'Linux-side install failed (see output above).' }
|
|
206
|
+
|
|
207
|
+
# --- Keep-alive task: keeps the distro up, holds off sleep while lanes run, refreshes portproxy on NAT -----------------
|
|
208
|
+
$keep = Join-Path $installDir 'keepalive.ps1'
|
|
209
|
+
Copy-Item (Join-Path $PSScriptRoot 'keepalive.ps1') $keep -Force
|
|
210
|
+
$taskArgs = "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$keep`" -Distro $Distro -Port $Port -TailscaleIp $tsIp" + $(if ($mirrored) { ' -Mirrored' } else { '' })
|
|
211
|
+
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $taskArgs
|
|
212
|
+
$trigger = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME"
|
|
213
|
+
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)
|
|
214
|
+
Register-ScheduledTask -TaskName 'computeshare-keepalive' -Action $action -Trigger $trigger -Settings $settings -Description 'computeshare agent keep-alive' -Force | Out-Null
|
|
215
|
+
Start-ScheduledTask -TaskName 'computeshare-keepalive'
|
|
216
|
+
|
|
217
|
+
Start-Sleep -Seconds 5
|
|
218
|
+
try {
|
|
219
|
+
# Windows cannot reach its own Tailscale IP through the mirrored WSL network (no hairpin), so check via loopback;
|
|
220
|
+
# the router reaches the agent on the Tailscale IP.
|
|
221
|
+
$h = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/health" -TimeoutSec 10
|
|
222
|
+
Say "Agent '$AgentName' is up on http://${tsIp}:$Port : $($h.cpus_total) CPUs / $($h.mem_total_gb) GB reserved for lanes."
|
|
223
|
+
} catch {
|
|
224
|
+
Say "Installed, but http://${tsIp}:$Port/health is not answering yet. Check: wsl -d $Distro -u root -- journalctl -u computeshare-agent -n 50"
|
|
225
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
# Linux side of the agent install. Runs as root inside the dedicated `computeshare` WSL distro (or any systemd Linux host).
|
|
5
|
+
# Required env: ROUTER_URL ENROLL_TOKEN AGENT_NAME AGENT_URL ALLOW_CPUS ALLOW_MEM_GB BUNDLE
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
: "${ROUTER_URL:?}" "${ENROLL_TOKEN:?}" "${AGENT_NAME:?}" "${AGENT_URL:?}" "${ALLOW_CPUS:?}" "${ALLOW_MEM_GB:?}" "${BUNDLE:?}"
|
|
8
|
+
# Never resolve tools through the Windows PATH that WSL appends (e.g. Docker Desktop's docker CLI).
|
|
9
|
+
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
10
|
+
NODE_VERSION=22.12.0
|
|
11
|
+
PORT="${AGENT_PORT:-7711}"
|
|
12
|
+
log() { echo "[computeshare] $*"; }
|
|
13
|
+
|
|
14
|
+
[ "$(id -u)" = 0 ] || { echo "run as root" >&2; exit 1; }
|
|
15
|
+
pidof systemd >/dev/null || [ "$(ps -p 1 -o comm=)" = systemd ] || { echo "systemd is not PID 1; enable it in /etc/wsl.conf and restart the distro" >&2; exit 1; }
|
|
16
|
+
|
|
17
|
+
log "base packages"
|
|
18
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
19
|
+
apt-get update -qq
|
|
20
|
+
apt-get install -y -qq ca-certificates curl git jq xz-utils >/dev/null
|
|
21
|
+
|
|
22
|
+
if [ ! -x /usr/bin/dockerd ]; then
|
|
23
|
+
log "docker engine"
|
|
24
|
+
install -d -m 0755 /etc/apt/keyrings
|
|
25
|
+
. /etc/os-release
|
|
26
|
+
DOCKER_DISTRO=ubuntu; [ "${ID:-}" = debian ] && DOCKER_DISTRO=debian
|
|
27
|
+
curl -fsSL "https://download.docker.com/linux/${DOCKER_DISTRO}/gpg" -o /etc/apt/keyrings/docker.asc
|
|
28
|
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/${DOCKER_DISTRO} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
|
|
29
|
+
apt-get update -qq
|
|
30
|
+
apt-get install -y -qq docker-ce docker-ce-cli containerd.io >/dev/null
|
|
31
|
+
fi
|
|
32
|
+
# WSL mirrored networking usually has no IPv6 route; dockerd would still dial registries over IPv6 and fail.
|
|
33
|
+
if grep -qi microsoft /proc/sys/kernel/osrelease && [ -z "$(ip -6 route show default 2>/dev/null)" ]; then
|
|
34
|
+
printf 'net.ipv6.conf.all.disable_ipv6=1\nnet.ipv6.conf.default.disable_ipv6=1\n' > /etc/sysctl.d/90-computeshare-noipv6.conf
|
|
35
|
+
sysctl -q -p /etc/sysctl.d/90-computeshare-noipv6.conf
|
|
36
|
+
systemctl restart docker 2>/dev/null || true
|
|
37
|
+
fi
|
|
38
|
+
systemctl enable --now docker >/dev/null
|
|
39
|
+
|
|
40
|
+
if [ ! -x /opt/node/bin/node ] || [ "$(/opt/node/bin/node --version)" != "v${NODE_VERSION}" ]; then
|
|
41
|
+
log "node ${NODE_VERSION}"
|
|
42
|
+
rm -rf /opt/node
|
|
43
|
+
case "$(uname -m)" in aarch64|arm64) NODE_ARCH=arm64 ;; *) NODE_ARCH=x64 ;; esac
|
|
44
|
+
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" | tar -xJ -C /opt
|
|
45
|
+
mv "/opt/node-v${NODE_VERSION}-linux-${NODE_ARCH}" /opt/node
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
log "agent code"
|
|
49
|
+
rm -rf /opt/computeshare.new && mkdir -p /opt/computeshare.new
|
|
50
|
+
tar -xzf "$BUNDLE" -C /opt/computeshare.new
|
|
51
|
+
rm -rf /opt/computeshare && mv /opt/computeshare.new /opt/computeshare
|
|
52
|
+
|
|
53
|
+
install -d -m 0700 /etc/computeshare /var/lib/computeshare
|
|
54
|
+
if [ ! -s /etc/computeshare/agent.token ]; then
|
|
55
|
+
log "enrolling ${AGENT_NAME} (${AGENT_URL}) with ${ROUTER_URL}"
|
|
56
|
+
resp=$(curl -fsS -X POST "${ROUTER_URL}/enroll" -H 'content-type: application/json' \
|
|
57
|
+
-d "$(jq -n --arg t "$ENROLL_TOKEN" --arg n "$AGENT_NAME" --arg u "$AGENT_URL" '{token:$t,name:$n,url:$u}')")
|
|
58
|
+
umask 077
|
|
59
|
+
echo "$resp" | jq -r .agent_token > /etc/computeshare/agent.token
|
|
60
|
+
[ -s /etc/computeshare/agent.token ] && [ "$(cat /etc/computeshare/agent.token)" != null ] || { echo "enrollment failed: $resp" >&2; rm -f /etc/computeshare/agent.token; exit 1; }
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
jq -n --arg name "$AGENT_NAME" --arg router "$ROUTER_URL" --argjson port "$PORT" --argjson cpus "$ALLOW_CPUS" --argjson mem "$ALLOW_MEM_GB" '{
|
|
64
|
+
name: $name, listen: {host: "0.0.0.0", port: $port}, token_file: "/etc/computeshare/agent.token",
|
|
65
|
+
router_url: $router, code_root: "/opt/computeshare",
|
|
66
|
+
state_dir: "/var/lib/computeshare",
|
|
67
|
+
allow_cpus: $cpus, allow_mem_gb: $mem, lease_ttl_s: 600, max_lifetime_s: 21600, keep_exited_s: 7200, reap_interval_s: 30
|
|
68
|
+
}' > /etc/computeshare/agent.json
|
|
69
|
+
chmod 0600 /etc/computeshare/agent.json
|
|
70
|
+
|
|
71
|
+
log "building lane image (first run takes several minutes)"
|
|
72
|
+
FD_VERSION="$(jq -r .version /opt/computeshare/package.json)"
|
|
73
|
+
docker build -q -t "forgedock-lane:${FD_VERSION}" --build-arg "FORGEDOCK_VERSION=${FD_VERSION}" /opt/computeshare/pool/image >/dev/null
|
|
74
|
+
|
|
75
|
+
cat > /etc/systemd/system/computeshare-agent.service <<EOF
|
|
76
|
+
[Unit]
|
|
77
|
+
Description=computeshare agent
|
|
78
|
+
After=docker.service network-online.target
|
|
79
|
+
Requires=docker.service
|
|
80
|
+
|
|
81
|
+
[Service]
|
|
82
|
+
ExecStart=/opt/node/bin/node /opt/computeshare/bin/pool/cli.mjs agent --config /etc/computeshare/agent.json
|
|
83
|
+
Restart=always
|
|
84
|
+
RestartSec=5
|
|
85
|
+
|
|
86
|
+
[Install]
|
|
87
|
+
WantedBy=multi-user.target
|
|
88
|
+
EOF
|
|
89
|
+
systemctl daemon-reload
|
|
90
|
+
systemctl enable computeshare-agent >/dev/null
|
|
91
|
+
systemctl restart computeshare-agent
|
|
92
|
+
|
|
93
|
+
for i in $(seq 1 20); do
|
|
94
|
+
if curl -fsS "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
|
|
95
|
+
log "agent healthy: $(curl -fsS "http://127.0.0.1:${PORT}/health")"
|
|
96
|
+
exit 0
|
|
97
|
+
fi
|
|
98
|
+
sleep 1
|
|
99
|
+
done
|
|
100
|
+
echo "agent did not become healthy; see: journalctl -u computeshare-agent" >&2
|
|
101
|
+
exit 1
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
# Installs the computeshare router as a systemd --user service on the host that runs ForgeDock.
|
|
5
|
+
# It listens only on this machine's Tailscale IP. Re-running keeps existing tokens and agent enrollments.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
8
|
+
CONF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/computeshare"
|
|
9
|
+
PORT="${ROUTER_PORT:-7710}"
|
|
10
|
+
TS_IP="$(tailscale ip -4 | head -1)"
|
|
11
|
+
[[ "$TS_IP" == 100.* ]] || { echo "no Tailscale IPv4 found" >&2; exit 1; }
|
|
12
|
+
NODE="$(command -v node)"
|
|
13
|
+
|
|
14
|
+
mkdir -p "$CONF_DIR" && chmod 700 "$CONF_DIR"
|
|
15
|
+
umask 077
|
|
16
|
+
for t in forgedock enroll; do
|
|
17
|
+
[ -s "$CONF_DIR/$t.token" ] || openssl rand -hex 32 > "$CONF_DIR/$t.token"
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
# repos: slug -> gitignored forge.yaml that lanes need (passed to the sandbox, never committed). Extend as needed.
|
|
21
|
+
# An existing router.json is kept (re-running the installer must not drop repos or settings added later).
|
|
22
|
+
[ -s "$CONF_DIR/router.json" ] || cat > "$CONF_DIR/router.json" <<EOF
|
|
23
|
+
{
|
|
24
|
+
"listen": { "host": "$TS_IP", "port": $PORT },
|
|
25
|
+
"public_url": "http://$TS_IP:$PORT",
|
|
26
|
+
"state_dir": "$CONF_DIR/state",
|
|
27
|
+
"lane": { "cpus": ${LANE_CPUS:-2}, "mem_gb": ${LANE_MEM_GB:-3} },
|
|
28
|
+
"lease_interval_s": 60,
|
|
29
|
+
"abandon_after_s": 1800,
|
|
30
|
+
"retry_after_s": 60,
|
|
31
|
+
"repos": {}
|
|
32
|
+
}
|
|
33
|
+
EOF
|
|
34
|
+
|
|
35
|
+
mkdir -p "$HOME/.config/systemd/user"
|
|
36
|
+
cat > "$HOME/.config/systemd/user/computeshare-router.service" <<EOF
|
|
37
|
+
[Unit]
|
|
38
|
+
Description=computeshare router
|
|
39
|
+
After=network-online.target tailscaled.service
|
|
40
|
+
|
|
41
|
+
[Service]
|
|
42
|
+
ExecStart=$NODE $REPO_ROOT/bin/pool/cli.mjs router --config $CONF_DIR/router.json
|
|
43
|
+
Restart=always
|
|
44
|
+
RestartSec=5
|
|
45
|
+
|
|
46
|
+
[Install]
|
|
47
|
+
WantedBy=default.target
|
|
48
|
+
EOF
|
|
49
|
+
systemctl --user daemon-reload
|
|
50
|
+
systemctl --user enable --now computeshare-router >/dev/null
|
|
51
|
+
systemctl --user restart computeshare-router
|
|
52
|
+
loginctl enable-linger "$USER" 2>/dev/null || true
|
|
53
|
+
|
|
54
|
+
# ForgeDock reads execution.token_env from the env, then the OS keychain (service forgedock, account = the name).
|
|
55
|
+
if command -v secret-tool >/dev/null; then
|
|
56
|
+
secret-tool store --label "forgedock COMPUTESHARE_TOKEN" service forgedock account COMPUTESHARE_TOKEN < "$CONF_DIR/forgedock.token" \
|
|
57
|
+
&& echo "[computeshare] stored the ForgeDock token in the keychain as COMPUTESHARE_TOKEN"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
for i in $(seq 1 15); do
|
|
61
|
+
curl -fsS "http://$TS_IP:$PORT/health" >/dev/null 2>&1 && break
|
|
62
|
+
sleep 1
|
|
63
|
+
done
|
|
64
|
+
echo "[computeshare] router: $(curl -fsS "http://$TS_IP:$PORT/health")"
|
|
65
|
+
echo
|
|
66
|
+
echo "Install an agent: run this in an elevated PowerShell on the Windows host:"
|
|
67
|
+
"$NODE" "$REPO_ROOT/bin/pool/cli.mjs" install-command --config "$CONF_DIR/router.json"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
# Runs at logon (scheduled task). Keeps the computeshare distro running, keeps Windows awake while lanes are active,
|
|
4
|
+
# and on NAT networking keeps a portproxy from the Tailscale IP to the distro's current address.
|
|
5
|
+
param([string] $Distro = 'computeshare', [int] $Port = 7711, [string] $TailscaleIp, [switch] $Mirrored)
|
|
6
|
+
$ErrorActionPreference = 'Continue'
|
|
7
|
+
$env:WSL_UTF8 = '1'
|
|
8
|
+
|
|
9
|
+
Add-Type -Namespace CS -Name Power -MemberDefinition '[DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint flags);'
|
|
10
|
+
$ES_CONTINUOUS = [uint32]'0x80000000'; $ES_SYSTEM_REQUIRED = [uint32]'0x00000001'
|
|
11
|
+
|
|
12
|
+
$holder = $null
|
|
13
|
+
while ($true) {
|
|
14
|
+
if (-not $holder -or $holder.HasExited) {
|
|
15
|
+
# A long-lived process keeps the distro (and its systemd services) from being stopped by WSL.
|
|
16
|
+
$holder = Start-Process -FilePath 'wsl.exe' -ArgumentList @('-d', $Distro, '-u', 'root', '--', 'sleep', 'infinity') -WindowStyle Hidden -PassThru
|
|
17
|
+
Start-Sleep -Seconds 5
|
|
18
|
+
}
|
|
19
|
+
if (-not $Mirrored -and $TailscaleIp) {
|
|
20
|
+
$wslIp = ((& wsl.exe -d $Distro -u root -- hostname -I) -split '\s+' | Where-Object { $_ -match '^\d+\.' } | Select-Object -First 1)
|
|
21
|
+
if ($wslIp) {
|
|
22
|
+
$cur = (netsh interface portproxy show v4tov4) -match [regex]::Escape("$TailscaleIp") -match "$Port"
|
|
23
|
+
if (-not ($cur -match [regex]::Escape($wslIp))) {
|
|
24
|
+
netsh interface portproxy delete v4tov4 listenaddress=$TailscaleIp listenport=$Port | Out-Null
|
|
25
|
+
netsh interface portproxy add v4tov4 listenaddress=$TailscaleIp listenport=$Port connectaddress=$wslIp connectport=$Port | Out-Null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
$active = 0
|
|
30
|
+
try { $active = [int](Invoke-RestMethod -Uri "http://127.0.0.1:$Port/health" -TimeoutSec 5).active } catch {
|
|
31
|
+
if ($TailscaleIp) { try { $active = [int](Invoke-RestMethod -Uri "http://${TailscaleIp}:$Port/health" -TimeoutSec 5).active } catch {} }
|
|
32
|
+
}
|
|
33
|
+
if ($active -gt 0) { [void][CS.Power]::SetThreadExecutionState($ES_CONTINUOUS -bor $ES_SYSTEM_REQUIRED) }
|
|
34
|
+
else { [void][CS.Power]::SetThreadExecutionState($ES_CONTINUOUS) }
|
|
35
|
+
Start-Sleep -Seconds 60
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
<#
|
|
4
|
+
.SYNOPSIS
|
|
5
|
+
Removes the computeshare agent from this Windows host: keep-alive task, firewall rules, portproxy, the dedicated WSL
|
|
6
|
+
distro (with every container, image and cache in it) and the .wslconfig changes (restored from the backup).
|
|
7
|
+
#>
|
|
8
|
+
param([string] $Distro = 'computeshare', [int] $Port = 7711)
|
|
9
|
+
$ErrorActionPreference = 'Continue'
|
|
10
|
+
$env:WSL_UTF8 = '1'
|
|
11
|
+
Unregister-ScheduledTask -TaskName 'computeshare-keepalive' -Confirm:$false -ErrorAction SilentlyContinue
|
|
12
|
+
Get-Process powershell -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match 'computeshare\\keepalive.ps1' } | Stop-Process -Force
|
|
13
|
+
Get-NetFirewallRule -Name 'computeshare-agent' -ErrorAction SilentlyContinue | Remove-NetFirewallRule
|
|
14
|
+
if (Get-Command Get-NetFirewallHyperVRule -ErrorAction SilentlyContinue) { Get-NetFirewallHyperVRule -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like 'computeshare*' -or $_.Name -like '*computeshare*' } | ForEach-Object { Remove-NetFirewallHyperVRule -Name $_.Name -ErrorAction SilentlyContinue } }
|
|
15
|
+
(netsh interface portproxy show v4tov4) -match " $Port " | ForEach-Object { $a = ($_ -split '\s+')[0]; if ($a) { netsh interface portproxy delete v4tov4 listenaddress=$a listenport=$Port | Out-Null } }
|
|
16
|
+
& wsl.exe --unregister $Distro
|
|
17
|
+
$cfg = Join-Path $env:USERPROFILE '.wslconfig'
|
|
18
|
+
if (Test-Path "$cfg.computeshare-backup") { Move-Item "$cfg.computeshare-backup" $cfg -Force; Write-Host 'Restored .wslconfig from backup (takes effect after wsl --shutdown).' }
|
|
19
|
+
Remove-Item -Recurse -Force (Join-Path $env:ProgramData 'computeshare') -ErrorAction SilentlyContinue
|
|
20
|
+
Write-Host '[computeshare] agent removed. Remove it from the router with: computeshare status (then delete it from router-state.json).'
|
package/scripts/hotspots.sh
CHANGED
|
@@ -7,14 +7,17 @@
|
|
|
7
7
|
#
|
|
8
8
|
# Usage: hotspots.sh [--base <ref>] [--] <path>...
|
|
9
9
|
# Config: forge.yaml maintenance.hotspot {enabled, file_lines, growth_lines, exclude_globs}; defaults true/2000/150/[]
|
|
10
|
-
# (also when forge.yaml or yq is missing).
|
|
10
|
+
# (also when forge.yaml or yq is missing); maintenance.generated_paths (forge#4519): shell globs of generated,
|
|
11
|
+
# bot-owned files, default [].
|
|
11
12
|
# Output (stdout, one JSON object):
|
|
12
|
-
# {"enabled","file_lines","growth_lines","
|
|
13
|
+
# {"enabled","file_lines","growth_lines","generated_paths",
|
|
14
|
+
# "files":[{path,lines,commits_90d,hotspot,over,generated,excluded,base_lines,growth}]}
|
|
13
15
|
# lines = `wc -l` of the working-tree file (0 when missing); commits_90d = `git rev-list --count --since=90.days HEAD`;
|
|
14
|
-
# hotspot = lines × commits_90d; over = lines > file_lines;
|
|
15
|
-
#
|
|
16
|
+
# hotspot = lines × commits_90d; over = lines > file_lines; generated = linguist-generated set by .gitattributes, a
|
|
17
|
+
# match of a generated_paths glob, or `@generated` / `DO NOT EDIT` in the first 5 lines of the working-tree file;
|
|
18
|
+
# excluded = generated, linguist-vendored, or a match of an exclude_glob (shell pattern, `*` crosses `/`). Without --base no blob is read and
|
|
16
19
|
# base_lines/growth are null; with --base they are the line count at git merge-base(<ref>, HEAD) (0 when absent there)
|
|
17
|
-
# and lines - base_lines. enabled:false prints files:[] and does no per-file work.
|
|
20
|
+
# and lines - base_lines. enabled:false prints files:[] (generated_paths still set) and does no per-file work.
|
|
18
21
|
# Exit: 0 ok | 2 usage or bad ref. bash 3.2 portable.
|
|
19
22
|
set -uo pipefail
|
|
20
23
|
|
|
@@ -31,19 +34,21 @@ while [ $# -gt 0 ]; do
|
|
|
31
34
|
done
|
|
32
35
|
|
|
33
36
|
# ---- config: maintenance.hotspot as JSON, {} when unreadable ----
|
|
34
|
-
CFG='{}'
|
|
37
|
+
CFG='{}'; GEN_CFG='[]'
|
|
35
38
|
if [ -f forge.yaml ] && command -v yq >/dev/null 2>&1; then
|
|
36
39
|
c=$(yq -o=json '.maintenance.hotspot // {}' forge.yaml 2>/dev/null) || c=""
|
|
37
40
|
printf '%s' "$c" | jq -e 'type == "object"' >/dev/null 2>&1 && CFG="$c"
|
|
41
|
+
g=$(yq -o=json '.maintenance.generated_paths // []' forge.yaml 2>/dev/null) || g=""
|
|
42
|
+
printf '%s' "$g" | jq -e 'type == "array"' >/dev/null 2>&1 && GEN_CFG="$g"
|
|
38
43
|
fi
|
|
44
|
+
GEN_CFG=$(printf '%s' "$GEN_CFG" | jq -c '[.[] | strings | select(length > 0)]')
|
|
39
45
|
CFG=$(printf '%s' "$CFG" | jq -c '
|
|
40
46
|
def pos($d): if type == "number" and . > 0 then floor else $d end;
|
|
41
47
|
{ enabled: (.enabled != false), file_lines: (.file_lines | pos(2000)), growth_lines: (.growth_lines | pos(150)),
|
|
42
48
|
exclude_globs: ([(.exclude_globs // [])[]? | strings]) }')
|
|
43
49
|
ENABLED=$(printf '%s' "$CFG" | jq -r '.enabled')
|
|
44
50
|
FILE_LINES=$(printf '%s' "$CFG" | jq -r '.file_lines')
|
|
45
|
-
|
|
46
|
-
head_json() { printf '%s' "$CFG" | jq -c --argjson files "$1" '{enabled, file_lines, growth_lines, files: $files}'; }
|
|
51
|
+
head_json() { printf '%s' "$CFG" | jq -c --argjson files "$1" --argjson gen "$GEN_CFG" '{enabled, file_lines, growth_lines, generated_paths: $gen, files: $files}'; }
|
|
47
52
|
[ "$ENABLED" = true ] || { head_json '[]'; exit 0; }
|
|
48
53
|
|
|
49
54
|
MB=""
|
|
@@ -53,6 +58,19 @@ if [ -n "$BASE" ]; then
|
|
|
53
58
|
fi
|
|
54
59
|
HAS_HEAD=0; git rev-parse --verify -q "HEAD^{commit}" >/dev/null && HAS_HEAD=1
|
|
55
60
|
GLOBS=$(printf '%s' "$CFG" | jq -r '.exclude_globs[]')
|
|
61
|
+
GEN_GLOBS=$(printf '%s' "$GEN_CFG" | jq -r '.[]')
|
|
62
|
+
# 0 when $1 matches a newline-separated list of shell patterns in $2 (`*` crosses `/`)
|
|
63
|
+
glob_hit() {
|
|
64
|
+
[ -n "$2" ] || return 1
|
|
65
|
+
while IFS= read -r g; do
|
|
66
|
+
[ -n "$g" ] || continue
|
|
67
|
+
# shellcheck disable=SC2254
|
|
68
|
+
case "$1" in $g) return 0 ;; esac
|
|
69
|
+
done <<EOF
|
|
70
|
+
$2
|
|
71
|
+
EOF
|
|
72
|
+
return 1
|
|
73
|
+
}
|
|
56
74
|
|
|
57
75
|
T=$(mktemp -d "${TMPDIR:-/tmp}/hotspots.XXXXXX") || die "mktemp failed"
|
|
58
76
|
trap 'rm -rf "$T"' EXIT
|
|
@@ -72,29 +90,23 @@ for p in "$@"; do
|
|
|
72
90
|
[ -n "$p" ] || continue
|
|
73
91
|
lines=0; [ -f "$p" ] && lines=$(wc -l < "$p" | tr -d ' ')
|
|
74
92
|
commits=$(cat "$T/c.$i" 2>/dev/null); case "$commits" in ''|*[!0-9]*) commits=0 ;; esac
|
|
75
|
-
excluded=false
|
|
76
|
-
if git check-attr linguist-generated
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# shellcheck disable=SC2254
|
|
81
|
-
case "$p" in $g) excluded=true; break ;; esac
|
|
82
|
-
done <<EOF
|
|
83
|
-
$GLOBS
|
|
84
|
-
EOF
|
|
85
|
-
fi
|
|
93
|
+
generated=false; excluded=false
|
|
94
|
+
if git check-attr linguist-generated -- "$p" 2>/dev/null | grep -qE ': (set|true)$' || glob_hit "$p" "$GEN_GLOBS" \
|
|
95
|
+
|| { [ -f "$p" ] && head -n 5 "$p" 2>/dev/null | grep -qE '@generated|DO NOT EDIT'; }; then generated=true; excluded=true; fi
|
|
96
|
+
if [ "$excluded" = false ] && git check-attr linguist-vendored -- "$p" 2>/dev/null | grep -qE ': (set|true)$'; then excluded=true; fi
|
|
97
|
+
if [ "$excluded" = false ] && glob_hit "$p" "$GLOBS"; then excluded=true; fi
|
|
86
98
|
base_lines=null; growth=null
|
|
87
99
|
if [ -n "$MB" ]; then
|
|
88
100
|
base_lines=0
|
|
89
101
|
git cat-file -e "$MB:$p" 2>/dev/null && base_lines=$(git show "$MB:$p" | wc -l | tr -d ' ')
|
|
90
102
|
growth=$((lines - base_lines))
|
|
91
103
|
fi
|
|
92
|
-
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$p" "$lines" "$commits" "$excluded" "$base_lines" "$growth" >> "$T/rows"
|
|
104
|
+
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$p" "$lines" "$commits" "$excluded" "$base_lines" "$growth" "$generated" >> "$T/rows"
|
|
93
105
|
done
|
|
94
106
|
|
|
95
107
|
FILES=$(jq -R -s -c --argjson fl "$FILE_LINES" '
|
|
96
108
|
[split("\n")[] | select(length > 0) | split("\t") |
|
|
97
109
|
{ path: .[0], lines: (.[1] | tonumber), commits_90d: (.[2] | tonumber) } as $r |
|
|
98
|
-
$r + { hotspot: ($r.lines * $r.commits_90d), over: ($r.lines > $fl), excluded: (.[3] == "true"),
|
|
110
|
+
$r + { hotspot: ($r.lines * $r.commits_90d), over: ($r.lines > $fl), generated: (.[6] == "true"), excluded: (.[3] == "true"),
|
|
99
111
|
base_lines: (.[4] | fromjson), growth: (.[5] | fromjson) }]' < "$T/rows")
|
|
100
112
|
head_json "$FILES"
|