installer-uri-router.ps1
|
<# .SYNOPSIS Installe, vérifie ou retire le routeur d'URI vscode://, et verrouille la clé de registre pour que VS Code ne puisse plus la reprendre. .DESCRIPTION VS Code enregistre le protocole vscode:// au nom de l'utilisateur, sans --user-data-dir. Mesuré le 10 août 2026 : il ne le fait pas seulement à l'installation, il réécrit la clé à CHAQUE démarrage d'instance. Poser une valeur ne suffit donc pas — elle ne survit pas au prochain lancement. La parade est une ACL : on refuse à l'utilisateur courant les droits SetValue et Delete sur la clé. VS Code tente sa réécriture, échoue en silence, et le routeur reste en place. L'utilisateur restant propriétaire de la clé, il conserve ChangePermissions : ce script peut toujours déverrouiller, et le verrou est réversible à tout moment. .PARAMETER Verifier N'écrit rien : affiche la valeur de la clé et l'état du verrou. .PARAMETER Restaurer Déverrouille et remet le gestionnaire d'origine livré par VS Code. .EXAMPLE .\installer-uri-router.ps1 -Verifier .EXAMPLE .\installer-uri-router.ps1 .NOTES Créé le 9 août 2026, verrou ACL ajouté le 10 août 2026. HKCU uniquement, aucun droit administrateur requis. Sauvegarde de la valeur d'origine : F:\Backups\vscode-uri-handler-2026-08-09\vscode-protocol-AVANT.reg #> [CmdletBinding(DefaultParameterSetName = 'Installer')] param( [Parameter(ParameterSetName = 'Verifier')][switch]$Verifier, [Parameter(ParameterSetName = 'Restaurer')][switch]$Restaurer ) $ErrorActionPreference = 'Stop' # Script autonome : T est interne au module, il faut sourcer la langue soi-meme. . (Join-Path $PSScriptRoot 'src' 'Langue.ps1') Set-CtxLangue | Out-Null $SousCle = 'Software\Classes\vscode\shell\open\command' $Key = "HKCU:\$SousCle" $Routeur = Join-Path $PSScriptRoot 'vscode-uri-router.ps1' # Resolution des executables : JAMAIS de chemin en dur. # # Ces deux lignes portaient le chemin de profil d'un utilisateur precis. # Le script ne fonctionnait donc que sur une seule machine, et publiait au # passage le nom d'utilisateur Windows de son auteur. Releve le 15 aout 2026. function Resolve-CtxExe { param([string[]]$Candidats, [string]$Commande) foreach ($c in $Candidats) { if ($c -and (Test-Path -LiteralPath $c -PathType Leaf)) { return $c } } if ($Commande) { # Depuis le lanceur bin/<nom>.cmd, remonter jusqu'a l'executable. $cli = Get-Command $Commande -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source $dossier = if ($cli) { Split-Path $cli -Parent } for ($i = 0; $i -lt 4 -and $dossier; $i++) { $exe = Get-ChildItem -LiteralPath $dossier -Filter '*.exe' -File -ErrorAction SilentlyContinue | Where-Object { $_.BaseName -eq $Commande } | Select-Object -First 1 if ($exe) { return $exe.FullName } $parent = Split-Path $dossier -Parent if ($parent -eq $dossier) { break } $dossier = $parent } } } $PwshExe = Resolve-CtxExe -Commande 'pwsh' -Candidats @( (Get-Process -Id $PID -ErrorAction SilentlyContinue).Path (Join-Path $env:ProgramFiles 'PowerShell\7\pwsh.exe') ) $CodeExe = Resolve-CtxExe -Commande 'code' -Candidats @( (Join-Path $env:LOCALAPPDATA 'Programs\Microsoft VS Code\Code.exe') (Join-Path $env:ProgramFiles 'Microsoft VS Code\Code.exe') (Join-Path ${env:ProgramFiles(x86)} 'Microsoft VS Code\Code.exe') ) $Attendu = '"{0}" -NoLogo -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "{1}" -Uri "%1"' -f $PwshExe, $Routeur $Origine = '"{0}" --open-url -- "%1"' -f $CodeExe # Droits refusés à VS Code. Delete est là au cas où une version future # tenterait de supprimer la clé plutôt que d'en réécrire la valeur. $DroitsBloques = @( [System.Security.AccessControl.RegistryRights]::SetValue, [System.Security.AccessControl.RegistryRights]::Delete ) function Get-Sid { [System.Security.Principal.WindowsIdentity]::GetCurrent().User } function Open-CleAcl { [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey( $SousCle, 'ReadWriteSubTree', [System.Security.AccessControl.RegistryRights]'ChangePermissions,ReadPermissions') } function Test-Verrou { if (-not (Test-Path -LiteralPath $Key)) { return $false } $k = Open-CleAcl try { $sid = Get-Sid $acl = $k.GetAccessControl([System.Security.AccessControl.AccessControlSections]::Access) foreach ($r in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) { if ($r.AccessControlType -eq 'Deny' -and $r.IdentityReference -eq $sid -and ($r.RegistryRights -band [System.Security.AccessControl.RegistryRights]::SetValue)) { return $true } } return $false } finally { $k.Close() } } function Set-Verrou { # Modifie les ACL d'une cle de registre : l'operation la moins reversible # de ce depot, et celle qui merite le plus un -WhatIf. [CmdletBinding(SupportsShouldProcess)] param([bool]$Actif) $action = if ($Actif) { 'poser le refus d ecriture' } else { 'retirer le refus d ecriture' } if (-not $PSCmdlet.ShouldProcess($Key, $action)) { return } $k = Open-CleAcl try { $sid = Get-Sid $acl = $k.GetAccessControl([System.Security.AccessControl.AccessControlSections]::Access) foreach ($d in $DroitsBloques) { $regle = New-Object System.Security.AccessControl.RegistryAccessRule( $sid, $d, [System.Security.AccessControl.InheritanceFlags]::None, [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Deny) if ($Actif) { $acl.AddAccessRule($regle) } else { $acl.RemoveAccessRuleAll($regle) } } $k.SetAccessControl($acl) } finally { $k.Close() } } function Get-Actuel { if (-not (Test-Path -LiteralPath $Key)) { return $null } (Get-ItemProperty -LiteralPath $Key).'(default)' } $actuel = Get-Actuel if ($Verifier) { $verrou = if (Test-Path -LiteralPath $Key) { Test-Verrou } else { $false } Write-Host "" Write-Host " $(T 'uri.cle' $Key)" Write-Host " $(T 'uri.valeur' $actuel)" Write-Host "" if ($actuel -eq $Attendu) { Write-Host " $(T 'uri.actif')" -ForegroundColor Green if ($verrou) { Write-Host " $(T 'uri.verrouActif')" -ForegroundColor Green } else { Write-Host " $(T 'uri.verrouAbsent')" -ForegroundColor Yellow } } elseif ($actuel -eq $Origine) { Write-Host " $(T 'uri.origine')" -ForegroundColor Yellow } else { Write-Host " $(T 'uri.inconnue')" -ForegroundColor Yellow } exit 0 } if ($Restaurer) { if (Test-Verrou) { Set-Verrou $false } Set-ItemProperty -LiteralPath $Key -Name '(default)' -Value $Origine Write-Host " $(T 'uri.restaure')" -ForegroundColor Green exit 0 } foreach ($f in @($PwshExe, $Routeur, $CodeExe)) { if (-not (Test-Path -LiteralPath $f)) { throw (T 'uri.introuvable' $f) } } if (-not (Test-Path -LiteralPath $Key)) { New-Item -Path $Key -Force | Out-Null } if ($actuel -eq $Attendu -and (Test-Verrou)) { Write-Host " $(T 'uri.dejaEnPlace')" -ForegroundColor Green exit 0 } # Une valeur qui n'est ni l'origine ni la nôtre mérite d'être conservée : un # autre outil a pu s'approprier le protocole entre-temps. if ($actuel -and $actuel -ne $Origine -and $actuel -ne $Attendu) { $sauv = Join-Path $env:LOCALAPPDATA ('DevContext\vscode-handler-remplace-{0}.txt' -f (Get-Date -Format 'yyyyMMdd-HHmmss')) New-Item -ItemType Directory -Force -Path (Split-Path $sauv -Parent) | Out-Null Set-Content -LiteralPath $sauv -Value $actuel -Encoding UTF8 Write-Host " $(T 'uri.valeurConservee')" -ForegroundColor Yellow Write-Host " $sauv" -ForegroundColor Yellow } # Le verrou bloque notre propre écriture : le retirer avant, le reposer après. if (Test-Verrou) { Set-Verrou $false } Set-ItemProperty -LiteralPath $Key -Name '(default)' -Value $Attendu Set-Verrou $true Write-Host " $(T 'uri.installe')" -ForegroundColor Green Write-Host " $(T 'uri.verifier')" |