Compare commits

...
8 changed files with 248 additions and 201 deletions

No files matched your search

+2
View File
@@ -3,3 +3,5 @@
.vscode/* .vscode/*
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
/tests/*
!/tests/test_update_manifests.py
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"version": "20261020T080626Z", "version": "20261109T091718Z",
"description": "MyElexis CLI", "description": "MyElexis CLI",
"url": "https://download.medelexis.ch/myelexis-cli/windows/myelexis-cli-1.0.0-SNAPSHOT-runner.exe#/myelexis-cli.exe", "url": "https://download.medelexis.ch/myelexis-cli/windows/myelexis-cli-1.0.0-SNAPSHOT-runner.exe#/myelexis-cli.exe",
"hash": "a397bae3e6c340f40b4b0a35ea22bafd3c30a02990e0b26bde7957a803902078", "hash": "b266ad9f0b01bebd0c15134096e011933aac6080af2b26a68b8467dbcc6e60b2",
"bin": [ "bin": [
[ [
"myelexis-cli.exe", "myelexis-cli.exe",
+12
View File
@@ -0,0 +1,12 @@
# Scoop Medelexis AG - Intern
Damit die PowerShell-Scripte ausgeführt werden können, muss im Terminal-Fenster zuerst `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass` eingegeben und ausgeführt werden.
**Scripts:**
* Install -> installiert Scoop und fügt die Buckets extras und medelexis hinzu
* update-bucket -> Migriert bestehende Buckets und Pakete auf medelexis *(kann mehrfach ausgeführt werden)
(ohne -Apply wird nur ein Probelauf durchgeführt. Wenn das Update angewendet werden soll, muss "update-bucket.ps1 -Apply" verwendet werden.)*
* setup-auto-update -> Richtet eine tägliche Scoop-Update-Aufgabe ein, repariert oder entfernt sie.
Andere Scripts sind System-Scripts und nicht für manuelle Ausführung gedacht.
+4 -4
View File
@@ -1,9 +1,9 @@
#requires -Version 5.1 #requires -Version 5.1
<# <#
.SYNOPSIS .SYNOPSIS
Aktualisiert Scoop und alle benutzerspezifischen Pakete; elexis* bleibt gesperrt. Aktualisiert Scoop und alle benutzerspezifischen Pakete; elexis* bleibt gesperrt.
.DESCRIPTION .DESCRIPTION
Fuehrt ausschliesslich den Update-Lauf aus. Die Aufgabe wird von update-bucket.ps1 Führt ausschliesslich den Update-Lauf aus. Die Aufgabe wird von update-bucket.ps1
eingerichtet und startet dieses Skript verborgen. Der letzte Lauf steht in eingerichtet und startet dieses Skript verborgen. Der letzte Lauf steht in
auto-update.log neben diesem Skript. Globale Pakete werden nicht aktualisiert. auto-update.log neben diesem Skript. Globale Pakete werden nicht aktualisiert.
#> #>
@@ -25,7 +25,7 @@ $exitCode = 0
try { try {
Start-Transcript -LiteralPath (Join-Path $PSScriptRoot 'auto-update.log') -Force | Out-Null Start-Transcript -LiteralPath (Join-Path $PSScriptRoot 'auto-update.log') -Force | Out-Null
$transcriptStarted = $true $transcriptStarted = $true
# Ein absoluter Scoop-Pfad und die shims im PATH funktionieren auch ohne Profil. # Ein absoluter Scoop-Pfad und die Shims im PATH funktionieren auch ohne Profil.
$env:PATH = (Join-Path $ScoopRoot 'shims') + [IO.Path]::PathSeparator + $env:PATH $env:PATH = (Join-Path $ScoopRoot 'shims') + [IO.Path]::PathSeparator + $env:PATH
$installed = @(& $scoopCommand list) $installed = @(& $scoopCommand list)
if ($LASTEXITCODE -ne 0) { throw 'Installierte Scoop-Pakete konnten nicht ermittelt werden.' } if ($LASTEXITCODE -ne 0) { throw 'Installierte Scoop-Pakete konnten nicht ermittelt werden.' }
@@ -33,7 +33,7 @@ try {
if ($package.Name -notlike 'elexis*' -or $package.Info -match 'Global install|Install failed') { continue } if ($package.Name -notlike 'elexis*' -or $package.Info -match 'Global install|Install failed') { continue }
& $scoopCommand hold $package.Name & $scoopCommand hold $package.Name
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "Updatesperre fuer '$($package.Name)' fehlgeschlagen. Kein Update ausgefuehrt." throw "Updatesperre für '$($package.Name)' fehlgeschlagen. Kein Update ausgeführt."
} }
} }
& $scoopCommand update & $scoopCommand update
+4 -4
View File
@@ -29,12 +29,12 @@ else {
} }
# 3. BUCKET CHECKS # 3. BUCKET CHECKS
if ($bucketList -match "elexis") { if ($bucketList -match '(?m)^\s*medelexis\s') {
Write-Host "elexis Bucket ist bereits vorhanden." -ForegroundColor White Write-Host "Medelexis-Bucket ist bereits vorhanden." -ForegroundColor White
} }
else { else {
scoop bucket add elexis https://gitea.medelexis.ch/public/scoop scoop bucket add medelexis https://gitea.medelexis.ch/public/scoop
Write-Host "elexis Bucket hinzugefügt." -ForegroundColor Green Write-Host "Medelexis-Bucket hinzugefügt." -ForegroundColor Green
} }
if ($bucketList -match "extras") { if ($bucketList -match "extras") {
+115 -21
View File
@@ -1,13 +1,14 @@
#requires -Version 5.1 #requires -Version 5.1
<# <#
.SYNOPSIS .SYNOPSIS
Fragt nach und richtet optional die taeglichen Scoop-Auto-Updates ein. Fragt nach und richtet optional die täglichen Scoop-Auto-Updates ein.
.DESCRIPTION .DESCRIPTION
Eigenstaendig ausfuehrbar, ohne Bucket-Migration. Bei y wird auto-update.ps1 Eigenständig ausführbar, ohne Bucket-Migration. Bei y wird auto-update.ps1
heruntergeladen und eine Aufgabe fuer taeglich 03:00 Uhr erstellt. heruntergeladen und eine Aufgabe für täglich 03:00 Uhr erstellt.
Die Aufgabe startet das Updateskript verborgen und holt verpasste Termine nach, Die Aufgabe startet das Updateskript verborgen und holt verpasste Termine nach,
sobald die Benutzersitzung und eine Netzwerkverbindung verfuegbar sind. sobald die Benutzersitzung und eine Netzwerkverbindung verfügbar sind.
Bei n wird nichts eingerichtet; bestehende Aufgaben bleiben unveraendert. Bestehende Aufgaben können nach Rückfrage repariert oder samt Updateskript
entfernt werden. Werden beide Fragen verneint, bleibt alles unverändert.
.EXAMPLE .EXAMPLE
.\scripts\setup-auto-update.ps1 .\scripts\setup-auto-update.ps1
#> #>
@@ -20,9 +21,48 @@ $ErrorActionPreference = 'Stop'
$ScoopRoot = [IO.Path]::GetFullPath($ScoopRoot) $ScoopRoot = [IO.Path]::GetFullPath($ScoopRoot)
$repository = 'https://gitea.medelexis.ch/public/scoop' $repository = 'https://gitea.medelexis.ch/public/scoop'
do { $userId = [Security.Principal.WindowsIdentity]::GetCurrent().Name
$autoUpdateAnswer = (Read-Host 'Scoop Auto-Update einschalten? [y/n]').Trim().ToLowerInvariant() $userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
} while ($autoUpdateAnswer -notin @('y', 'n')) $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') { if ($autoUpdateAnswer -eq 'y') {
$scoopCommand = Join-Path $ScoopRoot 'shims\scoop.ps1' $scoopCommand = Join-Path $ScoopRoot 'shims\scoop.ps1'
@@ -41,16 +81,67 @@ if ($autoUpdateAnswer -eq 'y') {
$downloadedScript = [Management.Automation.Language.Parser]::ParseFile($downloadPath, [ref] $parseTokens, [ref] $parseErrors) $downloadedScript = [Management.Automation.Language.Parser]::ParseFile($downloadPath, [ref] $parseTokens, [ref] $parseErrors)
if ($parseErrors.Count -gt 0 -or -not $downloadedScript.ParamBlock -or if ($parseErrors.Count -gt 0 -or -not $downloadedScript.ParamBlock -or
'ScoopRoot' -notin @($downloadedScript.ParamBlock.Parameters.Name.VariablePath.UserPath)) { 'ScoopRoot' -notin @($downloadedScript.ParamBlock.Parameters.Name.VariablePath.UserPath)) {
throw 'Das heruntergeladene Auto-Update-Skript ist kein gueltiges Updateskript.' throw 'Das heruntergeladene Auto-Update-Skript ist kein gültiges Updateskript.'
} }
Move-Item -LiteralPath $downloadPath -Destination $autoUpdateScript -Force Move-Item -LiteralPath $downloadPath -Destination $autoUpdateScript -Force
# Nur die Aufgabe einrichten; das Updateskript wird durch die Aufgabe gestartet. # Nur die Aufgabe einrichten; das Updateskript wird durch die Aufgabe gestartet.
$userId = [Security.Principal.WindowsIdentity]::GetCurrent().Name # Bei einer Reparatur zuerst eine laufende Aufgabe anhalten,
$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value # damit ihr Starter ersetzt werden kann.
$taskName = "Medelexis-Scoop-AutoUpdate-$userSid" foreach ($existingTask in $existingTasks) {
$powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' if ($existingTask.State -eq 'Running') {
$arguments = '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "{0}" -ScoopRoot "{1}"' -f $autoUpdateScript, $ScoopRoot Stop-ScheduledTask -TaskName $existingTask.TaskName
$action = New-ScheduledTaskAction -Execute $powershell -Argument $arguments }
}
# 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' $trigger = New-ScheduledTaskTrigger -Daily -At '03:00'
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited $principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable ` $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable `
@@ -58,12 +149,15 @@ if ($autoUpdateAnswer -eq 'y') {
-ExecutionTimeLimit (New-TimeSpan -Hours 4) -ExecutionTimeLimit (New-TimeSpan -Hours 4)
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger ` Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
-Principal $principal -Settings $settings -Force ` -Principal $principal -Settings $settings -Force `
-Description 'Scoop taeglich um 03:00 aktualisieren; verpasste Termine nachholen; elexis* auf hold setzen.' | Out-Null -Description 'Tägliche Aktualisierung der installierten Elemente (Elexis ausgeschlossen)' | Out-Null
Write-Host "Scoop Auto-Update eingeschaltet: $taskName (taeglich 03:00, verpasste Termine nachholen)." # Die frühere Aufgabe dieses Benutzers entfernen, damit nur ein Update läuft.
Write-Host "Ausfuehrung verborgen unter $userId, sobald der Benutzer angemeldet ist; kein Aufwecken des PCs." if (Get-ScheduledTask -TaskName $legacyTaskName -ErrorAction SilentlyContinue) {
Unregister-ScheduledTask -TaskName $legacyTaskName -Confirm:$false
}
Write-Host "Scoop-Auto-Update eingeschaltet: $taskName"
} finally { } finally {
if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath } if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath }
} }
} else { } elseif (-not $autoUpdateRemoved) {
Write-Host 'Auto-Update nicht eingerichtet. Eine eventuell bestehende Aufgabe bleibt unveraendert.' Write-Host 'Keine Änderung an Scoop-Auto-Update vorgenommen.'
} }
+109 -16
View File
@@ -9,7 +9,7 @@ Ein passender bestehender Bucket wird umbenannt. Danach werden elexis und streic
Andere Buckets wie main und extras bleiben bestehen. Andere Buckets wie main und extras bleiben bestehen.
Alte Paketnamen mit Unterstrich werden auf passende Manifestnamen mit Bindestrich umgestellt. Alte Paketnamen mit Unterstrich werden auf passende Manifestnamen mit Bindestrich umgestellt.
ee-iso/ee_iso und ee-cli werden deinstalliert. MyElexis CLI wird neu installiert. ee-iso/ee_iso und ee-cli werden deinstalliert. MyElexis CLI wird neu installiert.
Mit -Apply wird abschliessend gefragt, ob taegliche Auto-Updates um 03:00 Uhr Mit -Apply wird abschliessend gefragt, ob tägliche Auto-Updates um 03:00 Uhr
eingerichtet werden sollen. Das separate Skript wird aus dem Repository geladen. eingerichtet werden sollen. Das separate Skript wird aus dem Repository geladen.
.EXAMPLE .EXAMPLE
.\scripts\update-bucket.ps1 .\scripts\update-bucket.ps1
@@ -269,9 +269,48 @@ Write-Host "Fertig: $($changes.Count) install.json-Dateien auf medelexis umgeste
Write-Host "Paketnamen umgestellt: $($renames.Count)." Write-Host "Paketnamen umgestellt: $($renames.Count)."
Write-Host 'Von elexis/streichtech/medelexis bleibt nur medelexis bestehen. Prüfung: scoop status --local' Write-Host 'Von elexis/streichtech/medelexis bleibt nur medelexis bestehen. Prüfung: scoop status --local'
do { $userId = [Security.Principal.WindowsIdentity]::GetCurrent().Name
$autoUpdateAnswer = (Read-Host 'Scoop Auto-Update einschalten? [y/n]').Trim().ToLowerInvariant() $userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
} while ($autoUpdateAnswer -notin @('y', 'n')) $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') { if ($autoUpdateAnswer -eq 'y') {
$autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis' $autoUpdateDirectory = Join-Path $ScoopRoot 'scripts\medelexis'
@@ -286,16 +325,67 @@ if ($autoUpdateAnswer -eq 'y') {
$downloadedScript = [Management.Automation.Language.Parser]::ParseFile($downloadPath, [ref] $parseTokens, [ref] $parseErrors) $downloadedScript = [Management.Automation.Language.Parser]::ParseFile($downloadPath, [ref] $parseTokens, [ref] $parseErrors)
if ($parseErrors.Count -gt 0 -or -not $downloadedScript.ParamBlock -or if ($parseErrors.Count -gt 0 -or -not $downloadedScript.ParamBlock -or
'ScoopRoot' -notin @($downloadedScript.ParamBlock.Parameters.Name.VariablePath.UserPath)) { 'ScoopRoot' -notin @($downloadedScript.ParamBlock.Parameters.Name.VariablePath.UserPath)) {
throw 'Das heruntergeladene Auto-Update-Skript ist kein gueltiges Updateskript.' throw 'Das heruntergeladene Auto-Update-Skript ist kein gültiges Updateskript.'
} }
Move-Item -LiteralPath $downloadPath -Destination $autoUpdateScript -Force Move-Item -LiteralPath $downloadPath -Destination $autoUpdateScript -Force
# Nur die Aufgabe einrichten; das Updateskript wird durch die Aufgabe gestartet. # Nur die Aufgabe einrichten; das Updateskript wird durch die Aufgabe gestartet.
$userId = [Security.Principal.WindowsIdentity]::GetCurrent().Name # Bei einer Reparatur zuerst eine laufende Aufgabe anhalten,
$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value # damit ihr Starter ersetzt werden kann.
$taskName = "Medelexis-Scoop-AutoUpdate-$userSid" foreach ($existingTask in $existingTasks) {
$powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' if ($existingTask.State -eq 'Running') {
$arguments = '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "{0}" -ScoopRoot "{1}"' -f $autoUpdateScript, $ScoopRoot Stop-ScheduledTask -TaskName $existingTask.TaskName
$action = New-ScheduledTaskAction -Execute $powershell -Argument $arguments }
}
# 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' $trigger = New-ScheduledTaskTrigger -Daily -At '03:00'
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited $principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable ` $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable `
@@ -303,12 +393,15 @@ if ($autoUpdateAnswer -eq 'y') {
-ExecutionTimeLimit (New-TimeSpan -Hours 4) -ExecutionTimeLimit (New-TimeSpan -Hours 4)
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger ` Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
-Principal $principal -Settings $settings -Force ` -Principal $principal -Settings $settings -Force `
-Description 'Scoop taeglich um 03:00 aktualisieren; verpasste Termine nachholen; elexis* auf hold setzen.' | Out-Null -Description 'Tägliche Aktualisierung der installierten Elemente (Elexis ausgeschlossen)' | Out-Null
Write-Host "Scoop Auto-Update eingeschaltet: $taskName (taeglich 03:00, verpasste Termine nachholen)." # Die frühere Aufgabe dieses Benutzers entfernen, damit nur ein Update läuft.
Write-Host "Ausfuehrung verborgen unter $userId, sobald der Benutzer angemeldet ist; kein Aufwecken des PCs." if (Get-ScheduledTask -TaskName $legacyTaskName -ErrorAction SilentlyContinue) {
Unregister-ScheduledTask -TaskName $legacyTaskName -Confirm:$false
}
Write-Host "Scoop-Auto-Update eingeschaltet: $taskName"
} finally { } finally {
if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath } if (Test-Path -LiteralPath $downloadPath) { Remove-Item -LiteralPath $downloadPath }
} }
} else { } elseif (-not $autoUpdateRemoved) {
Write-Host 'Auto-Update nicht eingerichtet. Eine eventuell bestehende Aufgabe bleibt unveraendert.' Write-Host 'Keine Änderung an Scoop-Auto-Update vorgenommen.'
} }
-154
View File
@@ -1,154 +0,0 @@
#requires -Version 5.1
# Isolated behavioural tests: no real downloads, Scoop updates or scheduled tasks.
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path $PSScriptRoot -Parent
$fixtureRoot = Join-Path $PSScriptRoot ('.auto-update-test-' + [guid]::NewGuid())
$oldScoop = $env:SCOOP
$oldPath = $env:PATH
$oldFixture = $env:SCOOP_AUTO_UPDATE_TEST_ROOT
$oldFailure = $env:SCOOP_AUTO_UPDATE_TEST_FAILURE
function Assert-True($Condition, [string] $Message) {
if (-not $Condition) { throw $Message }
}
# These stand-ins capture scheduling settings without changing Windows.
function New-ScheduledTaskAction { param($Execute, $Argument) [pscustomobject]$PSBoundParameters }
function New-ScheduledTaskTrigger { param([switch]$Daily, $At) [pscustomobject]$PSBoundParameters }
function New-ScheduledTaskPrincipal { param($UserId, $LogonType, $RunLevel) [pscustomobject]$PSBoundParameters }
function New-ScheduledTaskSettingsSet {
param([switch]$StartWhenAvailable, [switch]$RunOnlyIfNetworkAvailable,
[switch]$AllowStartIfOnBatteries, [switch]$DontStopIfGoingOnBatteries,
$MultipleInstances, $ExecutionTimeLimit)
[pscustomobject]$PSBoundParameters
}
function Register-ScheduledTask {
param($TaskName, $Action, $Trigger, $Principal, $Settings, [switch]$Force, $Description)
$global:AutoUpdateTestTask = [pscustomobject]$PSBoundParameters
}
function Read-Host {
param($Prompt)
$global:AutoUpdateTestAnswers.Dequeue()
}
function Invoke-WebRequest {
param($Uri, $OutFile, [switch]$UseBasicParsing)
Assert-True ($Uri -in @('https://example.test/scoop/raw/branch/master/scripts/auto-update.ps1',
'https://gitea.medelexis.ch/public/scoop/raw/branch/master/scripts/auto-update.ps1')) 'Unexpected download URL.'
if ($global:AutoUpdateTestInvalidDownload) {
'This is not the setup script.' | Set-Content -LiteralPath $OutFile
} else {
@'
param([string] $ScoopRoot)
throw 'The downloaded runner must not execute during setup.'
'@ | Set-Content -LiteralPath $OutFile
}
}
try {
# Spaces in the installation path exercise the scheduled command's quoting.
$scoopRoot = Join-Path $fixtureRoot 'scoop with spaces'
$shims = Join-Path $scoopRoot 'shims'
New-Item -ItemType Directory -Path $shims -Force | Out-Null
$scriptPath = Join-Path $fixtureRoot 'auto-update.ps1'
Copy-Item -LiteralPath (Join-Path $projectRoot 'scripts\auto-update.ps1') -Destination $scriptPath
$env:SCOOP_AUTO_UPDATE_TEST_ROOT = $fixtureRoot
$commandLog = Join-Path $fixtureRoot 'commands.log'
@'
param([Parameter(ValueFromRemainingArguments = $true)][string[]] $ScoopArguments)
Add-Content -LiteralPath (Join-Path $env:SCOOP_AUTO_UPDATE_TEST_ROOT 'commands.log') -Value ($ScoopArguments -join ' ')
if ($env:SCOOP_AUTO_UPDATE_TEST_FAILURE -eq ($ScoopArguments -join ' ')) { exit 1 }
if ($ScoopArguments[0] -eq 'list') {
[pscustomobject]@{ Name = 'elexis'; Info = '' }
[pscustomobject]@{ Name = 'Elexis-test'; Info = 'Held package' }
[pscustomobject]@{ Name = 'myelexis-cli'; Info = '' }
[pscustomobject]@{ Name = 'git'; Info = '' }
[pscustomobject]@{ Name = 'elexis-global'; Info = 'Global install' }
[pscustomobject]@{ Name = 'elexis-broken'; Info = 'Install failed' }
}
exit 0
'@ | Set-Content -LiteralPath (Join-Path $shims 'scoop.ps1') -Encoding UTF8
& $scriptPath -ScoopRoot $scoopRoot
Assert-True ($LASTEXITCODE -eq 0) 'Expected successful update.'
Assert-True (((Get-Content $commandLog) -join '|') -eq 'list|hold elexis|hold Elexis-test|update|update *') 'Expected only local elexis* held before updates.'
Assert-True (Test-Path (Join-Path $fixtureRoot 'auto-update.log')) 'Expected update log.'
foreach ($failure in @('list', 'hold elexis', 'update', 'update *')) {
Remove-Item -LiteralPath $commandLog
$env:SCOOP_AUTO_UPDATE_TEST_FAILURE = $failure
# Expected errors are logged; do not let test ErrorActionPreference terminate the caller.
$ErrorActionPreference = 'Continue'
& $scriptPath -ScoopRoot $scoopRoot 2>$null
$result = $LASTEXITCODE
$ErrorActionPreference = 'Stop'
Assert-True ($result -eq 1) "Expected failure status for $failure."
$commands = @(Get-Content $commandLog)
Assert-True ($commands[-1] -eq $failure) "Must abort immediately after $failure."
}
# Exercise the actual opt-in/download block without running the bucket migration.
$tokens = $null
$errors = $null
$bucketAst = [Management.Automation.Language.Parser]::ParseFile((Join-Path $projectRoot 'scripts\update-bucket.ps1'), [ref]$tokens, [ref]$errors)
Assert-True ($errors.Count -eq 0) 'Invalid bucket script syntax.'
$optInBlock = [scriptblock]::Create((($bucketAst.EndBlock.Statements | Select-Object -Last 2 | ForEach-Object { $_.Extent.Text }) -join "`n"))
$repository = 'https://example.test/scoop'
Remove-Item -LiteralPath $commandLog
$global:AutoUpdateTestAnswers = [Collections.Generic.Queue[string]]::new()
$global:AutoUpdateTestAnswers.Enqueue('n')
& $optInBlock
$downloadDirectory = Join-Path $scoopRoot 'scripts\medelexis'
Assert-True (-not (Test-Path $downloadDirectory)) 'Declining must not download or set up anything.'
$global:AutoUpdateTestAnswers.Enqueue('invalid')
$global:AutoUpdateTestAnswers.Enqueue('Y')
& $optInBlock
$downloadedPath = Join-Path $downloadDirectory 'auto-update.ps1'
Assert-True (Test-Path $downloadedPath) 'Expected downloaded runner.'
$task = $global:AutoUpdateTestTask
Assert-True ($task.Trigger.Daily -and $task.Trigger.At -eq '03:00') 'Expected daily 03:00 schedule.'
Assert-True ($task.Settings.StartWhenAvailable -and $task.Settings.RunOnlyIfNetworkAvailable) 'Expected catch-up and network settings.'
Assert-True ($task.Settings.MultipleInstances -eq 'IgnoreNew') 'Expected overlap protection.'
Assert-True ($task.Principal.LogonType -eq 'Interactive' -and $task.Principal.RunLevel -eq 'Limited') 'Expected normal user session.'
Assert-True ($task.Action.Argument.Contains('-WindowStyle Hidden') -and $task.Action.Argument.Contains('-NonInteractive')) 'Expected background execution.'
Assert-True ($task.Action.Argument.Contains('-File "' + $downloadedPath + '"') -and $task.Action.Argument.Contains('-ScoopRoot "' + $scoopRoot + '"')) 'Expected quoted paths pointing to the downloaded runner.'
Assert-True (-not (Test-Path $commandLog)) 'Setup must not execute Scoop.'
$taskName = $task.TaskName
$global:AutoUpdateTestAnswers.Enqueue('y')
& $optInBlock
Assert-True ($global:AutoUpdateTestTask.TaskName -eq $taskName -and $global:AutoUpdateTestTask.Force) 'Repeated setup must replace the same task.'
$originalDownload = [IO.File]::ReadAllText($downloadedPath)
Remove-Variable AutoUpdateTestTask -Scope Global
$global:AutoUpdateTestInvalidDownload = $true
$global:AutoUpdateTestAnswers.Enqueue('y')
$rejected = $false
try { & $optInBlock } catch { $rejected = $true }
Assert-True $rejected 'Invalid download must be rejected.'
Assert-True ([IO.File]::ReadAllText($downloadedPath) -ceq $originalDownload) 'Invalid download must preserve the existing script.'
Assert-True ($null -eq (Get-Variable AutoUpdateTestTask -Scope Global -ErrorAction SilentlyContinue)) 'Invalid download must not register a task.'
Assert-True (@(Get-ChildItem $downloadDirectory -Filter '*.download').Count -eq 0) 'Temporary downloads must be removed.'
# The additional standalone installer must offer the same setup without migration.
$standaloneSetup = Join-Path $projectRoot 'scripts\setup-auto-update.ps1'
$global:AutoUpdateTestAnswers.Enqueue('n')
& $standaloneSetup -ScoopRoot $scoopRoot
Assert-True ($null -eq (Get-Variable AutoUpdateTestTask -Scope Global -ErrorAction SilentlyContinue)) 'Standalone n must not register a task.'
$global:AutoUpdateTestInvalidDownload = $false
$global:AutoUpdateTestAnswers.Enqueue('y')
& $standaloneSetup -ScoopRoot $scoopRoot
$task = $global:AutoUpdateTestTask
Assert-True ($task.TaskName -eq $taskName -and $task.Force) 'Standalone setup must use the same task.'
Assert-True ($task.Trigger.Daily -and $task.Trigger.At -eq '03:00' -and $task.Settings.StartWhenAvailable) 'Standalone setup must schedule daily 03:00 with catch-up.'
Assert-True ($task.Action.Argument.Contains('-WindowStyle Hidden') -and $task.Action.Argument.Contains('-File "' + $downloadedPath + '"')) 'Standalone task must invoke the downloaded runner hidden.'
Assert-True (-not (Test-Path $commandLog)) 'Standalone setup must not run Scoop or migrations.'
Write-Host 'PASS: schedule, repeat setup, hold filtering, command order, logging, failures, opt-in and download setup.'
} finally {
$env:SCOOP = $oldScoop
$env:PATH = $oldPath
$env:SCOOP_AUTO_UPDATE_TEST_ROOT = $oldFixture
$env:SCOOP_AUTO_UPDATE_TEST_FAILURE = $oldFailure
Remove-Variable AutoUpdateTestTask, AutoUpdateTestAnswers, AutoUpdateTestInvalidDownload -Scope Global -ErrorAction SilentlyContinue
# Delete only this test's unique directory, after checking its absolute parent.
if ((Test-Path -LiteralPath $fixtureRoot) -and
[IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($fixtureRoot)) -eq [IO.Path]::GetFullPath($PSScriptRoot) -and
[IO.Path]::GetFileName($fixtureRoot).StartsWith('.auto-update-test-')) {
Remove-Item -LiteralPath $fixtureRoot -Recurse -Force
}
}