[CmdletBinding()] param( [string]$Listen = "127.0.0.1:8765", [string[]]$Allow = @() ) $ErrorActionPreference = 'Stop' $ServiceName = "luxtools-client" $TaskName = $ServiceName function Write-Usage { @" Usage: install-windows-task.bat [--listen host:port] [--allow ]... Installs/updates $ServiceName as a Windows Scheduled Task (per-user, runs at logon). - Re-running updates the installed binary and restarts the task. - A stable token is stored in: %LOCALAPPDATA%\$ServiceName\config.json Options: --listen host:port Listen address (default: 127.0.0.1:8765) --allow PATH Allowed path prefix (repeatable). If none, any path is allowed. "@ | Write-Host } function Quote-Arg([string]$s) { if ($null -eq $s) { return '""' } # Simple CreateProcess-compatible quoting: wrap in quotes if whitespace or quotes exist. if ($s -match '[\s\"]') { $escaped = $s -replace '"', '\\"' return '"' + $escaped + '"' } return $s } function New-Base64UrlToken([int]$numBytes = 32) { $bytes = New-Object byte[] $numBytes [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) $b64 = [Convert]::ToBase64String($bytes) # Base64URL without padding return ($b64.TrimEnd('=') -replace '\+','-' -replace '/','_') } function Stop-ExistingInstance([string]$exePath) { try { if (Get-Command Stop-ScheduledTask -ErrorAction SilentlyContinue) { Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue | Out-Null } else { schtasks.exe /End /TN $TaskName 2>$null | Out-Null } } catch { # best-effort } try { $procs = Get-Process -Name $ServiceName -ErrorAction SilentlyContinue foreach ($p in $procs) { try { if ($p.Path -and (Split-Path -Path $p.Path -Resolve) -ieq (Split-Path -Path $exePath -Resolve)) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } } catch { # best-effort } } } catch { # best-effort } } # Parse args in a bash-like style to match the README expectation. $rawArgs = @($args) for ($i = 0; $i -lt $rawArgs.Count; $i++) { switch ($rawArgs[$i]) { '-h' { Write-Usage; exit 0 } '--help' { Write-Usage; exit 0 } '--listen' { if ($i + 1 -ge $rawArgs.Count) { throw "--listen requires a value" } $Listen = $rawArgs[$i + 1] $i++ continue } '--allow' { if ($i + 1 -ge $rawArgs.Count) { throw "--allow requires a value" } $p = $rawArgs[$i + 1] if ($p -and $p.Trim().Length -gt 0) { $Allow += $p } $i++ continue } default { throw "Unknown arg: $($rawArgs[$i])" } } } $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $srcExe = Join-Path $scriptDir "$ServiceName.exe" if (-not (Test-Path -LiteralPath $srcExe)) { Write-Error "Missing binary next to script: $srcExe`nBuild it first (e.g. 'go build -o $ServiceName.exe .') and re-run." } $installDir = Join-Path $env:LOCALAPPDATA $ServiceName $exePath = Join-Path $installDir "$ServiceName.exe" $configPath = Join-Path $installDir "config.json" New-Item -ItemType Directory -Force -Path $installDir | Out-Null # Load existing config if present (preserve TOKEN across updates). $existingToken = $null if (Test-Path -LiteralPath $configPath) { try { $cfg = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json if ($cfg -and $cfg.Token) { $existingToken = [string]$cfg.Token } } catch { # ignore malformed config; will regenerate } } $tokenSuggested = if ($existingToken) { "" } else { New-Base64UrlToken } Write-Host "" if ($existingToken) { Write-Host "A token is already configured. Press Enter to keep it, or paste a new one." } else { Write-Host "No token configured yet. Press Enter to use a generated token, or paste your own." } $tokenInput = Read-Host -Prompt "Token" # not secure, but matches Linux behavior $tokenInput = ("" + $tokenInput).Trim() $token = if ($tokenInput.Length -gt 0) { $tokenInput } elseif ($existingToken) { $existingToken } else { $tokenSuggested } if (-not $token -or $token.Trim().Length -eq 0) { throw "Failed to determine token" } # Persist config. $config = [ordered]@{ Listen = $Listen Token = $token Allow = @($Allow) } ($config | ConvertTo-Json -Depth 4) | Set-Content -LiteralPath $configPath -Encoding UTF8 # Update behavior: stop existing instance, then replace binary. Stop-ExistingInstance -exePath $exePath $tmpExe = Join-Path $installDir ("$ServiceName.tmp.{0}.exe" -f ([Guid]::NewGuid().ToString('N'))) Copy-Item -LiteralPath $srcExe -Destination $tmpExe -Force try { Move-Item -LiteralPath $tmpExe -Destination $exePath -Force } catch { # If replace failed, try removing and retry once. Remove-Item -LiteralPath $exePath -Force -ErrorAction SilentlyContinue Move-Item -LiteralPath $tmpExe -Destination $exePath -Force } # Build the argument string for the scheduled task. $argList = @('-listen', $Listen, '-token', $token) foreach ($p in $Allow) { if ($p -and $p.Trim().Length -gt 0) { $argList += @('-allow', $p) } } $argString = ($argList | ForEach-Object { Quote-Arg $_ }) -join ' ' # Register/update the scheduled task (per-user, interactive at logon). $principalUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $action = New-ScheduledTaskAction -Execute $exePath -Argument $argString $trigger = New-ScheduledTaskTrigger -AtLogOn -User $principalUser $principal = New-ScheduledTaskPrincipal -UserId $principalUser -LogonType Interactive -RunLevel Limited $settings = New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries $task = New-ScheduledTask -Action $action -Trigger $trigger -Principal $principal -Settings $settings Register-ScheduledTask -TaskName $TaskName -InputObject $task -Force | Out-Null # Start now (best-effort). try { Start-ScheduledTask -TaskName $TaskName | Out-Null } catch { # best-effort } Write-Host "" Write-Host "Installed/updated $ServiceName (Scheduled Task)." Write-Host "- Binary: $exePath" Write-Host "- Task: $TaskName" Write-Host "- Config: $configPath" Write-Host "Token (set this in the plugin config): $token" Write-Host "" Write-Host "To view task status: schtasks /Query /TN $TaskName /V" Write-Host "To run it manually: schtasks /Run /TN $TaskName"