Skip to content

Fix four setup and engine defects found in the CM1 lab freeze test - #213

Merged
Sev7eNup merged 1 commit into
mainfrom
fix/setup-provisioning-uninstall-and-junction-cancel
Aug 15, 2026
Merged

Fix four setup and engine defects found in the CM1 lab freeze test#213
Sev7eNup merged 1 commit into
mainfrom
fix/setup-provisioning-uninstall-and-junction-cancel

Conversation

@Sev7eNup

Copy link
Copy Markdown
Owner

Found during the pre-freeze live test on the CM1 lab server. All four reproduce on a real installation, and all four are verified fixed on the same machine — not only in unit tests.

What was wrong

Unattended install skipped all provisioning once a host already had NodePilot. AnswerMode came from IsUpdateSelected(), which reads ModePage.SelectedValueIndex — and /ANSWERFILE skips the mode page, so the index kept its hard default of 0 (update). An answer file saying "mode": "install" therefore dropped every provisioning.* key on any second install, SCCM re-deploy or golden-image refresh. Measured before the fix, with a marker written by a perfectly ordinary previous install:

expected actual
adapter modes InitSession → Provision → Apply InitSession → Apply
certificates in LocalMachine\My 8 → 9 8 → 8
target database created not created
setup exit 0 7

Database provisioning never received -CertificateHostName. The script declares the parameter and the installer builds HostNameInCertificate from the same answer-file key, but the provisioning call dropped it, so it derived the name from -Server. Same server, database and principal, only the parameter differs:

call result
-Server localhost Fail — The target principal name is incorrect
-Server localhost -CertificateHostName CM1.corp.contoso.com Pass — database created, db_owner granted

A failed provisioning was invisible unattended. It exits 0 and reports itself inside provision.ini, which the readiness page reads and the silent path did not — so the run walked on to Apply and died in the SQL pre-flight telling the operator to have a DBA create a login that was never the problem.

The uninstaller ignored serviceName, installPath and dataPath. It passed GetServiceName('') — the literal NodePilot in the uninstaller process — and {app}, which is only where Inno put the uninstaller, and never passed -DataPath at all. A non-default installation was "uninstalled" with exit 0 while this stayed behind:

after a "successful" uninstall
service still installed and running
its port still listening
program files still present
data directory not purged, despite /PURGEDATA=1
firewall rule still there
ARP entry, marker, {app} removed

Windows believed NodePilot was gone while it kept running and serving.

waitAny/waitNofM marked whole runs Failed when the losing branches were runScript. Standing a branch down is what the junction is for, and StepRunner records those as Cancelled — but only if the exception reaches it. The PowerShell engines converted the cancellation into an ordinary failed result, and one Failed step fails the execution. Same junction semantics, two branch activity types, run back to back:

loser branch step status message execution
delay Cancelled another branch already satisfied the workflow junction Succeeded
runScript Failed Script execution cancelled Failed

Every other step in the failing run was green. This also skewed GET /{id}/coverage, which counts junction-race cancellations under skippedCount.

The fixes

  • The answer file decides whether provisioning runs on the /ANSWERFILE path. Update mode accepts no provisioning keys, so a Provision run for one performs no action and exits 0 — the same reasoning the surrounding code already used for running it unconditionally.
  • Pass -CertificateHostName through to Provision-NodePilotDatabase.ps1; an absent key keeps the script's own fallback.
  • The silent path reads provision.database.status, exactly as the readiness page does.
  • The uninstaller reads ServiceName, InstallPath and DataPath back from HKLM\SOFTWARE\NodePilot\Server, falling back to the old guesses only when the marker is gone.
  • All four cancellation sites rethrow on caller cancellation — including the two catch-alls that re-wrapped the throw as Isolated execution failed: Script execution cancelled, which was the same red step under a new name. A timeout stays a failure with TimedOut set. Remote/WinRM is covered too.

Tests

  • WorkflowEngineTests — the junction contract end to end: loser Cancelled, no Failed step, execution Succeeded.
  • RunspaceEngineAsyncTests / ProcessIsolationEngineTests — cancel throws, timeout still returns a failed result. Both previously asserted the defective behaviour.
  • Test-DeploymentTemplates.ps1 — static contracts for the provisioning gate, the provisioning verdict, the certificate host name, and the three uninstall parameters, plus negative contracts against the exact old code. Three existing contracts were anchored on the gate line that was itself the bug and moved with it.

Ran: full NodePilot.Engine.Tests (1832 passed), Test-DeploymentTemplates.ps1, Test-SetupAdapter.ps1 (213 assertions). Everything else is covered by CI.

Verified live on CM1

Rebuilt and re-run against the failing cases: provisioning now runs with a previous installation present (InitSession → Provision → Apply, certificate generated, database created via localhost), a deliberately broken provisioning aborts naming the real TLS cause, a non-default install uninstalls completely with /PURGEDATA=1, and the junction runbook that reported Failed now reports Succeeded with its losers Cancelled.

Still open from the same test round and deliberately not in this PR: the update path does not refresh the installation marker, and Inno's {app} does not follow the answer file's installPath.

All four reproduce on a real installation and are verified fixed on the same
machine, not only in unit tests.

Unattended install skipped all provisioning once a host already had NodePilot.
AnswerMode came from IsUpdateSelected(), which reads ModePage.SelectedValueIndex
- and /ANSWERFILE skips the mode page, so the index kept its hard default of 0
('update'). An answer file saying "mode": "install" therefore dropped every
provisioning key on any second install, SCCM re-deploy or golden-image refresh.
The file decides on that path now; update mode accepts no provisioning keys, so
a Provision run for one performs no action and exits 0.

Database provisioning never received -CertificateHostName. The script declares
the parameter and the installer builds HostNameInCertificate from the same
answer-file key, but the provisioning call dropped it, so it derived the name
from -Server. The very normal 'localhost' against a server whose certificate
names the FQDN failed with "The target principal name is incorrect" and created
nothing.

A failed provisioning was invisible unattended. It exits 0 and reports itself
inside provision.ini, which the readiness page reads and the silent path did
not - so the run walked on to Apply and died in the SQL pre-flight telling the
operator to have a DBA create a login that was never the problem.

The uninstaller ignored serviceName, installPath and dataPath. It passed
GetServiceName('') - the literal 'NodePilot' in the uninstaller process - and
{app}, which is only where Inno put the uninstaller, and never passed -DataPath
at all. A non-default installation was "uninstalled" with exit 0 while the
service, its port, its firewall rule and every program file stayed exactly where
they were, and only the bookkeeping that said NodePilot existed was removed. It
now reads all three back from the installation marker.

waitAny/waitNofM marked whole runs Failed when the losing branches were
runScript. Standing a branch down is what the junction is for, and StepRunner
records those as Cancelled - but only if the exception reaches it. The
PowerShell engines converted the cancellation into an ordinary failed result,
and one Failed step fails the execution, so a correct run came back red with
every junction, log and returnData green. Branches built from delay were
unaffected because they let the exception through. All four sites now rethrow on
caller cancellation, including the two catch-alls that re-wrapped the throw as
"Isolated execution failed: Script execution cancelled". A timeout stays a
failure.

Tests: the junction contract end to end (loser Cancelled, run Succeeded), both
PowerShell engines' cancel-versus-timeout split, and static contracts for the
provisioning gate, the provisioning verdict, the certificate host name and the
three uninstall parameters.
@Sev7eNup
Sev7eNup merged commit 7784a80 into main Aug 15, 2026
9 checks passed
@Sev7eNup
Sev7eNup deleted the fix/setup-provisioning-uninstall-and-junction-cancel branch August 15, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant