win/lib/access.ps1

10.5 KB · 281 lines · text

1# brtwin -access wrapper (cross-platform relay compatible with brt on macOS/Linux)
2param(
3 [Parameter(Mandatory = $true, Position = 0)]
4 [string]$SubCmd,
5 [Parameter(ValueFromRemainingArguments = $true)]
6 [string[]]$Rest = @()
7)
8
9$ErrorActionPreference = "Continue"
10$Rest = @($Rest)
11
12$WinRoot = Split-Path $PSScriptRoot -Parent
13$Shared = Join-Path $PSScriptRoot "shared"
14$AccessPy = Join-Path $Shared "access.py"
15if (-not (Test-Path $AccessPy)) {
16 Write-Error "Missing $AccessPy — reinstall brtwin"
17 exit 1
18}
19
20function Write-BrtInfo($msg) { Write-Host "● $msg" -ForegroundColor Cyan }
21function Write-BrtOk($msg) { Write-Host "✓ $msg" -ForegroundColor Green }
22function Write-BrtWarn($msg) { Write-Host "! $msg" -ForegroundColor Yellow }
23function Write-BrtErr($msg) { Write-Host "✗ $msg" -ForegroundColor Red; exit 1 }
24
25function Get-BrtUserHome {
26 $candidates = @(
27 $env:USERPROFILE,
28 $env:HOME,
29 [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)
30 )
31 if ($env:LOCALAPPDATA) {
32 $candidates += (Split-Path $env:LOCALAPPDATA -Parent)
33 }
34 if ($env:USERNAME) {
35 $candidates += "C:\Users\$($env:USERNAME)"
36 }
37 $candidates += "C:\Users\Public"
38
39 foreach ($candidate in $candidates) {
40 if ([string]::IsNullOrWhiteSpace($candidate)) { continue }
41 $candidate = $candidate.Trim().TrimEnd('\')
42 if (Test-Path -LiteralPath $candidate -PathType Container -ErrorAction SilentlyContinue) {
43 return $candidate
44 }
45 }
46 foreach ($candidate in $candidates) {
47 if (-not [string]::IsNullOrWhiteSpace($candidate)) {
48 return $candidate.Trim().TrimEnd('\')
49 }
50 }
51 return "C:\Users\Public"
52}
53
54function Initialize-BrtAccessPaths {
55 $userHome = Get-BrtUserHome
56 $script:BrtStateDir = Join-Path $userHome ".brt"
57 $script:PidFile = Join-Path $script:BrtStateDir "access.pid"
58 $script:InfoFile = Join-Path $script:BrtStateDir "access.info"
59 if ($env:BRT_ACCESS_AUDIT_LOG -and $env:BRT_ACCESS_AUDIT_LOG.Trim()) {
60 $script:AuditLog = $env:BRT_ACCESS_AUDIT_LOG.Trim()
61 } else {
62 $script:AuditLog = Join-Path $script:BrtStateDir "access.audit.log"
63 }
64 if ($env:BRT_ACCESS_ADMIN_SOCK -and $env:BRT_ACCESS_ADMIN_SOCK.Trim()) {
65 $script:AdminSpec = $env:BRT_ACCESS_ADMIN_SOCK.Trim()
66 } else {
67 $script:AdminSpec = "tcp:127.0.0.1:8767"
68 }
69 if ([string]::IsNullOrWhiteSpace($script:BrtStateDir)) {
70 Write-BrtErr "Could not determine brt state directory"
71 }
72 New-Item -ItemType Directory -Force -Path $script:BrtStateDir | Out-Null
73}
74
75function Get-PyExe() {
76 if (Get-Command python -ErrorAction SilentlyContinue) { return @("python") }
77 if (Get-Command py -ErrorAction SilentlyContinue) { return @("py", "-3") }
78 Write-BrtErr "Python 3 is required"
79}
80
81function Invoke-AccessPy([string[]]$PyArgs) {
82 $py = Get-PyExe
83 $clean = @()
84 foreach ($arg in $PyArgs) {
85 if ($null -ne $arg -and -not [string]::IsNullOrWhiteSpace([string]$arg)) {
86 $clean += [string]$arg
87 }
88 }
89 & @py $AccessPy @clean
90 exit $LASTEXITCODE
91}
92
93function Get-PortPids([int]$Port) {
94 $pids = [System.Collections.Generic.List[int]]::new()
95 try {
96 Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue |
97 ForEach-Object { [void]$pids.Add($_.OwningProcess) }
98 } catch {
99 netstat -ano | Select-String ":$Port\s" | ForEach-Object {
100 if ($_ -match '\s(\d+)\s*$') { [void]$pids.Add([int]$Matches[1]) }
101 }
102 }
103 $pids | Select-Object -Unique
104}
105
106function Stop-AccessForce([int]$Port) {
107 $all = [System.Collections.Generic.List[int]]::new()
108 if (Test-Path $PidFile) {
109 $p = (Get-Content $PidFile -Raw).Trim()
110 if ($p -match '^\d+$') { [void]$all.Add([int]$p) }
111 }
112 Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
113 Where-Object { $_.CommandLine -and $_.CommandLine -match 'access\.py serve' } |
114 ForEach-Object { [void]$all.Add($_.ProcessId) }
115 Get-PortPids $Port | ForEach-Object { [void]$all.Add($_) }
116 $uniq = $all | Select-Object -Unique
117 foreach ($procId in $uniq) {
118 Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
119 }
120 Remove-Item $PidFile, $InfoFile -Force -ErrorAction SilentlyContinue
121 if ($uniq.Count -gt 0) { Write-BrtOk "Force stopped $($uniq.Count) process(es), port free" }
122 else { Write-BrtWarn "No access server processes found" }
123}
124
125function New-AccessToken() {
126 -join ((1..48) | ForEach-Object { '{0:x2}' -f (Get-Random -Max 256) })
127}
128
129function Get-LocalEndpoint([int]$Port) {
130 $ip = $null
131 try {
132 $ip = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
133 Where-Object { $_.IPAddress -notlike "127.*" -and $_.InterfaceOperationalStatus -eq "Up" } |
134 Select-Object -First 1).IPAddress
135 } catch { }
136 if (-not $ip) {
137 try {
138 $ip = ([System.Net.Dns]::GetHostAddresses([System.Net.Dns]::GetHostName()) |
139 Where-Object { $_.AddressFamily -eq 'InterNetwork' -and $_.IPAddressToString -notlike '127.*' } |
140 Select-Object -First 1).IPAddressToString
141 } catch { }
142 }
143 if (-not $ip) { $ip = "127.0.0.1" }
144 "${ip}:${Port}"
145}
146
147function Wait-AccessAdmin([string]$AdminSpec, [int]$TimeoutSec = 15) {
148 if ($AdminSpec -notmatch '^tcp:([^:]+):(\d+)$') { return $false }
149 $hostName = $Matches[1]
150 $port = [int]$Matches[2]
151 $deadline = (Get-Date).AddSeconds($TimeoutSec)
152 while ((Get-Date) -lt $deadline) {
153 $client = $null
154 try {
155 $client = New-Object System.Net.Sockets.TcpClient
156 $connect = $client.BeginConnect($hostName, $port, $null, $null)
157 if ($connect.AsyncWaitHandle.WaitOne(300, $false)) {
158 $client.EndConnect($connect)
159 return $true
160 }
161 } catch { }
162 finally {
163 if ($client) { $client.Close() }
164 }
165 Start-Sleep -Milliseconds 200
166 }
167 return $false
168}
169
170Initialize-BrtAccessPaths
171
172switch ($SubCmd) {
173 "serve" {
174 $port = 8765
175 for ($i = 0; $i -lt $Rest.Count; $i++) {
176 if ($Rest[$i] -in @("-p", "--port") -and ($i + 1) -lt $Rest.Count) {
177 $port = [int]$Rest[$i + 1]
178 }
179 }
180 $onPort = Get-PortPids $port
181 if ($onPort) {
182 Write-BrtWarn "Port $port in use (PID $($onPort -join ' ')) — freeing..."
183 Stop-AccessForce $port
184 }
185 $token = New-AccessToken
186 $logOut = Join-Path $BrtStateDir "access.stdout"
187 $logErr = Join-Path $BrtStateDir "access.log"
188 $py = Get-PyExe
189 $serveArgs = @(
190 $AccessPy, "serve", "--port", "$port", "--token", $token,
191 "--audit-log", $AuditLog, "--admin-sock", $AdminSpec
192 )
193 if ($py.Count -eq 1) {
194 $proc = Start-Process -FilePath $py[0] -ArgumentList $serveArgs -PassThru -WindowStyle Hidden `
195 -RedirectStandardOutput $logOut -RedirectStandardError $logErr
196 } else {
197 $proc = Start-Process -FilePath $py[0] -ArgumentList ($py[1..($py.Count - 1)] + $serveArgs) -PassThru -WindowStyle Hidden `
198 -RedirectStandardOutput $logOut -RedirectStandardError $logErr
199 }
200 Start-Sleep -Milliseconds 500
201 if ($proc.HasExited) { Write-BrtErr "Access server failed to start (see $logErr)" }
202 if (-not (Wait-AccessAdmin $AdminSpec)) {
203 Write-BrtWarn "Admin console not ready yet — dashboard may show offline briefly"
204 }
205 $endpoint = Get-LocalEndpoint $port
206 $connectCmd = "brtwin -access connect $endpoint $token"
207 try {
208 Set-Clipboard -Value $connectCmd
209 Write-BrtOk "Copied connect command to clipboard"
210 } catch {
211 Write-Host "● Connect: $connectCmd" -ForegroundColor Cyan
212 }
213 @"
214token=$token
215endpoint=$endpoint
216port=$port
217pid=$($proc.Id)
218audit_log=$AuditLog
219admin_sock=$AdminSpec
220started=$((Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"))
221"@ | Set-Content $InfoFile
222 $proc.Id | Set-Content $PidFile
223 Invoke-AccessPy @(
224 "watch-logs", "--audit-log", $AuditLog, "--admin-sock", $AdminSpec,
225 "--interactive", "--server-pid", "$($proc.Id)", "--endpoint", $endpoint,
226 "--token", $token, "--owns-server"
227 )
228 if (-not $proc.HasExited) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue }
229 Remove-Item $PidFile, $InfoFile -Force -ErrorAction SilentlyContinue
230 }
231 "connect" {
232 if ($Rest.Count -lt 2) { Write-BrtErr "Usage: brtwin -access connect <host:port> <token>" }
233 Invoke-AccessPy @("connect", $Rest[0], $Rest[1])
234 }
235 "clients" { Invoke-AccessPy @("clients", "--admin-sock", $AdminSpec) }
236 "leave" { Invoke-AccessPy @("leave") }
237 "disconnect" {
238 if (-not $Rest[0]) {
239 Invoke-AccessPy @("leave")
240 } else {
241 Invoke-AccessPy @("disconnect", $Rest[0], "--admin-sock", $AdminSpec)
242 }
243 }
244 "logs" {
245 $endpoint = ""; $token = ""; $serverPid = 0
246 if (Test-Path $InfoFile) {
247 Get-Content $InfoFile | ForEach-Object {
248 if ($_ -match '^endpoint=(.+)$') { $endpoint = $Matches[1] }
249 if ($_ -match '^token=(.+)$') { $token = $Matches[1] }
250 }
251 }
252 if (Test-Path $PidFile) {
253 $p = (Get-Content $PidFile -Raw).Trim()
254 if (Get-Process -Id $p -ErrorAction SilentlyContinue) { $serverPid = [int]$p }
255 }
256 Invoke-AccessPy @(
257 "watch-logs", "--audit-log", $AuditLog, "--admin-sock", $AdminSpec,
258 "--interactive", "--server-pid", "$serverPid", "--endpoint", $endpoint, "--token", $token
259 )
260 }
261 "clear-logs" {
262 Invoke-AccessPy @("clear-logs", "--audit-log", $AuditLog)
263 }
264 "force-stop" { Stop-AccessForce 8765 }
265 "stop" {
266 if (Test-Path $PidFile) {
267 $p = (Get-Content $PidFile -Raw).Trim()
268 Stop-Process -Id $p -Force -ErrorAction SilentlyContinue
269 Write-BrtOk "Stopped access server (PID $p)"
270 } else { Write-BrtWarn "No access server running" }
271 Remove-Item $PidFile, $InfoFile -Force -ErrorAction SilentlyContinue
272 }
273 "status" {
274 if (Test-Path $InfoFile) {
275 Get-Content $InfoFile
276 Invoke-AccessPy @("clients", "--admin-sock", $AdminSpec)
277 } else { Write-BrtWarn "No access server info found" }
278 }
279 default { Write-BrtErr "Unknown access command: $SubCmd" }
280}
281