<# .SYNOPSIS Installe (ou met à jour) l'agent Stream Control sur une VM Windows. .DESCRIPTION L'agent est enregistré comme tâche planifiée « à l'ouverture de session », et non comme service Windows : un service tourne en session 0 et ne verrait ni la fenêtre du navigateur, ni OBS. Réexécuter le script met à jour le binaire sans toucher à l'identité d'un agent déjà enrôlé. .EXAMPLE & ([scriptblock]::Create((irm 'https://git.jeanbonapp.com/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.ps1'))) ` -Registry 'https://git.jeanbonapp.com' ` -Server 'ws://control.lan:8080/ws/agent' ` -Token '' #> [CmdletBinding()] param( [Parameter(Mandatory = $true)][string] $Registry, [string] $Server = '', [string] $Token = '', [string] $Name = $env:COMPUTERNAME, [string] $Version = 'latest', [string] $Owner = 'jeanbon', [string] $PackageToken = '', [string] $ObsHost = '127.0.0.1', [int] $ObsPort = 4455, [string] $ObsPassword = '', [string] $InstallDir = 'C:\stream-control-agent', [string] $RunAsUser = "$env:USERDOMAIN\$env:USERNAME", [string] $TaskName = 'StreamControlAgent' ) $ErrorActionPreference = 'Stop' $Registry = $Registry.TrimEnd('/') function Info($msg) { Write-Host "· $msg" } function Die($msg) { Write-Host "✗ $msg" -ForegroundColor Red; exit 1 } $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { Die 'À exécuter dans un PowerShell administrateur.' } $configFile = Join-Path $InstallDir 'agent.config.json' $isUpgrade = Test-Path $configFile if (-not $isUpgrade) { if (-not $Server) { Die '-Server est obligatoire pour une première installation.' } if (-not $Token) { Die '-Token est obligatoire pour une première installation.' } } Write-Host '' Write-Host 'Installation de l''agent Stream Control' Write-Host " compte : $RunAsUser" Write-Host " destination : $InstallDir" Write-Host " version : $Version" Write-Host '' # --- Node.js ---------------------------------------------------------------- $nodeMajor = 0 $nodeCmd = Get-Command node -ErrorAction SilentlyContinue if ($nodeCmd) { $nodeMajor = [int]((& node -p 'process.versions.node.split(".")[0]') 2>$null) } if ($nodeMajor -lt 22) { Info "Node.js 22+ requis (trouvé : $nodeMajor) — installation via winget" if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { Die 'winget introuvable. Installe Node.js 22+ manuellement puis relance : https://nodejs.org/' } & winget install --id OpenJS.NodeJS.LTS --silent --accept-source-agreements --accept-package-agreements # winget ne rafraîchit pas le PATH de la session courante. $env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User') if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Die 'Node.js installé mais absent du PATH — rouvre PowerShell et relance le script.' } } else { Info "Node.js $(& node -v) présent" } # --- Téléchargement du bundle ----------------------------------------------- New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null $url = "$Registry/api/packages/$Owner/generic/stream-control-agent/$Version/agent.cjs" Info "Téléchargement depuis $url" $headers = @{} if ($PackageToken) { $headers['Authorization'] = "token $PackageToken" } $tmp = Join-Path $InstallDir 'agent.cjs.tmp' try { Invoke-WebRequest -Uri $url -Headers $headers -OutFile $tmp -UseBasicParsing } catch { Die "Téléchargement impossible ($($_.Exception.Message)). Vérifie -Registry, -Version, et -PackageToken si le paquet est privé." } & node --check $tmp 2>$null if ($LASTEXITCODE -ne 0) { Die 'Le fichier téléchargé n''est pas un script Node valide.' } Move-Item -Force $tmp (Join-Path $InstallDir 'agent.cjs') # --- Configuration ---------------------------------------------------------- if ($isUpgrade) { # Un agent déjà enrôlé détient un jeton permanent : l'écraser le ferait # réapparaître comme un second agent dans le dashboard. Info 'Configuration existante conservée (identité de l''agent préservée)' } else { $config = [ordered]@{ serverUrl = $Server token = $Token name = $Name obs = [ordered]@{ host = $ObsHost; port = $ObsPort; password = $ObsPassword } } # Surtout pas Set-Content -Encoding UTF8 : sous PowerShell 5.1 il préfixe un # BOM, et JSON.parse côté Node refuse le fichier. $json = $config | ConvertTo-Json -Depth 5 [System.IO.File]::WriteAllText($configFile, $json, (New-Object System.Text.UTF8Encoding($false))) Info "Configuration écrite dans $configFile" } # Le fichier porte un jeton : lecture réservée au compte de l'agent et aux admins. $acl = Get-Acl $configFile $acl.SetAccessRuleProtection($true, $false) foreach ($identity in @($RunAsUser, 'BUILTIN\Administrators', 'NT AUTHORITY\SYSTEM')) { try { $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule( $identity, 'FullControl', 'Allow'))) } catch { Write-Host "! Règle d'accès ignorée pour $identity" } } Set-Acl -Path $configFile -AclObject $acl # --- Tâche planifiée -------------------------------------------------------- $nodePath = (Get-Command node).Source $entry = Join-Path $InstallDir 'agent.cjs' $action = New-ScheduledTaskAction -Execute $nodePath -Argument "`"$entry`"" -WorkingDirectory $InstallDir $trigger = New-ScheduledTaskTrigger -AtLogOn -User $RunAsUser $principal = New-ScheduledTaskPrincipal -UserId $RunAsUser -LogonType Interactive -RunLevel Limited $settings = New-ScheduledTaskSettingsSet ` -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries ` -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) ` -ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger ` -Principal $principal -Settings $settings -Force | Out-Null Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue Start-ScheduledTask -TaskName $TaskName # --- Vérification ----------------------------------------------------------- Write-Host '' $env:AGENT_CONFIG = $configFile & node $entry --check Write-Host '' Start-Sleep -Seconds 2 $state = (Get-ScheduledTask -TaskName $TaskName).State if ($state -eq 'Running') { Write-Host '✓ Agent installé et démarré.' -ForegroundColor Green } else { Write-Host "! Tâche dans l'état « $state ». Détails :" -ForegroundColor Yellow Get-ScheduledTaskInfo -TaskName $TaskName | Format-List TaskName, LastRunTime, LastTaskResult } Write-Host @" état : Get-ScheduledTask -TaskName $TaskName test : node $entry --check "@