Fix : widows giea 2
All checks were successful
release / build (push) Successful in 16s
release / verify-windows (push) Successful in 1m13s

This commit is contained in:
jeanotx32
2026-08-11 22:48:15 +02:00
parent c9ab3dafb4
commit 6a45b3063f
2 changed files with 79 additions and 54 deletions

View File

@@ -199,9 +199,23 @@ jobs:
- name: Valider la syntaxe du script d'installation - name: Valider la syntaxe du script d'installation
shell: powershell shell: powershell
run: | 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 $errors = $null
[System.Management.Automation.Language.Parser]::ParseFile( [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) { if ($errors) {
$errors | ForEach-Object { Write-Host "$($_.Extent.StartLineNumber): $($_.Message)" } $errors | ForEach-Object { Write-Host "$($_.Extent.StartLineNumber): $($_.Message)" }
throw "$($errors.Count) erreur(s) de syntaxe dans install-agent.ps1" throw "$($errors.Count) erreur(s) de syntaxe dans install-agent.ps1"

View File

@@ -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 .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 .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 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. Reexecuter le script met a jour le binaire et applique les options fournies.
L'identité de l'agent (jeton permanent, agentId) est préservée : sans cela il L'identite de l'agent (jeton permanent, agentId) est preservee : sans cela il
réapparaîtrait en double dans le dashboard. Utiliser -ResetIdentity pour forcer reapparaitrait en double dans le dashboard. Utiliser -ResetIdentity pour forcer
un réenrôlement. un reenrolement.
.EXAMPLE .EXAMPLE
& ([scriptblock]::Create((irm 'https://git.jeanbonapp.com/api/packages/jeanbon/generic/stream-control-agent/latest/install-agent.ps1'))) ` & ([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] $InstallDir = 'C:\stream-control-agent',
[string] $RunAsUser = "$env:USERDOMAIN\$env:USERNAME", [string] $RunAsUser = "$env:USERDOMAIN\$env:USERNAME",
[string] $TaskName = 'StreamControlAgent', [string] $TaskName = 'StreamControlAgent',
# Réenrôle avec le -Token fourni : efface l'identité existante, ce qui crée un # Reenrole avec le -Token fourni : efface l'identite existante, ce qui cree un
# nouvel agent dans le dashboard. Sans ce commutateur, -Token est ignoré sur # nouvel agent dans le dashboard. Sans ce commutateur, -Token est ignore sur
# une mise à jour. # une mise a jour.
[switch] $ResetIdentity [switch] $ResetIdentity
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$Registry = $Registry.TrimEnd('/') $Registry = $Registry.TrimEnd('/')
function Info($msg) { Write-Host "· $msg" } function Info($msg) { Write-Host "- $msg" }
function Die($msg) { Write-Host " $msg" -ForegroundColor Red; exit 1 } function Die($msg) { Write-Host "[ECHEC] $msg" -ForegroundColor Red; exit 1 }
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent() $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) ).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' $configFile = Join-Path $InstallDir 'agent.config.json'
$isUpgrade = Test-Path $configFile $isUpgrade = Test-Path $configFile
if (-not $isUpgrade) { if (-not $isUpgrade) {
if (-not $Server) { Die '-Server 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 première installation.' } if (-not $Token) { Die '-Token est obligatoire pour une premiere installation.' }
} }
Write-Host '' Write-Host ''
@@ -69,65 +80,65 @@ Write-Host ''
$nodeMajor = 0 $nodeMajor = 0
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue $nodeCmd = Get-Command node -ErrorAction SilentlyContinue
if ($nodeCmd) { if ($nodeCmd) {
# On lit `node --version` plutôt que d'évaluer une expression JavaScript. # On lit `node --version` plutot que d'evaluer une expression JavaScript.
# PowerShell 5.1 perd les guillemets doubles imbriqués quand il passe un # PowerShell 5.1 perd les guillemets doubles imbriques quand il passe un
# argument à un exécutable natif : `node -p 'x.split(".")[0]'` arrive côté # argument a un executable natif : `node -p 'x.split(".")[0]'` arrive cote
# Node comme `x.split(.)[0]`, qui lève une SyntaxError. Le majeur valait # Node comme `x.split(.)[0]`, qui leve une SyntaxError. Le majeur valait
# alors 0 et le script réinstallait Node sur une machine déjà équipée. # alors 0 et le script reinstallait Node sur une machine deja equipee.
$nodeRaw = (& node --version 2>$null) $nodeRaw = (& node --version 2>$null)
if ($nodeRaw -match '^v(\d+)\.') { $nodeMajor = [int]$Matches[1] } if ($nodeRaw -match '^v(\d+)\.') { $nodeMajor = [int]$Matches[1] }
} }
if ($nodeMajor -lt 22) { 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)) { if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Die 'winget introuvable. Installe Node.js 22+ manuellement puis relance : https://nodejs.org/' 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 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') + ';' + $env:Path = [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' +
[Environment]::GetEnvironmentVariable('Path', 'User') [Environment]::GetEnvironmentVariable('Path', 'User')
if (-not (Get-Command node -ErrorAction SilentlyContinue)) { 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 { } 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 New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
$url = "$Registry/api/packages/$Owner/generic/stream-control-agent/$Version/agent.cjs" $url = "$Registry/api/packages/$Owner/generic/stream-control-agent/$Version/agent.cjs"
Info "Téléchargement depuis $url" Info "Telechargement depuis $url"
$headers = @{} $headers = @{}
if ($PackageToken) { $headers['Authorization'] = "token $PackageToken" } if ($PackageToken) { $headers['Authorization'] = "token $PackageToken" }
# Le fichier temporaire doit garder l'extension .cjs : `node --check` déduit le # 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 reconnaît pas # format du module de l'extension et rejette tout ce qu'il ne reconnait pas
# (« Unknown file extension .tmp »), quel que soit le contenu. # ('Unknown file extension .tmp'), quel que soit le contenu.
$tmp = Join-Path $InstallDir 'agent-new.cjs' $tmp = Join-Path $InstallDir 'agent-new.cjs'
try { try {
Invoke-WebRequest -Uri $url -Headers $headers -OutFile $tmp -UseBasicParsing Invoke-WebRequest -Uri $url -Headers $headers -OutFile $tmp -UseBasicParsing
} catch { } 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) $checkErr = (& node --check $tmp 2>&1 | Out-String)
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Host $checkErr Write-Host $checkErr
Write-Host " (fichier conservé pour inspection : $tmp)" Write-Host " (fichier conserve pour inspection : $tmp)"
Die 'Le fichier téléchargé n''est pas un script Node valide voir l''erreur ci-dessus.' 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') Move-Item -Force $tmp (Join-Path $InstallDir 'agent.cjs')
# --- Configuration ---------------------------------------------------------- # --- Configuration ----------------------------------------------------------
if ($isUpgrade) { if ($isUpgrade) {
# Fusion, et non conservation en bloc : l'identité (jeton, agentId) est # Fusion, et non conservation en bloc : l'identite (jeton, agentId) est
# préservée l'écraser ferait réapparaître l'agent en double dans le # preservee - l'ecraser ferait reapparaitre l'agent en double dans le
# dashboard mais toute option explicitement passée doit s'appliquer. # dashboard - mais toute option explicitement passee doit s'appliquer.
# $PSBoundParameters distingue « fourni » de « laissé au défaut ». # $PSBoundParameters distingue 'fourni' de 'laisse au defaut'.
$raw = (Get-Content $configFile -Raw) -replace "^\uFEFF", '' $raw = (Get-Content $configFile -Raw) -replace "^\uFEFF", ''
$current = $raw | ConvertFrom-Json $current = $raw | ConvertFrom-Json
if (-not $current.obs) { if (-not $current.obs) {
@@ -135,8 +146,8 @@ if ($isUpgrade) {
} }
$applied = @() $applied = @()
# Toujours rafraîchie : ce n'est pas de l'identité, c'est de l'infrastructure. # Toujours rafraichie : ce n'est pas de l'identite, c'est de l'infrastructure.
# Sans elle, l'agent ne saurait pas où chercher sa propre mise à jour. # Sans elle, l'agent ne saurait pas ou chercher sa propre mise a jour.
if ($current.PSObject.Properties.Name -notcontains 'packageUrl') { if ($current.PSObject.Properties.Name -notcontains 'packageUrl') {
$current | Add-Member -NotePropertyName packageUrl -NotePropertyValue $url -Force $current | Add-Member -NotePropertyName packageUrl -NotePropertyValue $url -Force
$applied += 'packageUrl' $applied += 'packageUrl'
@@ -154,17 +165,17 @@ if ($isUpgrade) {
if ($ResetIdentity -and $Token) { if ($ResetIdentity -and $Token) {
$current.token = $Token $current.token = $Token
$current.PSObject.Properties.Remove('agentId') $current.PSObject.Properties.Remove('agentId')
$applied += 'identité réinitialisée' $applied += 'identite reinitialisee'
} elseif ($Token -and $Token -ne $current.token) { } 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 $json = $current | ConvertTo-Json -Depth 5
[System.IO.File]::WriteAllText($configFile, $json, (New-Object System.Text.UTF8Encoding($false))) [System.IO.File]::WriteAllText($configFile, $json, (New-Object System.Text.UTF8Encoding($false)))
Write-Host $(if ($applied.Count) { " champs mis à jour : $($applied -join ', ')" } Write-Host $(if ($applied.Count) { " champs mis a jour : $($applied -join ', ')" }
else { ' aucun changement demandé' }) else { ' aucun changement demande' })
Info 'Configuration mise à jour, identité de l''agent préservée' Info 'Configuration mise a jour, identite de l''agent preservee'
} else { } else {
$config = [ordered]@{ $config = [ordered]@{
serverUrl = $Server serverUrl = $Server
@@ -173,14 +184,14 @@ if ($isUpgrade) {
packageUrl = $url packageUrl = $url
obs = [ordered]@{ host = $ObsHost; port = $ObsPort; password = $ObsPassword } obs = [ordered]@{ host = $ObsHost; port = $ObsPort; password = $ObsPassword }
} }
# Surtout pas Set-Content -Encoding UTF8 : sous PowerShell 5.1 il préfixe un # Surtout pas Set-Content -Encoding UTF8 : sous PowerShell 5.1 il prefixe un
# BOM, et JSON.parse côté Node refuse le fichier. # BOM, et JSON.parse cote Node refuse le fichier.
$json = $config | ConvertTo-Json -Depth 5 $json = $config | ConvertTo-Json -Depth 5
[System.IO.File]::WriteAllText($configFile, $json, (New-Object System.Text.UTF8Encoding($false))) [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 = Get-Acl $configFile
$acl.SetAccessRuleProtection($true, $false) $acl.SetAccessRuleProtection($true, $false)
foreach ($identity in @($RunAsUser, 'BUILTIN\Administrators', 'NT AUTHORITY\SYSTEM')) { 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( $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
$identity, 'FullControl', 'Allow'))) $identity, 'FullControl', 'Allow')))
} catch { } 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 Set-Acl -Path $configFile -AclObject $acl
# --- Tâche planifiée -------------------------------------------------------- # --- Tache planifiee --------------------------------------------------------
$nodePath = (Get-Command node).Source $nodePath = (Get-Command node).Source
$entry = Join-Path $InstallDir 'agent.cjs' $entry = Join-Path $InstallDir 'agent.cjs'
@@ -211,7 +222,7 @@ Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
Start-ScheduledTask -TaskName $TaskName Start-ScheduledTask -TaskName $TaskName
# --- Vérification ----------------------------------------------------------- # --- Verification -----------------------------------------------------------
Write-Host '' Write-Host ''
$env:AGENT_CONFIG = $configFile $env:AGENT_CONFIG = $configFile
& node $entry --check & node $entry --check
@@ -220,15 +231,15 @@ Write-Host ''
Start-Sleep -Seconds 2 Start-Sleep -Seconds 2
$state = (Get-ScheduledTask -TaskName $TaskName).State $state = (Get-ScheduledTask -TaskName $TaskName).State
if ($state -eq 'Running') { if ($state -eq 'Running') {
Write-Host ' Agent installé et démarré.' -ForegroundColor Green Write-Host '[OK] Agent installe et demarre.' -ForegroundColor Green
} else { } 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 Get-ScheduledTaskInfo -TaskName $TaskName | Format-List TaskName, LastRunTime, LastTaskResult
} }
Write-Host @" Write-Host @"
état : Get-ScheduledTask -TaskName $TaskName etat : Get-ScheduledTask -TaskName $TaskName
test : node $entry --check test : node $entry --check
"@ "@