fix(install): worker isolation survives a folder swap: recorded settings, -Reapply, -Verify, swap script (#901)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
An upgrade that swaps the install folder dropped the explicit ScriptWorker isolation ACEs on script-worker\, the install folder and a copied-back pki\ (vd03 2026-11-13). Re-applying needed the operator to repeat the arguments. - Install-ScriptWorkerIsolation.ps1 records its effective settings (InstallRoot, HostServiceAccount, PeCacheDir, HostServiceName, ConfigureHostService) beside DenyPaths under ...\Services\OtOpcUaScriptWorker\Parameters (new plan step SettingsRecord) and locks that key to SYSTEM + Administrators (protected DACL; no secrets recorded). - -Reapply: re-apply from the record, no arguments; prints a summary line (changed/unchanged). - -Verify: check only, [ok]/[MISSING] per step, exit 1 when anything is missing. -Uninstall also works from the record. A node isolated by a pre-#901 installer (DenyPaths only) must run the full-argument install once; both modes say so. - Invoke-OtOpcUaInstallSwap.ps1: preflight -> stop -> move live/candidate -> carry appsettings/pki/logs -> -Reapply -> -Verify -> start; a failed -Verify leaves the service stopped. Also the rollback (-NoCarry). - Tests: dry-run harness 101/101 (record + argument rules; record-key SDDL mutation turns 2 red). The Windows test adds swap -> Verify fails (exactly the 3 lost ACEs) -> Reapply (3 changed) -> Verify ok -> Reapply 12/12 unchanged, PE-cache and firewall negative controls, the swap script, and the pre-#901 refusal; 52/52 on windev under 5.1 and 7, with a scratch local host account and with SYSTEM. Evidence: scripts/validation/opcua-windows-service/evidence/2026-11-13-windev-901-reapply-verify. - Docs: ServiceHosting isolation section; vd03 pack (swap step, rollback, post-deploy gate, one-time full-argument run), the 2026-11-13 vd03 record, and the QA pack (QA swap preceded the isolation install; ACLs checked intact over SMB on vq03/vq04 2026-11-13; next-deploy procedure). Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Windows-executed test of Install-ScriptWorkerIsolation.ps1 (lmxopcua#883, #890): apply -> re-apply -> verify -> shorter
|
||||
-DenyPaths -> uninstall (from the record, without -DenyPaths) -> uninstall. ACL level only; the restricted-token proof that
|
||||
Windows-executed test of Install-ScriptWorkerIsolation.ps1 (lmxopcua#883, #890, #901): apply -> re-apply -> verify -> shorter
|
||||
-DenyPaths -> -Verify -> folder swap -> -Verify fails -> -Reapply -> -Verify passes -> -Reapply unchanged ->
|
||||
Invoke-OtOpcUaInstallSwap.ps1 (re-apply + verify built in; refuses a pre-#901 record) -> uninstall (from the record,
|
||||
without -DenyPaths) -> uninstall. ACL level only; the restricted-token proof that
|
||||
the worker cannot read the denied folders is InstallerLayoutIsolationTests (ScriptWorker.Tests, OTOPCUA_ISOLATION_INSTALLER_E2E=1).
|
||||
The dry-run harness (Test-ScriptWorkerIsolation.ps1) only asserts plan data; it cannot see host-specific failures such as
|
||||
DirectoryInfo.GetAccessControl missing on PowerShell 7, or StrictMode .Count on a scalar on Windows PowerShell 5.1.
|
||||
@@ -12,13 +14,15 @@
|
||||
|
||||
.DESCRIPTION
|
||||
Needs an elevated Windows session. It works in a scratch folder under $env:TEMP with a scratch worker service name and a
|
||||
scratch stand-in host service (a disabled service that is never started), and uses -HostServiceAccount 'NT AUTHORITY\SYSTEM'.
|
||||
scratch stand-in host service (a disabled service that is never started), and uses -HostServiceAccount 'NT AUTHORITY\SYSTEM',
|
||||
or with -LocalHostAccount a scratch local user (random password, never shown) that is deleted at the end.
|
||||
It does NOT touch OtOpcUaHost. The firewall rule name is fixed by the installer, so the test refuses to run (exit 2) when
|
||||
that rule already exists, i.e. on a node that has the real isolation installed. Everything it creates is removed in
|
||||
`finally`, including on failure.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param([string]$WorkerServiceName = 'OtOpcUaIsoTestWorker', [string]$HostServiceName = 'OtOpcUaIsoTestHost')
|
||||
param([string]$WorkerServiceName = 'OtOpcUaIsoTestWorker', [string]$HostServiceName = 'OtOpcUaIsoTestHost', [switch]$LocalHostAccount,
|
||||
[string]$LocalHostAccountName = 'otopcuaIsoTestAcct')
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -34,6 +38,8 @@ if (Get-NetFirewallRule -Name $ruleName -ErrorAction SilentlyContinue) { Write-H
|
||||
foreach ($n in $WorkerServiceName, $HostServiceName) {
|
||||
if (Get-Service -Name $n -ErrorAction SilentlyContinue) { Write-Host "refusing: service $n already exists"; exit 2 }
|
||||
}
|
||||
if ($LocalHostAccount -and (Get-LocalUser -Name $LocalHostAccountName -ErrorAction SilentlyContinue)) { Write-Host "refusing: local user $LocalHostAccountName already exists"; exit 2 }
|
||||
$script:hostAccount = 'NT AUTHORITY\SYSTEM'
|
||||
|
||||
$script:failures = 0; $script:passes = 0
|
||||
function Assert([bool]$Condition, [string]$Message) {
|
||||
@@ -43,11 +49,27 @@ function Invoke-Installer([string[]]$Extra) {
|
||||
# $Extra: e.g. @('-DenyPaths', 'a,b') or @('-Uninstall'); -File passes an array parameter as one string, which the installer splits.
|
||||
# Same PowerShell host as this test; the installer's output lines are ' [outcome ] StepId target'.
|
||||
$exe = if ($PSVersionTable.PSEdition -eq 'Core') { (Get-Process -Id $PID).Path } else { 'powershell.exe' }
|
||||
$out = & $exe -NoProfile -ExecutionPolicy Bypass -File $installer -InstallRoot $root -HostServiceAccount 'NT AUTHORITY\SYSTEM' `
|
||||
$out = & $exe -NoProfile -ExecutionPolicy Bypass -File $installer -InstallRoot $root -HostServiceAccount $script:hostAccount `
|
||||
-PeCacheDir $cache -WorkerServiceName $WorkerServiceName -HostServiceName $HostServiceName -ConfigureHostService @Extra 2>&1 | Out-String
|
||||
$steps = @([regex]::Matches($out, '\[(?<o>[a-z]+)\s*\]\s+(?<id>[A-Za-z]+)\s') | ForEach-Object { [pscustomobject]@{ Outcome = $_.Groups['o'].Value; Id = $_.Groups['id'].Value } })
|
||||
[pscustomobject]@{ Exit = $LASTEXITCODE; Output = $out; Steps = $steps }
|
||||
}
|
||||
function Invoke-Mode([string]$Mode) {
|
||||
# lmxopcua#901: -Reapply / -Verify with NO settings argument, only the scratch worker name (the record selector).
|
||||
$exe = if ($PSVersionTable.PSEdition -eq 'Core') { (Get-Process -Id $PID).Path } else { 'powershell.exe' }
|
||||
# Continue: an expected failure writes to stderr, which Stop would turn into a terminating error here. Whitespace is
|
||||
# collapsed (and pwsh's '|' margins dropped) because both hosts wrap the error text at the console width.
|
||||
$out = & { $ErrorActionPreference = 'Continue'; & $exe -NoProfile -ExecutionPolicy Bypass -File $installer "-$Mode" -WorkerServiceName $WorkerServiceName 2>&1 | Out-String }
|
||||
$steps = @([regex]::Matches($out, '\[(?<o>[a-z]+)\s*\]\s+(?<id>[A-Za-z]+)\s+(?<t>\S+)') | ForEach-Object { [pscustomobject]@{ Outcome = $_.Groups['o'].Value; Id = $_.Groups['id'].Value; Target = $_.Groups['t'].Value } })
|
||||
$code = $LASTEXITCODE
|
||||
$missing = @([regex]::Matches($out, '\[MISSING\]\s+(?<id>[A-Za-z]+)\s+(?<t>\S+)') | ForEach-Object { "$($_.Groups['id'].Value) $($_.Groups['t'].Value)" })
|
||||
[pscustomobject]@{ Exit = $code; Output = $out; Flat = ($out -replace '[\s|]+', ' '); Steps = $steps; Missing = $missing }
|
||||
}
|
||||
function Show([string]$Label, $R) {
|
||||
# The transcript the issue asks for: one line per run.
|
||||
$what = if ($R.Missing.Count) { "MISSING: $($R.Missing -join ' | ')" } else { ($R.Steps | Group-Object Outcome | ForEach-Object { "$($_.Count) $($_.Name)" }) -join ', ' }
|
||||
Write-Host (" {0,-34} exit {1}; {2}" -f $Label, $R.Exit, $what)
|
||||
}
|
||||
function Get-Explicit([string]$Path, [string]$Sid) {
|
||||
$id = [System.Security.Principal.SecurityIdentifier]::new($Sid)
|
||||
@((Get-Acl -LiteralPath $Path).GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier]) | Where-Object { $_.IdentityReference -eq $id })
|
||||
@@ -56,7 +78,8 @@ function Get-Explicit([string]$Path, [string]$Sid) {
|
||||
$scratch = Join-Path ([IO.Path]::GetTempPath()) ("otopcua-iso-test-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||
$root = Join-Path $scratch 'install'
|
||||
$cache = Join-Path $scratch 'pe-cache'
|
||||
$deny = @($scratch, (Join-Path $scratch 'pki'), (Join-Path $scratch 'data')) # lmxopcua#890: the deployment folder + keys + secrets
|
||||
# lmxopcua#890: the deployment folder + keys + secrets. pki\ inside the install root, as on vd03 (lmxopcua#901: the swap re-creates it).
|
||||
$deny = @($scratch, (Join-Path $root 'pki'), (Join-Path $scratch 'data'))
|
||||
$recordKey = "HKLM:\SYSTEM\CurrentControlSet\Services\$WorkerServiceName\Parameters"
|
||||
$workerExe = Join-Path $root 'script-worker\ZB.MOM.WW.OtOpcUa.ScriptWorker.exe'
|
||||
$workerSid = Get-OtOpcUaServiceSid -Name $WorkerServiceName
|
||||
@@ -65,10 +88,17 @@ try {
|
||||
New-Item -ItemType Directory -Path (Split-Path $workerExe) -Force | Out-Null
|
||||
Set-Content -LiteralPath $workerExe -Value 'stub'
|
||||
foreach ($d in $deny) { New-Item -ItemType Directory -Path $d -Force | Out-Null }
|
||||
if ($LocalHostAccount) {
|
||||
$pw = [Security.SecureString]::new()
|
||||
foreach ($ch in ([Convert]::ToBase64String((1..24 | ForEach-Object { [byte](Get-Random -Maximum 256) })) + 'aA1!').ToCharArray()) { $pw.AppendChar($ch) }
|
||||
New-LocalUser -Name $LocalHostAccountName -Password $pw -AccountNeverExpires -Description 'lmxopcua#901 isolation test (scratch)' | Out-Null
|
||||
$script:hostAccount = ".\$LocalHostAccountName"
|
||||
}
|
||||
Set-Content -LiteralPath (Join-Path $root 'appsettings.json') -Value '{}'
|
||||
& sc.exe create $HostServiceName binPath= "$env:SystemRoot\System32\cmd.exe" start= disabled | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "sc.exe create $HostServiceName failed" }
|
||||
Set-ItemProperty -Path $hostKey -Name Environment -Type MultiString -Value ([string[]]@('Keep__Me=1'))
|
||||
Write-Host "Host: $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion); scratch $scratch"
|
||||
Write-Host "Host: $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion); scratch $scratch; host account $script:hostAccount"
|
||||
|
||||
Write-Host 'Dry run'
|
||||
$dry = Invoke-Installer @('-WhatIf', '-DenyPaths', ($deny -join ','))
|
||||
@@ -79,7 +109,7 @@ try {
|
||||
Write-Host 'Apply'
|
||||
$a = Invoke-Installer @('-DenyPaths', ($deny -join ','))
|
||||
Assert ($a.Exit -eq 0) "apply exits 0 ($($a.Output))"
|
||||
Assert ($a.Steps.Count -eq 11 -and @($a.Steps | Where-Object Outcome -ne 'changed').Count -eq 0) "apply: 11 steps, all changed ($(($a.Steps | ForEach-Object { "$($_.Id)=$($_.Outcome)" }) -join ','))"
|
||||
Assert ($a.Steps.Count -eq 12 -and @($a.Steps | Where-Object Outcome -ne 'changed').Count -eq 0) "apply: 12 steps, all changed ($(($a.Steps | ForEach-Object { "$($_.Id)=$($_.Outcome)" }) -join ','))"
|
||||
Assert ((Get-CimInstance Win32_Service -Filter "Name='$WorkerServiceName'").StartMode -eq 'Disabled') 'anchor service is Disabled'
|
||||
Assert ((& sc.exe showsid $WorkerServiceName | Out-String) -match [regex]::Escape($workerSid)) 'anchor SID matches Get-OtOpcUaServiceSid'
|
||||
Assert ((Get-CimInstance Win32_Service -Filter "Name='$WorkerServiceName'").PathName -eq "`"$workerExe`"") 'anchor binPath is stored quoted (Windows PowerShell 5.1 used to drop the quotes)'
|
||||
@@ -107,7 +137,7 @@ try {
|
||||
Write-Host 'Re-apply (idempotent)'
|
||||
$b = Invoke-Installer @('-DenyPaths', ($deny -join ','))
|
||||
Assert ($b.Exit -eq 0) "re-apply exits 0 ($($b.Output))"
|
||||
Assert ($b.Steps.Count -eq 11 -and @($b.Steps | Where-Object Outcome -ne 'unchanged').Count -eq 0) "re-apply: 11 steps, all unchanged ($(($b.Steps | ForEach-Object { "$($_.Id)=$($_.Outcome)" }) -join ','))"
|
||||
Assert ($b.Steps.Count -eq 12 -and @($b.Steps | Where-Object Outcome -ne 'unchanged').Count -eq 0) "re-apply: 12 steps, all unchanged ($(($b.Steps | ForEach-Object { "$($_.Id)=$($_.Outcome)" }) -join ','))"
|
||||
|
||||
Write-Host 'Re-apply with data\ dropped from -DenyPaths: its deny goes, the others stay'
|
||||
$c = Invoke-Installer @('-DenyPaths', (($deny | Select-Object -First 2) -join ','))
|
||||
@@ -118,6 +148,81 @@ try {
|
||||
$c2 = Invoke-Installer @('-DenyPaths', ($deny -join ','))
|
||||
Assert ($c2.Exit -eq 0 -and @($c2.Steps | Where-Object Outcome -eq 'changed').Count -eq 2) 'putting data\ back changes its deny and the record only'
|
||||
|
||||
Write-Host 'Settings record (lmxopcua#901)'
|
||||
$recVals = Get-ItemProperty -Path $recordKey
|
||||
Assert ($recVals.InstallRoot -eq $root -and $recVals.HostServiceAccount -eq $script:hostAccount -and $recVals.PeCacheDir -eq $cache -and
|
||||
$recVals.HostServiceName -eq $HostServiceName -and $recVals.ConfigureHostService -eq 1) 'install root, host account, PE cache, host service and the flag are recorded'
|
||||
Assert (Test-OtOpcUaRecordKeyAcl -Key $recordKey) "record key DACL is protected SYSTEM + Administrators ($((Get-Acl -Path $recordKey).GetSecurityDescriptorSddlForm('Access')))"
|
||||
$settings = Get-OtOpcUaIsolationSettings -WorkerServiceName $WorkerServiceName
|
||||
Assert ($null -ne $settings -and (@($settings.DenyPaths) -join '|') -eq ($deny -join '|') -and $settings.ConfigureHostService) 'Get-OtOpcUaIsolationSettings reads it back'
|
||||
|
||||
Write-Host 'Swap -> Verify fails -> Reapply -> Verify passes -> Reapply unchanged (lmxopcua#901)'
|
||||
$v0 = Invoke-Mode 'Verify'; Show 'Verify (installed)' $v0
|
||||
Assert ($v0.Exit -eq 0 -and $v0.Output -match 'VERIFY OK') "verify passes on a fresh install ($($v0.Output))"
|
||||
# The swap as the deploy packs did it: build the candidate beside the live folder, move live aside, move candidate in,
|
||||
# copy pki\ and appsettings back (robocopy without ACLs, as a new folder).
|
||||
$cand = Join-Path $scratch 'install.new'
|
||||
New-Item -ItemType Directory -Path (Join-Path $cand 'script-worker') -Force | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $cand 'script-worker\ZB.MOM.WW.OtOpcUa.ScriptWorker.exe') -Value 'stub v2'
|
||||
$prev1 = Join-Path $scratch 'install.bak.1'
|
||||
Move-Item -LiteralPath $root -Destination $prev1
|
||||
Move-Item -LiteralPath $cand -Destination $root
|
||||
& robocopy (Join-Path $prev1 'pki') (Join-Path $root 'pki') /E /COPY:DAT /R:0 /W:0 /NFL /NDL /NJH /NJS /NP | Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $prev1 'appsettings.json') -Destination $root
|
||||
$v1 = Invoke-Mode 'Verify'; Show 'Verify (after the swap)' $v1
|
||||
Assert ($v1.Exit -eq 1 -and $v1.Output -match 'VERIFY FAILED') "verify exits 1 after the swap ($($v1.Exit))"
|
||||
$expectMissing = @("WorkerFolderAcl $root\script-worker", "InstallRootDeny $root", "SensitiveDirDeny $root\pki")
|
||||
Assert ((@($v1.Missing | Sort-Object) -join '|') -eq (@($expectMissing | Sort-Object) -join '|')) "exactly the three folder ACEs the swap drops are MISSING ($($v1.Missing -join ' | '))"
|
||||
$r1 = Invoke-Mode 'Reapply'; Show 'Reapply (no arguments)' $r1
|
||||
Assert ($r1.Exit -eq 0 -and $r1.Output -match 'settings from the record') "reapply exits 0 and says it used the record ($($r1.Output))"
|
||||
$r1Changed = @($r1.Steps | Where-Object Outcome -eq 'changed' | ForEach-Object { "$($_.Id) $($_.Target)" })
|
||||
Assert ($r1.Steps.Count -eq 12 -and (@($r1Changed | Sort-Object) -join '|') -eq (@($expectMissing | Sort-Object) -join '|')) "reapply changes exactly the missing three ($($r1Changed -join ' | '))"
|
||||
$v2 = Invoke-Mode 'Verify'; Show 'Verify (after Reapply)' $v2
|
||||
Assert ($v2.Exit -eq 0 -and $v2.Output -match 'VERIFY OK') "verify passes after the reapply ($($v2.Exit))"
|
||||
$r2 = Invoke-Mode 'Reapply'; Show 'Reapply again' $r2
|
||||
Assert ($r2.Exit -eq 0 -and $r2.Steps.Count -eq 12 -and @($r2.Steps | Where-Object Outcome -ne 'unchanged').Count -eq 0) "second reapply: 12 steps, all unchanged ($(($r2.Steps | ForEach-Object Outcome) -join ','))"
|
||||
Assert ($r2.Output -match 'summary: 12 step\(s\), 0 changed, 12 unchanged') 'the summary line says 0 changed'
|
||||
|
||||
Write-Host 'Negative control: Verify catches each kind of loss'
|
||||
& icacls $cache /grant '*S-1-5-32-545:(OI)(CI)(RX)' /Q | Out-Null # Users on the PE cache root
|
||||
Remove-NetFirewallRule -Name $ruleName
|
||||
$v3 = Invoke-Mode 'Verify'; Show 'Verify (cache ACL + rule broken)' $v3
|
||||
Assert ($v3.Exit -eq 1 -and ($v3.Missing -join '|') -match 'PeCacheDir' -and ($v3.Missing -join '|') -match 'FirewallBlockOutbound') "verify flags the PE cache ACL and the missing rule ($($v3.Missing -join ' | '))"
|
||||
$r3 = Invoke-Mode 'Reapply'; $v4 = Invoke-Mode 'Verify'; Show 'Verify (after Reapply)' $v4
|
||||
Assert ($r3.Exit -eq 0 -and $v4.Exit -eq 0) 'reapply repairs them'
|
||||
|
||||
Write-Host 'Invoke-OtOpcUaInstallSwap.ps1: swap + Reapply + Verify in one step (no service control in the scratch test)'
|
||||
$swapScript = Join-Path (Split-Path -Parent $PSScriptRoot) 'Invoke-OtOpcUaInstallSwap.ps1'
|
||||
$exe = if ($PSVersionTable.PSEdition -eq 'Core') { (Get-Process -Id $PID).Path } else { 'powershell.exe' }
|
||||
$cand2 = Join-Path $scratch 'install.new2'
|
||||
New-Item -ItemType Directory -Path (Join-Path $cand2 'script-worker') -Force | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $cand2 'script-worker\ZB.MOM.WW.OtOpcUa.ScriptWorker.exe') -Value 'stub v3'
|
||||
$prev2 = Join-Path $scratch 'install.bak.2'
|
||||
$sw = & $exe -NoProfile -ExecutionPolicy Bypass -File $swapScript -InstallRoot $root -Candidate $cand2 -PreviousInstall $prev2 `
|
||||
-Carry 'appsettings*.json,pki' -NoServiceControl -WorkerServiceName $WorkerServiceName 2>&1 | Out-String
|
||||
$swExit = $LASTEXITCODE
|
||||
Write-Host (" {0,-34} exit {1}; {2}" -f 'Invoke-OtOpcUaInstallSwap', $swExit, $(if ($sw -match 'VERIFY OK') { 'VERIFY OK after its Reapply' } else { 'no VERIFY OK' }))
|
||||
Assert ($swExit -eq 0 -and $sw -match 'VERIFY OK' -and $sw -match 'Swap complete') "swap script exits 0 with the gate passed ($sw)"
|
||||
Assert ((Get-Content -LiteralPath (Join-Path $root 'script-worker\ZB.MOM.WW.OtOpcUa.ScriptWorker.exe')) -eq 'stub v3' -and (Test-Path (Join-Path $root 'pki')) -and
|
||||
(Test-Path (Join-Path $root 'appsettings.json'))) 'the candidate is live and pki\ + appsettings were carried'
|
||||
Assert (@(Get-Explicit $root $workerSid).Count -eq 1 -and @(Get-Explicit (Join-Path $root 'script-worker') $workerSid).Count -eq 1 -and
|
||||
@(Get-Explicit (Join-Path $root 'pki') $workerSid).Count -eq 1) 'the three folder ACEs are back on the swapped-in folder'
|
||||
|
||||
Write-Host 'Swap refuses a pre-#901 record (DenyPaths only) before changing anything'
|
||||
$saved = (Get-ItemProperty -Path $recordKey).InstallRoot
|
||||
Remove-ItemProperty -Path $recordKey -Name InstallRoot
|
||||
$cand3 = Join-Path $scratch 'install.new3'
|
||||
New-Item -ItemType Directory -Path (Join-Path $cand3 'script-worker') -Force | Out-Null
|
||||
$sw2 = & { $ErrorActionPreference = 'Continue'; & $exe -NoProfile -ExecutionPolicy Bypass -File $swapScript -InstallRoot $root -Candidate $cand3 `
|
||||
-PreviousInstall (Join-Path $scratch 'install.bak.3') -NoServiceControl -WorkerServiceName $WorkerServiceName 2>&1 | Out-String }
|
||||
$sw2Exit = $LASTEXITCODE
|
||||
Write-Host (" {0,-34} exit {1}; {2}" -f 'Invoke-OtOpcUaInstallSwap (pre-#901)', $sw2Exit, $(if (($sw2 -replace '[\s|]+', ' ') -match 'older than lmxopcua#901') { 'refused: settings not recorded' } else { 'NOT refused' }))
|
||||
Assert ($sw2Exit -ne 0 -and ($sw2 -replace '[\s|]+', ' ') -match 'older than lmxopcua#901' -and (Test-Path $cand3) -and -not (Test-Path (Join-Path $scratch 'install.bak.3'))) "refused, nothing moved ($sw2)"
|
||||
$r4 = Invoke-Mode 'Reapply'
|
||||
Show 'Reapply (pre-#901 record)' $r4
|
||||
Assert ($r4.Exit -ne 0 -and $r4.Flat -match 'full arguments') "-Reapply without a full record refuses and says how to fix it ($($r4.Exit))"
|
||||
Set-ItemProperty -Path $recordKey -Name InstallRoot -Value $saved
|
||||
|
||||
Write-Host 'Uninstall WITHOUT -DenyPaths (the record drives it), twice'
|
||||
$u1 = Invoke-Installer @('-Uninstall')
|
||||
Assert ($u1.Exit -eq 0) "uninstall exits 0 ($($u1.Output))"
|
||||
@@ -142,6 +247,7 @@ finally {
|
||||
foreach ($n in $WorkerServiceName, $HostServiceName) {
|
||||
if (Get-Service -Name $n -ErrorAction SilentlyContinue) { & sc.exe delete $n | Out-Null }
|
||||
}
|
||||
if ($LocalHostAccount -and (Get-LocalUser -Name $LocalHostAccountName -ErrorAction SilentlyContinue)) { Remove-LocalUser -Name $LocalHostAccountName }
|
||||
if (Test-Path -LiteralPath $scratch) {
|
||||
& icacls $scratch /reset /T /C /Q 2>&1 | Out-Null
|
||||
Remove-Item -LiteralPath $scratch -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Reference in New Issue
Block a user