#requires -RunAsAdministrator <# ============================================================================== BrowserDesk KnowledgeBase - Client reparieren (bereinigen & neu installieren) ============================================================================== KB-ID : KB-0001 Zweck : Einen BrowserDesk-Client, der auf einer alten Version haengt oder sich seltsam verhaelt ("Altlast"), bereinigen und mit der aktuellen Version frisch installieren. Kennung, Zugriffskennwort, Geraete- zertifikat und die Anmeldung im Mandanten bleiben erhalten. Symptome: haengt auf Altversion, Autoupdate wirkungslos, Host kommt nach Login/Reboot nicht sauber online, doppelte/streuende Installation. Ziel : Windows-Client. ADMINISTRATOR-Rechte noetig. Aufruf (auf dem Zielrechner, PowerShell als Admin): .\repair-browserdesk-client.ps1 # aktuelle Version, Anmeldung bleibt .\repair-browserdesk-client.ps1 -Version 20260714.2 .\repair-browserdesk-client.ps1 -KeepIdentity # %ProgramData%\BrowserDesk ganz behalten .\repair-browserdesk-client.ps1 -FullWipe # ALLES loeschen - Geraet verlaesst seinen Mandanten Was passiert: 1) Tasks BrowserDeskUnattended + BrowserDeskWatchdog stoppen + loeschen 2) Prozesse killen (BrowserDesk / browserdesk_client / bd-watchdog / bd-uia) 3) Bereinigen: Programmverzeichnis und warp-Cache (pro Benutzer UND im SYSTEM-Profil - der warp-Cache ist die haeufigste Altlast); in %ProgramData%\BrowserDesk alles ausser der Liste unten; in den Benutzer- Ordnern %LOCALAPPDATA%\BrowserDesk nur die Programmdateien einer frueheren Benutzer-Installation (Adressbuch, Verlauf und die Mandanten- Mitgliedschaft der Oberflaeche liegen im selben Ordner und bleiben) 4) Aktuelle EXE ueber den Relay-Proxy laden (sauberes https, kein 503) 5) --install-service: nach Program Files kopieren, Task anlegen, --service starten -> das Geraet meldet sich an wie vorher: dieselbe Kennung, dasselbe Kennwort, derselbe Mandant, dasselbe Relay Was in %ProgramData%\BrowserDesk bleibt (Vorgabe): identity.json Zugriffskennwort (die Kennung selbst ist fest, sie wird aus der MachineGuid berechnet) enrollment.json Anmeldung im Mandanten device-*.pem, device-*.txt Geraetezertifikat mit Relay-Bindung und mTLS-Endpunkt relay-address.txt, relays\ eingestelltes Relay und Relay-Historie host-settings.json Einstellungen am Geraet (Zustimmung, Rechte, eigenes Kennwort) managed-settings.json Einstellungen vom Verwalter proxy.json Proxy - ohne ihn erreicht ein Geraet hinter einem Proxy das Relay nicht autoupdate-enabled, incoming-enabled Schalter "Automatische Updates" / "Eingehende Verbindungen" Alles andere dort (Protokolle, Herzschlag, Update-Auftraege, Merker, Zwischenstaende) ist Zustand oder Altlast und wird geloescht. Warum (Befund 30.09.2026, docs/DIENST-HOST-OHNE-MANDANT.md): Bis dahin loeschte dieses Skript %ProgramData%\BrowserDesk ganz und versprach eine "neue Geraete-ID". Die Kennung ist aber fest: das Geraet kam mit DERSELBEN Kennung, einem NEUEN Kennwort und OHNE Mandant zurueck - still im oeffentlichen Topf "default". Ein Surface stand so eine Woche ausserhalb seines Mandanten, ohne dass es jemand merkte. -FullWipe (das fruehere Verhalten) loescht auch Kennwort, Anmeldung und Zertifikat. Danach meldet sich das Geraet mit derselben Kennung, neuem Kennwort und ohne Mandant in "default" an. Zurueck in den Mandanten nur ueber "Umzug" in der Konsole oder einen neuen Beitritt am Geraet. ASCII mit Absicht: Windows PowerShell 5.1 liest eine .ps1 ohne BOM als ANSI - Umlaute in den Ausgaben kaemen verstuemmelt an. Test: tests/kb/repair-plan.test.ps1 (liest Get-RepairPlan/Invoke-RepairPlan aus dieser Datei und prueft sie an einem Probeverzeichnis). ============================================================================== #> param( # "latest" = die aktuelle Version aus dem Feed. KEINE feste Nummer als Vorgabe: dieses Skript ist der # Weg, den man geht, wenn nichts mehr laeuft - es darf nicht selbst eine Altversion installieren und # sich dann auf genau das Autoupdate verlassen, das gerade defekt ist. [string]$Version = "latest", [string]$Url = "", # %ProgramData%\BrowserDesk ganz behalten (auch Protokolle, Merker, Zustimmungen). [switch]$KeepIdentity, # Alles loeschen - auch Kennwort, Anmeldung im Mandanten und Geraetezertifikat (Verhalten bis 30.09.2026). [switch]$FullWipe ) if ($KeepIdentity -and $FullWipe) { throw "-KeepIdentity und -FullWipe schliessen sich aus." } if (-not $Url) { $Url = "https://browserdesk.de/dl/client/$Version" } $ErrorActionPreference = "SilentlyContinue" function Say($m){ Write-Host "==> $m" -ForegroundColor Cyan } # Was eine Reparatur loescht und was bleibt. Liest nur Verzeichnisse, loescht nichts. # Anmeldung (Vorgabe): in %ProgramData%\BrowserDesk bleibt nur, was Kennwort, Anmeldung, Zertifikat, # Relay-Wahl, Einstellungen und Proxy traegt (Liste im Kopf). # Daten (-KeepIdentity): %ProgramData%\BrowserDesk bleibt ganz. # Alles (-FullWipe): alles weg, auch die Benutzer-Ordner %LOCALAPPDATA%\BrowserDesk. # Programmverzeichnis und warp-Cache gehen immer. Aus den Benutzer-Ordnern gehen sonst nur die Programmdateien einer # frueheren Benutzer-Installation - im selben Ordner liegen Adressbuch, Verlauf und tenant.json der Oberflaeche. function Get-RepairPlan { param( [string]$ProgramFilesDir, [string]$ProgramDataDir, [string[]]$LocalRoots, [ValidateSet('Anmeldung', 'Daten', 'Alles')][string]$Modus ) $behaltenNamen = @( 'identity.json', 'enrollment.json', 'device-cert.pem', 'device-key.pem', 'device-ca.pem', 'device-relay.txt', 'device-mtls.txt', 'device-mtls-sni.txt', 'relay-address.txt', 'relays', 'host-settings.json', 'managed-settings.json', 'proxy.json', 'autoupdate-enabled', 'incoming-enabled' ) $programmMuster = @('*.exe', '*.exe.version', '.install.lock') $loeschen = New-Object System.Collections.Generic.List[string] $behalten = New-Object System.Collections.Generic.List[string] $loeschen.Add($ProgramFilesDir) if (Test-Path -LiteralPath $ProgramDataDir -ErrorAction SilentlyContinue) { if ($Modus -eq 'Alles') { $loeschen.Add($ProgramDataDir) } elseif ($Modus -eq 'Daten') { $behalten.Add($ProgramDataDir) } else { foreach ($e in @(Get-ChildItem -LiteralPath $ProgramDataDir -Force -ErrorAction SilentlyContinue)) { # -contains vergleicht ohne Gross-/Kleinschreibung - wie das Dateisystem. if ($behaltenNamen -contains $e.Name) { $behalten.Add($e.FullName) } else { $loeschen.Add($e.FullName) } } } } foreach ($lr in $LocalRoots) { $bd = Join-Path $lr 'BrowserDesk' if (Test-Path -LiteralPath $bd -ErrorAction SilentlyContinue) { if ($Modus -eq 'Alles') { $loeschen.Add($bd) } else { foreach ($f in @(Get-ChildItem -LiteralPath $bd -File -Force -ErrorAction SilentlyContinue)) { $programm = $false foreach ($m in $programmMuster) { if ($f.Name -like $m) { $programm = $true } } if ($programm) { $loeschen.Add($f.FullName) } } $behalten.Add($bd) } } foreach ($w in @(Get-ChildItem -LiteralPath (Join-Path $lr 'warp\packages') -ErrorAction SilentlyContinue)) { if ($w.Name -like '*BrowserDesk*') { $loeschen.Add($w.FullName) } } } [pscustomobject]@{ Loeschen = @($loeschen | Select-Object -Unique); Behalten = @($behalten | Select-Object -Unique) } } function Invoke-RepairPlan($Plan) { foreach ($p in $Plan.Loeschen) { if (Test-Path -LiteralPath $p -ErrorAction SilentlyContinue) { Remove-Item -LiteralPath $p -Recurse -Force; Write-Host " geloescht: $p" } } foreach ($p in $Plan.Behalten) { Write-Host " behalten : $p" -ForegroundColor DarkGray } } $modus = if ($FullWipe) { 'Alles' } elseif ($KeepIdentity) { 'Daten' } else { 'Anmeldung' } if ($FullWipe) { Write-Warning ("-FullWipe loescht auch Kennwort, Anmeldung im Mandanten und Geraetezertifikat. Das Geraet meldet " + "sich danach mit derselben Kennung, NEUEM Kennwort und OHNE Mandant in 'default' an. Abbruch mit Strg+C (10 s).") Start-Sleep -Seconds 10 } # ---------- 1) STOPPEN + BEREINIGEN --------------------------------------------------- Say "Scheduled Tasks stoppen + entfernen" foreach ($t in @("BrowserDeskUnattended","BrowserDeskWatchdog")) { schtasks /End /TN $t 2>$null | Out-Null schtasks /Delete /TN $t /F 2>$null | Out-Null } Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -like "*BrowserDesk*" } | ForEach-Object { schtasks /End /TN $_.TaskName 2>$null; schtasks /Delete /TN $_.TaskName /F 2>$null } | Out-Null Say "Prozesse beenden" foreach ($p in @("BrowserDesk","browserdesk_client","bd-watchdog","bd-uia")) { Get-Process -Name $p -ErrorAction SilentlyContinue | Stop-Process -Force } Start-Sleep -Seconds 2 Say "Verzeichnisse bereinigen (Modus: $modus)" $localRoots = @() Get-ChildItem "C:\Users" -Directory -ErrorAction SilentlyContinue | ForEach-Object { $localRoots += "$($_.FullName)\AppData\Local" } $localRoots += "C:\Windows\System32\config\systemprofile\AppData\Local" $localRoots += "C:\Windows\SysWOW64\config\systemprofile\AppData\Local" $plan = Get-RepairPlan -ProgramFilesDir "$env:ProgramFiles\BrowserDesk" -ProgramDataDir "$env:ProgramData\BrowserDesk" ` -LocalRoots $localRoots -Modus $modus Invoke-RepairPlan $plan Say "Bereinigung fertig." # ---------- 2) NEU INSTALLIEREN ------------------------------------------------------- $tmp = "$env:TEMP\BrowserDesk-$Version.exe" Say "Download $Version von $Url" [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-WebRequest -Uri $Url -OutFile $tmp -UseBasicParsing $mb = [math]::Round((Get-Item $tmp).Length/1MB,1) if ($mb -lt 5) { throw "Download zu klein ($mb MB) - Abbruch." } Say "Geladen: $tmp ($mb MB)" Say "Signatur pruefen" $sig = Get-AuthenticodeSignature $tmp Write-Host " Status=$($sig.Status) Signer=$($sig.SignerCertificate.Subject)" if ($sig.Status -ne 'Valid') { Write-Warning "Signatur NICHT gueltig - trotzdem fortfahren? (Abbruch mit Strg+C)"; Start-Sleep 5 } Say "Dienst installieren (--install-service) + starten" $p = Start-Process -FilePath $tmp -ArgumentList "--install-service" -Wait -PassThru Write-Host " ExitCode=$($p.ExitCode)" Start-Sleep -Seconds 5 Say "Verifikation" schtasks /Query /TN BrowserDeskUnattended /FO LIST 2>$null | Select-String "Taskname|TaskName|Status" Get-Process -Name "browserdesk_client","BrowserDesk" -ErrorAction SilentlyContinue | Select-Object Name, Id | Format-Table -Auto if ($modus -eq 'Alles') { Say "Fertig. Das Geraet meldet sich in ~30-60 s mit DERSELBEN Kennung, NEUEM Kennwort und OHNE Mandant in 'default' an." Write-Warning "Zurueck in den Mandanten: Konsole -> Geraet -> Umzug, oder am Geraet neu beitreten." } else { Say "Fertig. Das Geraet meldet sich in ~30-60 s an wie vorher: dieselbe Kennung, dasselbe Kennwort, derselbe Mandant." }