diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 58d4104..4b41a55 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -199,9 +199,23 @@ jobs: - name: Valider la syntaxe du script d'installation shell: powershell run: | + $path = (Resolve-Path 'deploy/install-agent.ps1').Path + + # Le script doit rester en ASCII pur. Il est publié sans BOM et exécuté + # par PowerShell 5.1, qui relit alors le fichier dans la page de codes + # ANSI. Au-delà de l'affichage abîmé, certains caractères cassent le + # parseur : le tiret cadratin (E2 80 94) se relit en CP1252 comme « — », + # dont le dernier caractère est U+201D — un délimiteur de chaîne valide + # en PowerShell. La chaîne se ferme en plein milieu et l'erreur signalée + # est une accolade manquante, des lignes plus loin. + $bad = [System.IO.File]::ReadAllBytes($path) | Where-Object { $_ -gt 127 } + if ($bad) { + throw "install-agent.ps1 doit rester en ASCII : $($bad.Count) octet(s) hors plage" + } + $errors = $null [System.Management.Automation.Language.Parser]::ParseFile( - (Resolve-Path 'deploy/install-agent.ps1'), [ref]$null, [ref]$errors) | Out-Null + $path, [ref]$null, [ref]$errors) | Out-Null if ($errors) { $errors | ForEach-Object { Write-Host "$($_.Extent.StartLineNumber): $($_.Message)" } throw "$($errors.Count) erreur(s) de syntaxe dans install-agent.ps1" diff --git a/deploy/install-agent.ps1 b/deploy/install-agent.ps1 index dcbed35..b2f9832 100644 --- a/deploy/install-agent.ps1 +++ b/deploy/install-agent.ps1 @@ -1,16 +1,27 @@ <# + ATTENTION : ce fichier doit rester en ASCII pur, sans accent ni typographie. + + Il est publie sans BOM et execute par Windows PowerShell 5.1, qui relit alors + un .ps1 dans la page de codes ANSI. Un caractere non-ASCII y devient illisible, + et certains cassent carrement le parseur : le tiret cadratin se relit en CP1252 + comme trois caracteres dont le dernier, U+201D, est un delimiteur de chaine + valide en PowerShell. La chaine se ferme en plein milieu de la ligne, et + l'erreur remontee est une accolade manquante ailleurs dans le fichier. + + La CI verifie cette regle avant le controle de syntaxe. + .SYNOPSIS - Installe (ou met à jour) l'agent Stream Control sur une VM Windows. + Installe (ou met a jour) l'agent Stream Control sur une VM Windows. .DESCRIPTION - L'agent est enregistré comme tâche planifiée « à l'ouverture de session », et + L'agent est enregistre comme tache planifiee 'a 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. + fenetre du navigateur, ni OBS. - Réexécuter le script met à jour le binaire et applique les options fournies. - L'identité de l'agent (jeton permanent, agentId) est préservée : sans cela il - réapparaîtrait en double dans le dashboard. Utiliser -ResetIdentity pour forcer - un réenrôlement. + Reexecuter le script met a jour le binaire et applique les options fournies. + L'identite de l'agent (jeton permanent, agentId) est preservee : sans cela il + reapparaitrait en double dans le dashboard. Utiliser -ResetIdentity pour forcer + un reenrolement. .EXAMPLE & ([scriptblock]::Create((irm 'https://git.jeanbonapp.com/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.ps1'))) ` @@ -34,28 +45,28 @@ param( [string] $InstallDir = 'C:\stream-control-agent', [string] $RunAsUser = "$env:USERDOMAIN\$env:USERNAME", [string] $TaskName = 'StreamControlAgent', - # Réenrôle avec le -Token fourni : efface l'identité existante, ce qui crée un - # nouvel agent dans le dashboard. Sans ce commutateur, -Token est ignoré sur - # une mise à jour. + # Reenrole avec le -Token fourni : efface l'identite existante, ce qui cree un + # nouvel agent dans le dashboard. Sans ce commutateur, -Token est ignore sur + # une mise a jour. [switch] $ResetIdentity ) $ErrorActionPreference = 'Stop' $Registry = $Registry.TrimEnd('/') -function Info($msg) { Write-Host "· $msg" } -function Die($msg) { Write-Host "✗ $msg" -ForegroundColor Red; exit 1 } +function Info($msg) { Write-Host "- $msg" } +function Die($msg) { Write-Host "[ECHEC] $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.' } +if (-not $isAdmin) { Die 'A executer 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.' } + if (-not $Server) { Die '-Server est obligatoire pour une premiere installation.' } + if (-not $Token) { Die '-Token est obligatoire pour une premiere installation.' } } Write-Host '' @@ -69,65 +80,65 @@ Write-Host '' $nodeMajor = 0 $nodeCmd = Get-Command node -ErrorAction SilentlyContinue if ($nodeCmd) { - # On lit `node --version` plutôt que d'évaluer une expression JavaScript. - # PowerShell 5.1 perd les guillemets doubles imbriqués quand il passe un - # argument à un exécutable natif : `node -p 'x.split(".")[0]'` arrive côté - # Node comme `x.split(.)[0]`, qui lève une SyntaxError. Le majeur valait - # alors 0 et le script réinstallait Node sur une machine déjà équipée. + # On lit `node --version` plutot que d'evaluer une expression JavaScript. + # PowerShell 5.1 perd les guillemets doubles imbriques quand il passe un + # argument a un executable natif : `node -p 'x.split(".")[0]'` arrive cote + # Node comme `x.split(.)[0]`, qui leve une SyntaxError. Le majeur valait + # alors 0 et le script reinstallait Node sur une machine deja equipee. $nodeRaw = (& node --version 2>$null) if ($nodeRaw -match '^v(\d+)\.') { $nodeMajor = [int]$Matches[1] } } if ($nodeMajor -lt 22) { - Info "Node.js 22+ requis (trouvé : $nodeMajor) — installation via winget" + Info "Node.js 22+ requis (trouve : $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. + # winget ne rafraichit 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.' + Die 'Node.js installe mais absent du PATH - rouvre PowerShell et relance le script.' } } else { - Info "Node.js $(& node -v) présent" + Info "Node.js $(& node -v) present" } -# --- Téléchargement du bundle ----------------------------------------------- +# --- Telechargement 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" +Info "Telechargement depuis $url" $headers = @{} if ($PackageToken) { $headers['Authorization'] = "token $PackageToken" } -# Le fichier temporaire doit garder l'extension .cjs : `node --check` déduit le -# format du module de l'extension et rejette tout ce qu'il ne reconnaît pas -# (« Unknown file extension .tmp »), quel que soit le contenu. +# Le fichier temporaire doit garder l'extension .cjs : `node --check` deduit le +# format du module de l'extension et rejette tout ce qu'il ne reconnait pas +# ('Unknown file extension .tmp'), quel que soit le contenu. $tmp = Join-Path $InstallDir 'agent-new.cjs' 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é." + Die "Telechargement impossible ($($_.Exception.Message)). Verifie -Registry, -Version, et -PackageToken si le paquet est prive." } $checkErr = (& node --check $tmp 2>&1 | Out-String) if ($LASTEXITCODE -ne 0) { Write-Host $checkErr - Write-Host " (fichier conservé pour inspection : $tmp)" - Die 'Le fichier téléchargé n''est pas un script Node valide — voir l''erreur ci-dessus.' + Write-Host " (fichier conserve pour inspection : $tmp)" + Die 'Le fichier telecharge n''est pas un script Node valide - voir l''erreur ci-dessus.' } Move-Item -Force $tmp (Join-Path $InstallDir 'agent.cjs') # --- Configuration ---------------------------------------------------------- if ($isUpgrade) { - # Fusion, et non conservation en bloc : l'identité (jeton, agentId) est - # préservée — l'écraser ferait réapparaître l'agent en double dans le - # dashboard — mais toute option explicitement passée doit s'appliquer. - # $PSBoundParameters distingue « fourni » de « laissé au défaut ». + # Fusion, et non conservation en bloc : l'identite (jeton, agentId) est + # preservee - l'ecraser ferait reapparaitre l'agent en double dans le + # dashboard - mais toute option explicitement passee doit s'appliquer. + # $PSBoundParameters distingue 'fourni' de 'laisse au defaut'. $raw = (Get-Content $configFile -Raw) -replace "^\uFEFF", '' $current = $raw | ConvertFrom-Json if (-not $current.obs) { @@ -135,8 +146,8 @@ if ($isUpgrade) { } $applied = @() - # Toujours rafraîchie : ce n'est pas de l'identité, c'est de l'infrastructure. - # Sans elle, l'agent ne saurait pas où chercher sa propre mise à jour. + # Toujours rafraichie : ce n'est pas de l'identite, c'est de l'infrastructure. + # Sans elle, l'agent ne saurait pas ou chercher sa propre mise a jour. if ($current.PSObject.Properties.Name -notcontains 'packageUrl') { $current | Add-Member -NotePropertyName packageUrl -NotePropertyValue $url -Force $applied += 'packageUrl' @@ -154,17 +165,17 @@ if ($isUpgrade) { if ($ResetIdentity -and $Token) { $current.token = $Token $current.PSObject.Properties.Remove('agentId') - $applied += 'identité réinitialisée' + $applied += 'identite reinitialisee' } elseif ($Token -and $Token -ne $current.token) { - Write-Host ' (-Token ignoré : identité conservée. Utilise -ResetIdentity pour forcer.)' + Write-Host ' (-Token ignore : identite conservee. Utilise -ResetIdentity pour forcer.)' } $json = $current | ConvertTo-Json -Depth 5 [System.IO.File]::WriteAllText($configFile, $json, (New-Object System.Text.UTF8Encoding($false))) - Write-Host $(if ($applied.Count) { " champs mis à jour : $($applied -join ', ')" } - else { ' aucun changement demandé' }) - Info 'Configuration mise à jour, identité de l''agent préservée' + Write-Host $(if ($applied.Count) { " champs mis a jour : $($applied -join ', ')" } + else { ' aucun changement demande' }) + Info 'Configuration mise a jour, identite de l''agent preservee' } else { $config = [ordered]@{ serverUrl = $Server @@ -173,14 +184,14 @@ if ($isUpgrade) { packageUrl = $url 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. + # Surtout pas Set-Content -Encoding UTF8 : sous PowerShell 5.1 il prefixe un + # BOM, et JSON.parse cote 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" + Info "Configuration ecrite dans $configFile" } -# Le fichier porte un jeton : lecture réservée au compte de l'agent et aux admins. +# Le fichier porte un jeton : lecture reservee 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')) { @@ -188,12 +199,12 @@ foreach ($identity in @($RunAsUser, 'BUILTIN\Administrators', 'NT AUTHORITY\SYST $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule( $identity, 'FullControl', 'Allow'))) } catch { - Write-Host "! Règle d'accès ignorée pour $identity" + Write-Host "! Regle d'acces ignoree pour $identity" } } Set-Acl -Path $configFile -AclObject $acl -# --- Tâche planifiée -------------------------------------------------------- +# --- Tache planifiee -------------------------------------------------------- $nodePath = (Get-Command node).Source $entry = Join-Path $InstallDir 'agent.cjs' @@ -211,7 +222,7 @@ Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger ` Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue Start-ScheduledTask -TaskName $TaskName -# --- Vérification ----------------------------------------------------------- +# --- Verification ----------------------------------------------------------- Write-Host '' $env:AGENT_CONFIG = $configFile & node $entry --check @@ -220,15 +231,15 @@ 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 + Write-Host '[OK] Agent installe et demarre.' -ForegroundColor Green } else { - Write-Host "! Tâche dans l'état « $state ». Détails :" -ForegroundColor Yellow + Write-Host "! Tache dans l'etat '$state'. Details :" -ForegroundColor Yellow Get-ScheduledTaskInfo -TaskName $TaskName | Format-List TaskName, LastRunTime, LastTaskResult } Write-Host @" - état : Get-ScheduledTask -TaskName $TaskName + etat : Get-ScheduledTask -TaskName $TaskName test : node $entry --check "@