Skip to content

Make the setup say what it is doing, extract faster, and stop crushing the prerequisites page - #183

Merged
Sev7eNup merged 4 commits into
mainfrom
fix/update-progress-and-av-staging
Aug 11, 2026
Merged

Make the setup say what it is doing, extract faster, and stop crushing the prerequisites page#183
Sev7eNup merged 4 commits into
mainfrom
fix/update-progress-and-av-staging

Conversation

@Sev7eNup

@Sev7eNup Sev7eNup commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Four changes to the server setup, all from one thread: an upgrade on a domain-joined client sat for minutes on "Updating NodePilot" with a motionless bar, and chasing that turned up three more things worth fixing.

1. The silent stretch now has a phase

The first update phase was the backup, so signature verification, extraction and the first hash pass ran with nothing to display. The updater now announces Extracting artifact before it expands and verifies — the same heading the installer already uses — with the matching table entry at 10 %, ahead of the backup at 20 %.

Where the time actually goes (measured on the 1.2.1 artifact): 2867 files, 114 MB, of which 2646 are under 64 KB and 1573 under 8 KB; knowledge/ alone contributes 2304 files. An update walks that tree six times — expand to staging, hash against the signed manifest, back up, wipe, copy in, hash again — roughly 17 000 file operations. One full hashing pass measures 2.7 s on a warm NVMe, which rules hashing out as the cause; what turns it into minutes is a real-time scanner inspecting every creation. Repo growth is ruled out too: 1.1.0 → 1.2.1 added only +64 files (+3 %).

2. The AV hand-off list was missing the place that dominates the runtime

docs/av-exclusions.md documented Program Files, ProgramData and the backup folders — but not %TEMP%\nodepilot-artifact-*, where installer and updater create those ~2900 files first. Recorded as recommended, with the residual risk stated: the directory already carries a restrictive DACL applied atomically at creation, and its contents are verified file by file against the signed manifest immediately afterwards.

3. Faster extraction — with the honest numbers

Expand-Archive[IO.Compression.ZipFile]::ExtractToDirectory. Faster everywhere measured, but the margin does not travel, so no single multiplier is quoted:

Expand-Archive ExtractToDirectory
Win 11 workstation (PS 5.1.22621) 24.3–24.8 s 2.0–2.7 s ~9–12×
Server 2025 VM, 4 cores (PS 5.1.26100) 7.3–7.7 s 4.6–5.2 s ~1.6×

The workstation is the faster machine on the new path and the slower one on the old, so the gap is in what Expand-Archive costs per entry there, not in raw I/O. The lab number is the one a server operator will get — an earlier commit message in this branch called it "~12×" and the follow-up commit corrects that.

Equivalence verified, not assumed: both produce the same 2867 files across the same 377 directories, zero differences in relative path, length or SHA-256. Zip-slip is not given up — .NET refuses an entry resolving outside the destination, confirmed against a crafted archive carrying ../escaped.txt, and Test-ArtifactSecurity now builds that archive and asserts the rejection.

4. Setup-log noise removed

Import-NodePilotPkcsTypes asks the edition instead of try/catch. Add-Type raises a terminating error under the setup's Stop preference and Start-Transcript records it before the catch swallows it — so every setup log carried a red "Die Assembly System.Security.Cryptography.Pkcs konnte nicht gefunden werden" directly above the line confirming the signature had verified.

5. Prerequisites page — ⚠️ not visually verified

With all ten checks reporting, the remediation box got ~30 px: one clipped line, in the field that has to show a CREATE LOGIN block when the database check fails. WizardSizePercent=125,145 — width puts most rows back on one line, height gives the explanation ~210 px. The floor moves 13 px → 34 px so it can never render as a broken sliver.

This one lands unverified visually. Contract suites pass and ISCC compiles, but on the Inno-owned Installing page the extra height leaves a large empty area, and the prerequisites page itself has not been seen with the new size. Height is two numbers on one line and easy to tune down (145 → 120) once it has been looked at.

Scrolling was the alternative and is disproportionate: Inno's Pascal Script exposes no TScrollBox, the rows are individual controls, and a real scroll container would mean rebuilding the list as TRichEditViewer — losing the per-row click targets and the auto-fix checkboxes.

Verification

Three deployment suites green on Windows PowerShell 5.1 (templates, setup adapter 211 assertions, artifact security), ASCII contract on the .iss holds.

End-to-end on the lab server (CM1, Server 2025, gMSA, SQL Server 2022): uninstall → full wipe incl. database drop → install published 1.2.1 → upgrade to this branch → uninstall with /PURGEDATA=1 → fresh install with unattended bootstrap admin. All exit 0, health live/ready/database 200 throughout, bootstrap admin login verified against the API. The new phase appears in the update log exactly where the silence was, and the log carries 0 Add-Type terminating errors.

…twice over

An upgrade sat for minutes on "Updating NodePilot" with a motionless bar,
then finished quickly. Nothing was wrong; the wizard simply had no
vocabulary for the stretch that costs the most.

Measured on the 1.2.1 artifact: 2867 files totalling 114 MB, of which 2646
are under 64 KB and 1573 under 8 KB - `knowledge/` alone contributes 2304
files for 25 MB. An update walks that tree six times over (expand to
staging, hash against the signed manifest, back up the old install, wipe
the install path, copy staging in, hash again), so roughly 17,000
individual file operations, nearly all on tiny files. One full hashing
pass measures 2.7 s on a warm NVMe, which rules hashing out as the cost:
what makes it minutes is a real-time scanner inspecting every one of those
creations.

Three changes, none of them to what the update actually does:

The updater now announces `Extracting artifact` before it expands and
verifies, matching the installer's existing heading, and the progress
table gains the matching entry at 10% - ahead of the backup at 20%. The
first phase used to be the backup, so everything before it ran silently.
The bidirectional drift guard already pins table and Write-Step calls to
each other, and Test-SetupAdapter now asserts specifically that extraction
is announced and ordered ahead of the backup, so a rename or a reorder
cannot quietly restore the silence.

The AV hand-off list gains `%TEMP%\nodepilot-artifact-*`. It documented
Program Files, ProgramData and the backup folders, but not the staging
directory where installer and updater create those ~2900 files first - so
the one place that dominates the runtime was the one place not covered.
Recorded as recommended rather than mandatory, with the residual risk
stated plainly: the directory already carries a restrictive DACL applied
atomically at creation, and its contents are verified file by file against
the signed manifest immediately afterwards.

`Import-NodePilotPkcsTypes` now asks the edition instead of attempting and
catching. Add-Type raises a TERMINATING error under the setup's Stop
preference and Start-Transcript records it before the catch swallows it,
so every setup log carried a red "assembly could not be found" directly
above the line confirming the signature had verified. Harmless, and
exactly the kind of thing that makes an operator abort a healthy install.
Measured on the real 1.2.1 artifact (2867 files, 114 MB, 2646 of them
under 64 KB), twice, warm:

    Expand-Archive       25.8 s / 24.7 s
    ExtractToDirectory    2.2 s /  2.1 s     ~12x

Expand-Archive pays per-entry pipeline overhead, which dominates a tree of
mostly tiny files. On a host whose real-time scanner inspects every
creation the absolute saving is proportionally larger, and this runs on
both the install and the update path.

Equivalence was verified rather than assumed: both extractors produce the
same 2867 files across the same 377 directories, with zero differences in
relative path, length or SHA-256.

Zip-slip is not given up in the trade. .NET refuses an entry whose
resolved path leaves the destination, confirmed against a crafted archive
carrying '../escaped.txt' - nothing was written outside, same as
Expand-Archive. Test-ArtifactSecurity now builds that archive and asserts
the rejection, because a later swap to a hand-rolled extraction loop is
exactly how this property would be lost quietly. Assert-NodePilotExtracted-
Files remains the second line regardless: rooted and dot-dot manifest paths
rejected, exact file count required, every file hashed against the signed
manifest.

Paths are resolved to absolute first - ExtractToDirectory resolves a
relative path against the process working directory, which is neither the
caller's location nor the staging parent. Import-NodePilotZipTypes loads
the assembly by edition for the same reason as the Pkcs helper: a
try/catch would write a red terminating error into every setup transcript.
…sured

The comment quoted 2.2 s against 25.8 s and the commit before it called
that ~12x. That number is real but it is a workstation number, and a
Server 2025 VM does not reproduce it:

    Win 11 workstation (PS 5.1.22621)   24.3-24.8 s -> 2.0-2.7 s   ~9-12x
    Server 2025 VM, 4 cores (26100)      7.3- 7.7 s -> 4.6-5.2 s   ~1.6x

Same artifact, three and two runs. Notably the workstation is the FASTER
machine on the new path and the slower one on the old, so the gap is in
what Expand-Archive costs per entry there rather than in raw I/O - which
is exactly why a single multiplier does not travel.

The change stays: faster on both, identical output, zip-slip still
rejected. But a server operator reading "12x" and measuring 1.6x would be
right to distrust the rest of the file, and the lab number is the one they
will get.
With all ten checks reporting, the page had nothing left for the field
that explains them. Each row is "Title: Detail", so at the default width
six of ten wrap to two lines; LayoutReadiness stacks ~238 px of rows into
a ~309 px surface and the remediation box gets what remains - about 30 px,
one clipped line. That box is where a failed database check has to show a
CREATE LOGIN / CREATE USER block, so one line is not a cosmetic problem.

Scrolling was the other option and is disproportionate here: Inno's Pascal
Script exposes no TScrollBox, the rows are individual controls on the page
surface, and a real scroll container would mean rebuilding the list as a
TRichEditViewer - losing the per-row click targets and the auto-fix
checkboxes. That is a different feature, not a layout fix.

So the window grows instead, in both directions and for different reasons.
Width does most of the work: +25% puts most rows back on one line, which
shortens the stack before height is even considered. Height then buys the
explanation real room: ~309 -> ~471 px of surface leaves ~210 px, about
twelve lines. At ~560 px including its frame the window still fits the
768 px console of a server VM.

An earlier note in [Setup] claimed "every page fits at 100%" and reverted a
previous attempt on that basis. That claim is what the ten-check page
disproves, so it is replaced with the measurement rather than deleted -
otherwise the next reader reverts this for the same stated reason.

WizardResizable stays off: those controls are positioned once, at
construction, and carry no anchors, so a window dragged open at runtime
would grow around a certificate picker that stays put. A fixed larger
START size is a different thing and is safe - everything that must grow is
already sized from SurfaceWidth.

The remediation floor moves 13 px -> 34 px in the same pass. At one line
the control reads as a broken edit field rather than an explanation. The
larger window means the floor is not reached in practice; it is there for
high-DPI scaling and for the day an eleventh check arrives. Rows still win
and the box still gives - the other precedence would draw the explanation
over the last checks, and a check nobody can see is worse than an
explanation that has to scroll.
@Sev7eNup
Sev7eNup force-pushed the fix/update-progress-and-av-staging branch from 56b9480 to 7f34808 Compare August 11, 2026 15:02
@Sev7eNup Sev7eNup changed the title Name the longest part of an update, and stop scanning it twice over Make the setup say what it is doing, extract faster, and stop crushing the prerequisites page Aug 11, 2026
@Sev7eNup
Sev7eNup merged commit 392cdee into main Aug 11, 2026
8 checks passed
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