Install swap: first Move-Item of InstallRoot fails "file in use" right after the stop (host still exiting) #908
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found in the 2026-11-13 QA deploy of
76e73e7d(recorddocs/deploy/2026-11-13-vd03-qa-to-76e73e7d.md).What happened.
Invoke-OtOpcUaInstallSwap.ps1 -NoStart, run on QA with the service up, failed at step 3 on both nodes. The firstMove-Item E:\ApiInstall\OtOpcUa\app -> app.bak.20261113-pre-76e73e7dthrewThe process cannot access the file because it is being used by another process.WaitForStatus('Stopped')returned at 12:35:57.08Z, and the move failed at the same moment.State after the failure was safe. A directory rename fails as a whole, so
app\was intact, the candidate was still staged, there was noPreviousInstall, and the service was Stopped. A retry went through on both nodes:app\found nothing.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.ScriptWorkerprocess 0.5 s after the failure, waited 2 s and re-ran the swap.So the holder lives less than a second after SCM reports Stopped. The likely cause is the host process (or a worker child) still tearing down after it reported
SERVICE_STOPPED. The pre-#905 deploy wrappers waited for theOtOpcUa.Hostand worker processes to exit after the stop, so they never hit this. vd03 (single node, flat layout) did not hit it.Cost. On vq03 the retry re-ran
-Scope Outside(~20 s) inside the pair outage. That outage was 36.5 s; the move itself would have taken about 15 s.Fix. In step 2/3 of
Invoke-OtOpcUaInstallSwap.ps1:WaitForStatus('Stopped'), wait (bounded, e.g. 30 s) for the service's process to exit (Win32_Service.ProcessId, thenWait-Process), and for anyZB.MOM.WW.OtOpcUa.ScriptWorkerwhose image is under InstallRoot;Move-Itema few times with a short back-off when it fails with a sharing violation, since nothing has moved yet.Add a scratch test that holds a handle under InstallRoot for ~1 s after the stop.
Fixed on
masterina157b276.Change (
scripts/install/Invoke-OtOpcUaInstallSwap.ps1+ScriptWorkerIsolation.psm1)WaitForStatus('Stopped'), the swap now waits for the service's host process to exit (PID fromWin32_Service.ProcessId+ its start time, both captured before the stop, so a reused PID does not count). It also waits for every process whose image is underInstallRoot(OtOpcUa.Host, the ScriptWorker). The wait is bounded by-ProcessExitTimeoutSeconds(default 60). On a timeout it throws, naming each process (name, PID, path), and nothing is moved. The window line gains anexit N spart.[IO.Directory]::Moveinstead ofMove-Item. On windev, PowerShell 7'sMove-Itemhandles a rename that fails because a file under the folder is open (IOException 0x80070020, the QA message) by falling back to an item-by-item move. That left a partialPreviousInstalltree behind (app.bak\logs, and on a second tryapp.bak\app\logs). A retry would then nest into it.Directory.Moveis one rename and fails as a whole (IOException 0x80070005, nothing created) on both pwsh 7 and 5.1. So the QA "nothing moved" observation could have been luck, or 5.1.-MoveRetrySeconds(default 10) while it fails because something underInstallRootis open. This is a backstop for a holder whose image is outsideInstallRoot. Other errors are not retried.-Verify/-Reapply/-Scopebehaviour is unchanged.docs/ServiceHosting.mdand the76e73e7drecord are updated.Tests
Test-ScriptWorkerIsolation.ps1(dry run, any OS): 145 passed, 0 failed. New: holder detection from fake process lists (host by PID, also with an unreadable path; worker underapp\; the candidateapp.new-*and a reused PID are not holders). The wait returns once the holders are gone and times out within its bound, naming them. The sharing-violation detector covers all three windev shapes. The retry covers in use ×2 then success, a non-sharing error, and-MoveRetrySeconds 0. AST checks: PID capture → stop → wait → retried first rename → candidate rename, and noMove-Itemin the script. Mutations: pre-fix script gives 4 failures,Move-Itemreintroduced gives 3, wait removed gives 1, the wait helper as a no-op gives 3, holder detection ignoring the PID gives 1, and the retry or detector disabled gives 1 and 3.Test-ScriptWorkerIsolation.Windows.ps1(windev, elevated, scratch tree). The stand-in holder is a copy of powershell.exe underInstallRootthat holdslogs\host908.logopen. A running image alone does not block the rename; an open file does (checked on windev). Three new cases:-MoveRetrySeconds 0: "released after 2.3 s" (pwsh) / "2.6 s" (5.1), swapped and verified.InstallRoot: moved on attempt 6, and the previous install moved whole.Results: pwsh 7.6 77/0, Windows PowerShell 5.1 77/0. With the wait removed (mutant), cases 1 and 2 fail: case 2's move fails "in use" exactly as on QA.
Nothing was run on the wonder boxes.