# Planification - gestionnaire graphique du helper de sync. // 2026.1.5 - QRO # Developpe par Quentin Rouiller (QRO) - DGNSI, Canton de Vaud. # Licence MIT - voir le fichier LICENSE. # # Fenetre avec : Installer / Mettre a jour, Reparer, Desinstaller. # Per-user (HKCU), sans admin, sans signature. Lance par Gerer.cmd. # # Structure du paquet : Gerer.cmd (racine), bin\ (manage/host/tray/.vbs), # assets\ (icon.png). Structure installee (%LOCALAPPDATA%\Planification) : # bin\ (host.ps1, host.bat, tray.ps1, tray.vbs), assets\ (icon.png), # manifests\ (nm-*.json), activity.log (a la racine). $ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $HostName = 'ch.netaplaid.planification' $ExtIdChromium = 'hlkepgcngeckdbapafkheondiagjgphc' $ExtIdFirefox = 'planification-dgnsi@netaplaid.ch' $ScriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path } # paquet\bin $PkgRoot = Split-Path -Parent $ScriptDir # paquet\ $InstallDir = Join-Path $env:LOCALAPPDATA 'Planification' $InstallBin = Join-Path $InstallDir 'bin' $InstallAssets = Join-Path $InstallDir 'assets' $InstallManifests = Join-Path $InstallDir 'manifests' function Get-HostVersion([string]$psFile) { if (Test-Path -LiteralPath $psFile) { $m = Select-String -LiteralPath $psFile -Pattern "HOST_VERSION\s*=\s*'([^']*)'" -ErrorAction SilentlyContinue if ($m) { return $m.Matches[0].Groups[1].Value } } return $null } function Stop-Tray { Get-CimInstance Win32_Process -Filter "Name='wscript.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like '*tray.vbs*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like '*tray.ps1*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } } function Install-Host { Stop-Tray foreach ($d in @($InstallBin, $InstallAssets, $InstallManifests)) { New-Item -ItemType Directory -Path $d -Force | Out-Null } # Scripts -> bin\ (ecriture fraiche = pas de Mark of the Web). # v2026.1.7 - on copie AUSSI manage.ps1 dans l'install pour que le gestionnaire # (Reparer / Desinstaller) reste accessible via le Menu Demarrer meme apres # suppression du dossier du paquet. foreach ($name in @('host.ps1', 'tray.ps1', 'manage.ps1')) { $src = Join-Path $ScriptDir $name if (Test-Path -LiteralPath $src) { Get-Content -LiteralPath $src -Raw | Set-Content -LiteralPath (Join-Path $InstallBin $name) -Encoding UTF8 Unblock-File -LiteralPath (Join-Path $InstallBin $name) -ErrorAction SilentlyContinue } } $srcVbs = Join-Path $ScriptDir 'tray.vbs' if (Test-Path -LiteralPath $srcVbs) { Get-Content -LiteralPath $srcVbs -Raw | Set-Content -LiteralPath (Join-Path $InstallBin 'tray.vbs') -Encoding ASCII Unblock-File -LiteralPath (Join-Path $InstallBin 'tray.vbs') -ErrorAction SilentlyContinue } # Icone -> assets\ (binaire : Copy-Item). $srcIcon = Join-Path $PkgRoot 'assets\icon.png' if (Test-Path -LiteralPath $srcIcon) { Copy-Item -LiteralPath $srcIcon -Destination (Join-Path $InstallAssets 'icon.png') -Force } # Wrapper .bat dans bin\ (host.ps1 est dans le meme dossier -> %~dp0host.ps1). $dstBat = Join-Path $InstallBin 'host.bat' Set-Content -LiteralPath $dstBat -Encoding ASCII -Value @( '@echo off', 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0host.ps1" %*' ) # Manifests natifs -> manifests\ (path = bin\host.bat). $manChromium = Join-Path $InstallManifests 'nm-chromium.json' $manFirefox = Join-Path $InstallManifests 'nm-firefox.json' (([ordered]@{ name=$HostName; description='Planification cache sync host'; path=$dstBat; type='stdio'; allowed_origins=@("chrome-extension://$ExtIdChromium/") }) | ConvertTo-Json -Depth 5) | Set-Content -LiteralPath $manChromium -Encoding UTF8 (([ordered]@{ name=$HostName; description='Planification cache sync host'; path=$dstBat; type='stdio'; allowed_extensions=@($ExtIdFirefox) }) | ConvertTo-Json -Depth 5) | Set-Content -LiteralPath $manFirefox -Encoding UTF8 function Reg([string]$base, [string]$manifest) { $key = Join-Path $base $HostName New-Item -Path $key -Force | Out-Null Set-Item -Path $key -Value $manifest } Reg 'HKCU:\Software\Google\Chrome\NativeMessagingHosts' $manChromium Reg 'HKCU:\Software\Microsoft\Edge\NativeMessagingHosts' $manChromium Reg 'HKCU:\Software\Mozilla\NativeMessagingHosts' $manFirefox # v2026.1.7 - le tray ne demarre PLUS au login et n'est PLUS lance a l'install. # Il est demarre A LA DEMANDE par host.ps1 quand l'extension se connecte, et # s'auto-ferme apres 2 h sans activite. On retire l'ancien autostart s'il # existe (installs precedentes) pour que "de base = eteint". Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'PlanificationTray' -ErrorAction SilentlyContinue # v2026.1.7 - Menu Demarrer : raccourci vers le gestionnaire installe # (Installer / Reparer / Desinstaller), retrouvable sans le dossier du paquet. # 1) lanceur .vbs (fenetre PowerShell masquee) du manage.ps1 installe. $dstGererVbs = Join-Path $InstallBin 'gerer.vbs' Set-Content -LiteralPath $dstGererVbs -Encoding ASCII -Value @( "' Lance le gestionnaire Planification (fenetre PowerShell masquee).", 'Set sh = CreateObject("WScript.Shell")', 'Set fso = CreateObject("Scripting.FileSystemObject")', 'dir = fso.GetParentFolderName(WScript.ScriptFullName)', 'sh.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File """ & dir & "\manage.ps1""", 0, False' ) # 2) icone .ico (depuis icon.png) pour le raccourci, best-effort. $icoPath = Join-Path $InstallAssets 'icon.ico' try { $pngForIco = Join-Path $InstallAssets 'icon.png' if (Test-Path -LiteralPath $pngForIco) { Add-Type -AssemblyName System.Drawing $bmp = New-Object System.Drawing.Bitmap($pngForIco) $ico = [System.Drawing.Icon]::FromHandle($bmp.GetHicon()) $fs = [System.IO.File]::Create($icoPath) $ico.Save($fs); $fs.Close() } } catch { $icoPath = $null } # 3) raccourcis .lnk dans le Menu Demarrer (deux entrees) : # - "gerer" -> manage.ps1 (installer / reparer / desinstaller) # - "demarrer" -> tray.vbs (relance/affiche la sync a la demande) $dstTrayVbs = Join-Path $InstallBin 'tray.vbs' try { $startDir = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs' New-Item -ItemType Directory -Path $startDir -Force | Out-Null # Nettoyage de l'ancien nom de raccourci (installs precedentes). Remove-Item -LiteralPath (Join-Path $startDir 'Planification - synchronisation.lnk') -Force -ErrorAction SilentlyContinue $ws = New-Object -ComObject WScript.Shell function New-Lnk([string]$name, [string]$targetVbs, [string]$desc) { $sc = $ws.CreateShortcut((Join-Path $startDir $name)) $sc.TargetPath = 'wscript.exe' $sc.Arguments = '"' + $targetVbs + '"' $sc.WorkingDirectory = $InstallBin $sc.Description = $desc if ($icoPath -and (Test-Path -LiteralPath $icoPath)) { $sc.IconLocation = "$icoPath,0" } $sc.Save() } New-Lnk 'Planification - gerer.lnk' $dstGererVbs 'Gerer la synchronisation Planification (installer / reparer / desinstaller)' New-Lnk 'Planification - demarrer la synchro.lnk' $dstTrayVbs 'Demarrer / afficher la synchronisation Planification' } catch {} } function Uninstall-Host { Stop-Tray Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'PlanificationTray' -ErrorAction SilentlyContinue foreach ($base in @( 'HKCU:\Software\Google\Chrome\NativeMessagingHosts', 'HKCU:\Software\Microsoft\Edge\NativeMessagingHosts', 'HKCU:\Software\Mozilla\NativeMessagingHosts')) { Remove-Item -Path (Join-Path $base $HostName) -Recurse -Force -ErrorAction SilentlyContinue } if (Test-Path -LiteralPath $InstallDir) { Remove-Item -LiteralPath $InstallDir -Recurse -Force -ErrorAction SilentlyContinue } # v2026.1.7 - retirer les raccourcis du Menu Demarrer (hors InstallDir). $startDir = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs' foreach ($lnk in @('Planification - synchronisation.lnk', 'Planification - gerer.lnk', 'Planification - demarrer la synchro.lnk')) { Remove-Item -LiteralPath (Join-Path $startDir $lnk) -Force -ErrorAction SilentlyContinue } } # ----------------------------- Interface ----------------------------------- $available = Get-HostVersion (Join-Path $ScriptDir 'host.ps1') $form = New-Object System.Windows.Forms.Form $form.Text = 'Planification - synchronisation' $form.Size = New-Object System.Drawing.Size(460, 320) $form.StartPosition = 'CenterScreen' $form.FormBorderStyle = 'FixedSingle' $form.MaximizeBox = $false try { $ic = Join-Path $PkgRoot 'assets\icon.png' if (Test-Path -LiteralPath $ic) { $form.Icon = [System.Drawing.Icon]::FromHandle((New-Object System.Drawing.Bitmap($ic)).GetHicon()) } } catch {} $title = New-Object System.Windows.Forms.Label $title.Text = 'Synchronisation du planning entre postes' $title.Font = New-Object System.Drawing.Font('Segoe UI', 11, [System.Drawing.FontStyle]::Bold) $title.AutoSize = $true $title.Location = New-Object System.Drawing.Point(18, 16) $form.Controls.Add($title) $status = New-Object System.Windows.Forms.Label $status.AutoSize = $false $status.Size = New-Object System.Drawing.Size(420, 40) $status.Location = New-Object System.Drawing.Point(18, 48) $form.Controls.Add($status) function Refresh-Status { $installed = Get-HostVersion (Join-Path $InstallBin 'host.ps1') if ($installed) { $status.Text = "Installe : version $installed`r`nDisponible (ce dossier) : version $available" } else { $status.Text = "Non installe sur ce poste.`r`nDisponible (ce dossier) : version $available" } } Refresh-Status $result = New-Object System.Windows.Forms.Label $result.AutoSize = $false $result.Size = New-Object System.Drawing.Size(420, 30) $result.Location = New-Object System.Drawing.Point(18, 248) $result.ForeColor = [System.Drawing.Color]::DarkGreen function Add-Button([string]$text, [int]$y, [scriptblock]$action) { $b = New-Object System.Windows.Forms.Button $b.Text = $text $b.Size = New-Object System.Drawing.Size(420, 38) $b.Location = New-Object System.Drawing.Point(18, $y) $b.Font = New-Object System.Drawing.Font('Segoe UI', 10) $b.add_Click($action) $form.Controls.Add($b) } Add-Button 'Installer / Mettre a jour' 100 { try { Install-Host; $result.ForeColor=[System.Drawing.Color]::DarkGreen; $result.Text='Installation / mise a jour terminee.'; Refresh-Status } catch { $result.ForeColor=[System.Drawing.Color]::Firebrick; $result.Text=('Erreur : ' + $_.Exception.Message) } } Add-Button 'Reparer (reinstaller proprement)' 144 { try { Install-Host; $result.ForeColor=[System.Drawing.Color]::DarkGreen; $result.Text='Reparation terminee.'; Refresh-Status } catch { $result.ForeColor=[System.Drawing.Color]::Firebrick; $result.Text=('Erreur : ' + $_.Exception.Message) } } Add-Button 'Desinstaller' 188 { $r = [System.Windows.Forms.MessageBox]::Show('Desinstaller le helper de synchronisation ? (le cache du dossier partage n''est pas touche)', 'Confirmer', 'YesNo', 'Question') if ($r -eq 'Yes') { try { Uninstall-Host; $result.ForeColor=[System.Drawing.Color]::DarkGreen; $result.Text='Desinstalle. Pense a retirer l''extension du navigateur.'; Refresh-Status } catch { $result.ForeColor=[System.Drawing.Color]::Firebrick; $result.Text=('Erreur : ' + $_.Exception.Message) } } } $form.Controls.Add($result) [System.Windows.Forms.Application]::EnableVisualStyles() [void]$form.ShowDialog()