Compare commits

..
Author SHA1 Message Date
Simon Daniel Streich 4bbde1dfe2 anpassungen 2026-10-21 12:37:59 +02:00
5 changed files with 361 additions and 75 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
+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
+113 -19
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'
$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 { do {
$autoUpdateAnswer = (Read-Host 'Scoop Auto-Update einschalten? [y/n]').Trim().ToLowerInvariant() $autoUpdateAnswer = (Read-Host 'Bestehende Scoop-Auto-Update-Aufgabe reparieren (ersetzen)? [y/n]').Trim().ToLowerInvariant()
} while ($autoUpdateAnswer -notin @('y', 'n')) } 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.'
} }
+107 -14
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'
$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 { do {
$autoUpdateAnswer = (Read-Host 'Scoop Auto-Update einschalten? [y/n]').Trim().ToLowerInvariant() $autoUpdateAnswer = (Read-Host 'Bestehende Scoop-Auto-Update-Aufgabe reparieren (ersetzen)? [y/n]').Trim().ToLowerInvariant()
} while ($autoUpdateAnswer -notin @('y', 'n')) } 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.'
} }
+135 -38
View File
@@ -1,4 +1,4 @@
#requires -Version 5.1 #requires -Version 5.1
# Isolated behavioural tests: no real downloads, Scoop updates or scheduled tasks. # Isolated behavioural tests: no real downloads, Scoop updates or scheduled tasks.
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$projectRoot = Split-Path $PSScriptRoot -Parent $projectRoot = Split-Path $PSScriptRoot -Parent
@@ -11,6 +11,14 @@ $oldFailure = $env:SCOOP_AUTO_UPDATE_TEST_FAILURE
function Assert-True($Condition, [string] $Message) { function Assert-True($Condition, [string] $Message) {
if (-not $Condition) { throw $Message } if (-not $Condition) { throw $Message }
} }
function Assert-HiddenTask($Task, [string] $Runner, [string] $ScoopRoot) {
Assert-True ($Task.TaskName -eq 'Medelexis AG - Scoop AutoUpdate') 'Unexpected task name.'
$description = 'T' + [char]0x00e4 + 'gliche Aktualisierung der installierten Elemente (Elexis ausgeschlossen)'
Assert-True ($Task.Description -ceq $description) 'Unexpected task description.'
$launcher = Join-Path (Split-Path $Runner -Parent) 'auto-update-hidden.exe'
Assert-True ($Task.Action.Execute -eq $launcher -and (Test-Path -LiteralPath $launcher)) 'Task must use the GUI launcher.'
Assert-True ($Task.Action.Argument -eq ('"{0}" "{1}"' -f $Runner, $ScoopRoot)) 'Expected quoted runner and Scoop root.'
}
# These stand-ins capture scheduling settings without changing Windows. # These stand-ins capture scheduling settings without changing Windows.
function New-ScheduledTaskAction { param($Execute, $Argument) [pscustomobject]$PSBoundParameters } function New-ScheduledTaskAction { param($Execute, $Argument) [pscustomobject]$PSBoundParameters }
@@ -25,13 +33,32 @@ function New-ScheduledTaskSettingsSet {
function Register-ScheduledTask { function Register-ScheduledTask {
param($TaskName, $Action, $Trigger, $Principal, $Settings, [switch]$Force, $Description) param($TaskName, $Action, $Trigger, $Principal, $Settings, [switch]$Force, $Description)
$global:AutoUpdateTestTask = [pscustomobject]$PSBoundParameters $global:AutoUpdateTestTask = [pscustomobject]$PSBoundParameters
$global:AutoUpdateTestTasks[$TaskName] = $global:AutoUpdateTestTask
}
function Get-ScheduledTask {
param($TaskName, $ErrorAction)
if ($global:AutoUpdateTestTasks.ContainsKey($TaskName)) { $global:AutoUpdateTestTasks[$TaskName] }
}
function Unregister-ScheduledTask {
param($TaskName, [switch]$Confirm)
$expected = 'Medelexis-Scoop-AutoUpdate-' + [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
Assert-True ($TaskName -in @($expected, 'Medelexis AG - Scoop AutoUpdate') -and -not $Confirm) 'Only remove the matching auto-update task.'
$global:AutoUpdateTestRemovedTask = $TaskName
$global:AutoUpdateTestTasks.Remove($TaskName)
}
function Stop-ScheduledTask {
param($TaskName)
$global:AutoUpdateTestStoppedTask = $TaskName
$global:AutoUpdateTestTasks[$TaskName].State = 'Ready'
} }
function Read-Host { function Read-Host {
param($Prompt) param($Prompt)
$global:AutoUpdateTestPrompts.Add($Prompt)
$global:AutoUpdateTestAnswers.Dequeue() $global:AutoUpdateTestAnswers.Dequeue()
} }
function Invoke-WebRequest { function Invoke-WebRequest {
param($Uri, $OutFile, [switch]$UseBasicParsing) param($Uri, $OutFile, [switch]$UseBasicParsing)
$global:AutoUpdateTestDownloads++
Assert-True ($Uri -in @('https://example.test/scoop/raw/branch/master/scripts/auto-update.ps1', 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.' 'https://gitea.medelexis.ch/public/scoop/raw/branch/master/scripts/auto-update.ps1')) 'Unexpected download URL.'
if ($global:AutoUpdateTestInvalidDownload) { if ($global:AutoUpdateTestInvalidDownload) {
@@ -46,7 +73,7 @@ throw 'The downloaded runner must not execute during setup.'
try { try {
# Spaces in the installation path exercise the scheduled command's quoting. # Spaces in the installation path exercise the scheduled command's quoting.
$scoopRoot = Join-Path $fixtureRoot 'scoop with spaces' $scoopRoot = Join-Path $fixtureRoot ('scoop with spaces ' + [char]0x00e4)
$shims = Join-Path $scoopRoot 'shims' $shims = Join-Path $scoopRoot 'shims'
New-Item -ItemType Directory -Path $shims -Force | Out-Null New-Item -ItemType Directory -Path $shims -Force | Out-Null
$scriptPath = Join-Path $fixtureRoot 'auto-update.ps1' $scriptPath = Join-Path $fixtureRoot 'auto-update.ps1'
@@ -85,66 +112,136 @@ exit 0
$commands = @(Get-Content $commandLog) $commands = @(Get-Content $commandLog)
Assert-True ($commands[-1] -eq $failure) "Must abort immediately after $failure." Assert-True ($commands[-1] -eq $failure) "Must abort immediately after $failure."
} }
# Exercise the actual opt-in/download block without running the bucket migration. # Test all entry points without migrating buckets or changing real tasks.
$tokens = $null $tokens = $null
$errors = $null $errors = $null
$bucketAst = [Management.Automation.Language.Parser]::ParseFile((Join-Path $projectRoot 'scripts\update-bucket.ps1'), [ref]$tokens, [ref]$errors) $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.' 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")) $setupStatements = @($bucketAst.EndBlock.Statements)
$setupStart = 0
while ($setupStart -lt $setupStatements.Count -and -not $setupStatements[$setupStart].Extent.Text.StartsWith('$userId =')) { $setupStart++ }
Assert-True ($setupStart -lt $setupStatements.Count) 'Setup block missing in bucket migration.'
$optInBlock = [scriptblock]::Create((($setupStatements[$setupStart..($setupStatements.Count - 1)] | ForEach-Object { $_.Extent.Text }) -join "`n"))
$repository = 'https://example.test/scoop' $repository = 'https://example.test/scoop'
Remove-Item -LiteralPath $commandLog Remove-Item -LiteralPath $commandLog
$global:AutoUpdateTestAnswers = [Collections.Generic.Queue[string]]::new() $global:AutoUpdateTestAnswers = [Collections.Generic.Queue[string]]::new()
$global:AutoUpdateTestAnswers.Enqueue('n') $global:AutoUpdateTestPrompts = [Collections.Generic.List[string]]::new()
& $optInBlock $global:AutoUpdateTestTasks = @{}
$global:AutoUpdateTestDownloads = 0
$downloadDirectory = Join-Path $scoopRoot 'scripts\medelexis' $downloadDirectory = Join-Path $scoopRoot 'scripts\medelexis'
Assert-True (-not (Test-Path $downloadDirectory)) 'Declining must not download or set up anything.' $downloadedPath = Join-Path $downloadDirectory 'auto-update.ps1'
$launcherPath = Join-Path $downloadDirectory 'auto-update-hidden.exe'
$taskName = 'Medelexis AG - Scoop AutoUpdate'
$legacyName = 'Medelexis-Scoop-AutoUpdate-' + [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$setupEntries = @($optInBlock, (Join-Path $projectRoot 'scripts\setup-auto-update.ps1'))
foreach ($entry in $setupEntries) {
$global:AutoUpdateTestPrompts.Clear()
$global:AutoUpdateTestAnswers.Enqueue('n')
& $entry -ScoopRoot $scoopRoot
Assert-True ($global:AutoUpdateTestPrompts.Count -eq 1 -and $global:AutoUpdateTestPrompts[0] -eq 'Scoop-Auto-Update einschalten? [y/n]') 'Missing task must offer normal setup.'
Assert-True ($global:AutoUpdateTestTasks.Count -eq 0) 'Declining initial setup must not register a task.'
$global:AutoUpdateTestInvalidDownload = $false
$global:AutoUpdateTestAnswers.Enqueue('invalid') $global:AutoUpdateTestAnswers.Enqueue('invalid')
$global:AutoUpdateTestAnswers.Enqueue('Y') $global:AutoUpdateTestAnswers.Enqueue('Y')
& $optInBlock & $entry -ScoopRoot $scoopRoot
$downloadedPath = Join-Path $downloadDirectory 'auto-update.ps1' $task = $global:AutoUpdateTestTasks[$taskName]
Assert-True (Test-Path $downloadedPath) 'Expected downloaded runner.' Assert-HiddenTask $task $downloadedPath $scoopRoot
$task = $global:AutoUpdateTestTask Assert-True ($task.Trigger.Daily -and $task.Trigger.At -eq '03:00' -and $task.Settings.StartWhenAvailable) 'Expected daily 03:00 with catch-up.'
Assert-True ($task.Trigger.Daily -and $task.Trigger.At -eq '03:00') 'Expected daily 03:00 schedule.' Assert-True ($task.Settings.RunOnlyIfNetworkAvailable -and $task.Settings.MultipleInstances -eq 'IgnoreNew') 'Expected network and overlap settings.'
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.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 (-not (Test-Path $commandLog)) 'Setup must not execute Scoop or migrations.'
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.' # No/no must leave task, runner and launcher byte-for-byte unchanged.
$taskName = $task.TaskName $downloads = $global:AutoUpdateTestDownloads
$runnerBefore = (Get-FileHash -LiteralPath $downloadedPath).Hash
$launcherBefore = (Get-FileHash -LiteralPath $launcherPath).Hash
$global:AutoUpdateTestPrompts.Clear()
$global:AutoUpdateTestAnswers.Enqueue('n')
$global:AutoUpdateTestAnswers.Enqueue('n')
& $entry -ScoopRoot $scoopRoot
Assert-True ($global:AutoUpdateTestPrompts.Count -eq 2 -and $global:AutoUpdateTestPrompts[0] -like '*reparieren*' -and $global:AutoUpdateTestPrompts[1] -like '*entfernen*') 'Expected repair then removal questions.'
Assert-True ($global:AutoUpdateTestTasks[$taskName] -eq $task -and $global:AutoUpdateTestDownloads -eq $downloads) 'No/no must preserve the task and avoid downloading.'
Assert-True ((Get-FileHash -LiteralPath $downloadedPath).Hash -eq $runnerBefore -and (Get-FileHash -LiteralPath $launcherPath).Hash -eq $launcherBefore) 'No/no must preserve files.'
# Repair downloads and replaces the existing task, without duplicating it.
$global:AutoUpdateTestPrompts.Clear()
$global:AutoUpdateTestAnswers.Enqueue('y') $global:AutoUpdateTestAnswers.Enqueue('y')
& $optInBlock & $entry -ScoopRoot $scoopRoot
Assert-True ($global:AutoUpdateTestTask.TaskName -eq $taskName -and $global:AutoUpdateTestTask.Force) 'Repeated setup must replace the same task.' Assert-True ($global:AutoUpdateTestPrompts.Count -eq 1 -and $global:AutoUpdateTestPrompts[0] -like '*reparieren*') 'Repair must ask only the repair question.'
$originalDownload = [IO.File]::ReadAllText($downloadedPath) Assert-True ($global:AutoUpdateTestDownloads -eq $downloads + 1 -and $global:AutoUpdateTestTasks.Count -eq 1) 'Repair must download and replace the existing task.'
Remove-Variable AutoUpdateTestTask -Scope Global Assert-HiddenTask $global:AutoUpdateTestTasks[$taskName] $downloadedPath $scoopRoot
# Failed repair must retain the prior runner and task.
$task = $global:AutoUpdateTestTasks[$taskName]
$global:AutoUpdateTestInvalidDownload = $true $global:AutoUpdateTestInvalidDownload = $true
$global:AutoUpdateTestAnswers.Enqueue('y') $global:AutoUpdateTestAnswers.Enqueue('y')
$rejected = $false $rejected = $false
try { & $optInBlock } catch { $rejected = $true } try { & $entry -ScoopRoot $scoopRoot } catch { $rejected = $true }
Assert-True $rejected 'Invalid download must be rejected.' 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 ($global:AutoUpdateTestTasks[$taskName] -eq $task -and (Get-FileHash -LiteralPath $downloadedPath).Hash -eq $runnerBefore) 'Failed repair must preserve the existing runner and task.'
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.' 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:AutoUpdateTestInvalidDownload = $false
# Also recognize the earlier task name, and remove only managed files.
$global:AutoUpdateTestTasks.Clear()
$global:AutoUpdateTestTasks[$legacyName] = [pscustomobject]@{ TaskName = $legacyName; State = 'Running' }
Set-Content -LiteralPath (Join-Path $downloadDirectory 'auto-update.log') -Value 'old log'
Set-Content -LiteralPath (Join-Path $downloadDirectory 'unrelated.txt') -Value 'keep'
$downloads = $global:AutoUpdateTestDownloads
$global:AutoUpdateTestAnswers.Enqueue('n')
$global:AutoUpdateTestAnswers.Enqueue('y') $global:AutoUpdateTestAnswers.Enqueue('y')
& $standaloneSetup -ScoopRoot $scoopRoot & $entry -ScoopRoot $scoopRoot
$task = $global:AutoUpdateTestTask Assert-True ($global:AutoUpdateTestTasks.Count -eq 0 -and $global:AutoUpdateTestStoppedTask -eq $legacyName) 'Remove must stop and unregister the existing legacy task.'
Assert-True ($task.TaskName -eq $taskName -and $task.Force) 'Standalone setup must use the same task.' Assert-True (-not (Test-Path $downloadedPath) -and -not (Test-Path $launcherPath) -and -not (Test-Path (Join-Path $downloadDirectory 'auto-update.log'))) 'Remove must delete the runner, launcher and log.'
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 ((Test-Path (Join-Path $downloadDirectory 'unrelated.txt')) -and $global:AutoUpdateTestDownloads -eq $downloads) 'Remove must preserve unrelated files and avoid downloading.'
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.' # Repair migrates the legacy name to the requested task name.
Write-Host 'PASS: schedule, repeat setup, hold filtering, command order, logging, failures, opt-in and download setup.' $global:AutoUpdateTestTasks[$legacyName] = [pscustomobject]@{ TaskName = $legacyName; State = 'Ready' }
$global:AutoUpdateTestAnswers.Enqueue('y')
& $entry -ScoopRoot $scoopRoot
Assert-True ($global:AutoUpdateTestTasks.Count -eq 1 -and $global:AutoUpdateTestTasks.ContainsKey($taskName)) 'Repair must migrate the legacy task.'
# Removal of the current name must offer the same behaviour.
$global:AutoUpdateTestAnswers.Enqueue('n')
$global:AutoUpdateTestAnswers.Enqueue('y')
& $entry -ScoopRoot $scoopRoot
Assert-True ($global:AutoUpdateTestTasks.Count -eq 0 -and -not (Test-Path $downloadedPath)) 'Remove must also handle the current task name.'
}
# Prepare one local launcher for the real hidden-process smoke test below.
$global:AutoUpdateTestAnswers.Enqueue('y')
& (Join-Path $projectRoot 'scripts\setup-auto-update.ps1') -ScoopRoot $scoopRoot
$task = $global:AutoUpdateTestTasks[$taskName]
# Run only a harmless local fixture through the actual GUI launcher. No Scoop
# or scheduler calls: verify quoting, hidden child window and exit propagation.
@'
param([string] $ScoopRoot)
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class AutoUpdateWindowTest {
[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr handle);
}
"@
$visible = [AutoUpdateWindowTest]::IsWindowVisible([AutoUpdateWindowTest]::GetConsoleWindow())
Set-Content -LiteralPath (Join-Path $ScoopRoot 'hidden-test.txt') -Value ([string]$visible)
exit 23
'@ | Set-Content -LiteralPath $downloadedPath -Encoding UTF8
$process = Start-Process -FilePath $task.Action.Execute -ArgumentList $task.Action.Argument -WindowStyle Hidden -PassThru
if (-not $process.WaitForExit(20000)) {
$process.Kill()
throw 'Hidden launcher did not finish within 20 seconds.'
}
Assert-True ($process.ExitCode -eq 23) 'Launcher must forward the runner exit code.'
Assert-True ((Get-Content -LiteralPath (Join-Path $scoopRoot 'hidden-test.txt')) -eq 'False') 'Runner must have no visible console window.'
Write-Host 'PASS: install, repair, no/no, removal, legacy tasks, hidden launcher, schedule, hold filtering and failures.'
} finally { } finally {
$env:SCOOP = $oldScoop $env:SCOOP = $oldScoop
$env:PATH = $oldPath $env:PATH = $oldPath
$env:SCOOP_AUTO_UPDATE_TEST_ROOT = $oldFixture $env:SCOOP_AUTO_UPDATE_TEST_ROOT = $oldFixture
$env:SCOOP_AUTO_UPDATE_TEST_FAILURE = $oldFailure $env:SCOOP_AUTO_UPDATE_TEST_FAILURE = $oldFailure
Remove-Variable AutoUpdateTestTask, AutoUpdateTestAnswers, AutoUpdateTestInvalidDownload -Scope Global -ErrorAction SilentlyContinue Remove-Variable AutoUpdateTestTask, AutoUpdateTestAnswers, AutoUpdateTestInvalidDownload, AutoUpdateTestRemovedTask, AutoUpdateTestTasks, AutoUpdateTestDownloads, AutoUpdateTestPrompts, AutoUpdateTestStoppedTask -Scope Global -ErrorAction SilentlyContinue
# Delete only this test's unique directory, after checking its absolute parent. # Delete only this test's unique directory, after checking its absolute parent.
if ((Test-Path -LiteralPath $fixtureRoot) -and if ((Test-Path -LiteralPath $fixtureRoot) -and
[IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($fixtureRoot)) -eq [IO.Path]::GetFullPath($PSScriptRoot) -and [IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($fixtureRoot)) -eq [IO.Path]::GetFullPath($PSScriptRoot) -and