#requires -Version 5.1 <# .SYNOPSIS Fragt nach und richtet optional die täglichen Scoop-Auto-Updates ein. .DESCRIPTION Eigenständig ausführbar, ohne Bucket-Migration. Bei y wird auto-update.ps1 heruntergeladen und eine Aufgabe für täglich 03:00 Uhr erstellt. Die Aufgabe startet das Updateskript verborgen und holt verpasste Termine nach, sobald die Benutzersitzung und eine Netzwerkverbindung verfügbar sind. Bestehende Aufgaben können nach Rückfrage repariert oder samt Updateskript entfernt werden. Werden beide Fragen verneint, bleibt alles unverändert. .EXAMPLE .\scripts\setup-auto-update.ps1 #> [CmdletBinding()] param( [string] $ScoopRoot = $(if ($env:SCOOP) { $env:SCOOP } else { Join-Path $env:USERPROFILE 'scoop' }) ) $ErrorActionPreference = 'Stop' $ScoopRoot = [IO.Path]::GetFullPath($ScoopRoot) $repository = 'https://gitea.medelexis.ch/public/scoop' $userId = [Security.Principal.WindowsIdentity]::GetCurrent().Name $userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value $taskName = 'Medelexis AG - Scoop AutoUpdate' $legacyTaskName = "Medelexis-Scoop-AutoUpdate-$userSid" $autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis' $existingTasks = @( foreach ($name in @($taskName, $legacyTaskName)) { Get-ScheduledTask -TaskName $name -ErrorAction SilentlyContinue } ) $autoUpdateRemoved = $false if ($existingTasks.Count -gt 0) { do { $autoUpdateAnswer = (Read-Host 'Bestehende Scoop-Auto-Update-Aufgabe reparieren (ersetzen)? [y/n]').Trim().ToLowerInvariant() } while ($autoUpdateAnswer -notin @('y', 'n')) if ($autoUpdateAnswer -eq 'n') { do { $removeAnswer = (Read-Host 'Scoop-Auto-Update entfernen? [y/n]').Trim().ToLowerInvariant() } while ($removeAnswer -notin @('y', 'n')) if ($removeAnswer -eq 'y') { foreach ($existingTask in $existingTasks) { if ($existingTask.State -eq 'Running') { Stop-ScheduledTask -TaskName $existingTask.TaskName } Unregister-ScheduledTask -TaskName $existingTask.TaskName -Confirm:$false } # Nur die zugehörigen Dateien entfernen, niemals den Ordner rekursiv. foreach ($fileName in @('auto-update.ps1', 'auto-update-hidden.exe', 'auto-update.log')) { $filePath = Join-Path $autoUpdateDirectory $fileName if (Test-Path -LiteralPath $filePath -PathType Leaf) { Remove-Item -LiteralPath $filePath -Force } } $autoUpdateRemoved = $true Write-Host 'Scoop-Auto-Update entfernt (Aufgabe und zugehörige Dateien).' } } } else { do { $autoUpdateAnswer = (Read-Host 'Scoop-Auto-Update einschalten? [y/n]').Trim().ToLowerInvariant() } while ($autoUpdateAnswer -notin @('y', 'n')) } if ($autoUpdateAnswer -eq 'y') { $scoopCommand = Join-Path $ScoopRoot 'shims\scoop.ps1' if (-not (Test-Path -LiteralPath $scoopCommand -PathType Leaf)) { throw "Scoop wurde nicht gefunden: $scoopCommand" } $autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis' New-Item -ItemType Directory -Path $autoUpdateDirectory -Force | Out-Null $autoUpdateScript = Join-Path $autoUpdateDirectory 'auto-update.ps1' $downloadPath = Join-Path $autoUpdateDirectory ('auto-update-{0}.download' -f [guid]::NewGuid()) try { $autoUpdateUrl = "$repository/raw/branch/master/scripts/auto-update.ps1" Invoke-WebRequest -Uri $autoUpdateUrl -OutFile $downloadPath -UseBasicParsing $parseTokens = $null $parseErrors = $null $downloadedScript = [Management.Automation.Language.Parser]::ParseFile($downloadPath, [ref] $parseTokens, [ref] $parseErrors) if ($parseErrors.Count -gt 0 -or -not $downloadedScript.ParamBlock -or 'ScoopRoot' -notin @($downloadedScript.ParamBlock.Parameters.Name.VariablePath.UserPath)) { throw 'Das heruntergeladene Auto-Update-Skript ist kein gültiges Updateskript.' } Move-Item -LiteralPath $downloadPath -Destination $autoUpdateScript -Force # Nur die Aufgabe einrichten; das Updateskript wird durch die Aufgabe gestartet. # Bei einer Reparatur zuerst eine laufende Aufgabe anhalten, # damit ihr Starter ersetzt werden kann. foreach ($existingTask in $existingTasks) { if ($existingTask.State -eq 'Running') { Stop-ScheduledTask -TaskName $existingTask.TaskName } } # Ein GUI-Programm startet PowerShell ohne Konsole, auch wenn Windows # Terminal als Standardterminal eingerichtet ist. Es wartet auf das Ende # des Updates und reicht den Exitcode weiter (Fehlerstatus / IgnoreNew). $launcherPath = Join-Path $autoUpdateDirectory 'auto-update-hidden.exe' $launcherSource = @' using System; using System.Diagnostics; public static class ScoopAutoUpdateLauncher { [STAThread] public static int Main(string[] args) { if (args.Length != 2) return 2; try { var start = new ProcessStartInfo(); start.FileName = System.IO.Path.Combine(Environment.GetEnvironmentVariable("SystemRoot"), @"System32\WindowsPowerShell\v1.0\powershell.exe"); start.Arguments = "-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" + args[0] + "\" -ScoopRoot \"" + args[1] + "\""; start.UseShellExecute = false; start.CreateNoWindow = true; start.WindowStyle = ProcessWindowStyle.Hidden; using (var child = Process.Start(start)) { child.WaitForExit(); return child.ExitCode; } } catch { return 1; } } } '@ $compiledLauncher = Join-Path $autoUpdateDirectory ('auto-update-{0}.exe' -f [guid]::NewGuid()) $launcherSourcePath = [IO.Path]::ChangeExtension($compiledLauncher, '.cs') # Der .NET-Framework-Compiler erzeugt eine Windows-Anwendung auch unter # PowerShell 7, wo Add-Type -OutputType WindowsApplication nicht existiert. $compiler = @( (Join-Path $env:SystemRoot 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'), (Join-Path $env:SystemRoot 'Microsoft.NET\Framework\v4.0.30319\csc.exe') ) | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1 if (-not $compiler) { throw 'Der Windows-.NET-Framework-Compiler wurde nicht gefunden.' } try { [IO.File]::WriteAllText($launcherSourcePath, $launcherSource, [Text.Encoding]::UTF8) $compilerOutput = & $compiler /nologo /target:winexe "/out:$compiledLauncher" $launcherSourcePath 2>&1 if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $compiledLauncher -PathType Leaf)) { throw "Der fensterlose Starter konnte nicht erstellt werden: $($compilerOutput | Out-String)" } Move-Item -LiteralPath $compiledLauncher -Destination $launcherPath -Force } finally { foreach ($temporaryFile in @($compiledLauncher, $launcherSourcePath)) { if (Test-Path -LiteralPath $temporaryFile) { Remove-Item -LiteralPath $temporaryFile } } } $action = New-ScheduledTaskAction -Execute $launcherPath -Argument ('"{0}" "{1}"' -f $autoUpdateScript, $ScoopRoot.TrimEnd('\')) $trigger = New-ScheduledTaskTrigger -Daily -At '03:00' $principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable ` -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew ` -ExecutionTimeLimit (New-TimeSpan -Hours 4) Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger ` -Principal $principal -Settings $settings -Force ` -Description 'Tägliche Aktualisierung der installierten Elemente (Elexis ausgeschlossen)' | Out-Null # Die frühere Aufgabe dieses Benutzers entfernen, damit nur ein Update läuft. if (Get-ScheduledTask -TaskName $legacyTaskName -ErrorAction SilentlyContinue) { Unregister-ScheduledTask -TaskName $legacyTaskName -Confirm:$false } Write-Host "Scoop-Auto-Update eingeschaltet: $taskName" } finally { if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath } } } elseif (-not $autoUpdateRemoved) { Write-Host 'Keine Änderung an Scoop-Auto-Update vorgenommen.' }