Modernize the Windows MSI: WiX v5, x64, upgrade from 5.1.x - #701
Modernize the Windows MSI: WiX v5, x64, upgrade from 5.1.x#701vharseko wants to merge 60 commits into
Conversation
The opendj-msi package was built and uploaded but never installed or exercised in CI. Add a test-msi job (needs: build-maven) that, on a windows-latest runner, installs the built .msi silently (msiexec /i), runs setup, registers and starts/stops the OpenDJ Windows service with an ldapsearch liveness check, then uninstalls (msiexec /x).
…E 25) The MSI ships no JRE, so add a WiX launch condition that fails the install early with a clear message when Java is not detected (it does not install Java). JAVA_HOME is captured from the environment before LaunchConditions; `Installed` keeps uninstall/repair working regardless of Java. Fix the test-msi CI job (it failed with `net start` exit 2 "service already started"): - setup.bat is now invoked with --doNotStart, so the server is started only by `net start "OpenDJ Server"` (setup.bat no longer starts a standalone instance first). - Bump actions/setup-java from 21 to 25 (latest LTS; smoke-tests the MSI under a fresh JRE). The runner has JAVA_HOME from setup-java, so the new launch condition is satisfied and the install proceeds.
The launch condition (Installed OR JAVA_HOME_ENV) false-blocked valid installs: a JRE does not always set JAVA_HOME (it may be only on PATH). Drop it - the MSI again only copies files and Java availability stays the admin's responsibility. The test-msi fix (--doNotStart, JRE 25) is unaffected.
The install guide covered only the .zip and native .deb/.rpm. Add Windows MSI sections to chap-install/chap-upgrade/chap-uninstall: GUI and silent msiexec install, Java as a runtime prerequisite the installer does not enforce, configure via setup.bat, optional Windows service registration via windows-service.bat, MSI upgrade (disable service, back up, install newer .msi, upgrade.bat, re-enable), and uninstall via Apps & features / msiexec /x.
Migrate the Windows MSI off the EOL WiX 3.11.1 (heat/candle/light run under wine + winetricks dotnet40) to the cross-platform WiX v5 .NET tool, and address the MSI validation findings: - package.wxs rewritten to the WiX v4+ schema: single <Package>, x64 (ProgramFiles64Folder), MediaTemplate CompressionLevel="high", InstallerVersion=500, <Files> directory harvest (replaces heat), and a <ServiceInstall>/<ServiceControl> on opendj_service.exe so the MSI itself registers the "OpenDJ Server" Windows service (Start=auto, not started during install - setup must configure the instance first). UpgradeCode preserved. - opendj-msi-standard/pom.xml builds the MSI with `wix build -arch x64`; removed the external-dependency-plugin (wix3111 + winetricks), heat/candle/light and the wine/winetricks antrun steps. - opendj-msi/pom.xml: wine-path profiles replaced by an unconditional module so the MSI builds natively on every OS where opendj-msi is included. - build.yml: drop wine (keep rpm); add a "Setup WiX (.NET tool)" step; test-msi no longer calls windows-service.bat --enableService (the MSI registers it). - install guide: x64 install path; the MSI registers the service. Signing (OpenIdentityPlatform#4) deferred (no certificate). The Burn bundle that auto-installs a JRE is a follow-up, to be added after this MSI build is validated in CI.
WiX 5 ships as a net6.0 .NET tool, but the runners only have a newer .NET runtime (no .NET 6), so 'wix' failed to launch (exit 131, missing_runtime) on arm64 macOS. Set DOTNET_ROLL_FORWARD=Major in the Setup WiX step (exported and written to GITHUB_ENV) so the tool rolls forward to the installed runtime, both for that step and the Maven build that invokes 'wix build'.
wix build failed with WIX0004 ('Files element contains an unexpected attribute Exclude'): in WiX 5.0.2 exclusion is a child <Exclude Path=.../> element (with <Include Path=.../>), not an inline attribute. Convert both <Files> harvests to the child-element form.
macOS (arm64) runners have no .NET runtime for the net6.0 wix apphost (DOTNET_ROOT unset, exit 131); add actions/setup-dotnet (8.0.x) on macOS so the tool finds a runtime and rolls forward.
…only Include) WiX 5.0.2 requires Include as an attribute on <Files> and rejects an Exclude attribute as well as <Include>/<Exclude Path> child elements (WIX0004/0005/0010). Move all exclusions to the Ant staging step: stagingRoot = payload minus lib (and macOS/Unix bits), stagingLib = lib minus opendj_service.exe (registered via an explicit component sourced from the package dir). Each <Files> now uses only the Include attribute, and there is no duplicate lib directory.
On Linux/macOS the wix tool treats backslash as a literal, not a path separator, which may cascade into the WIX0389 'not a relative path' errors. Use '/**' (accepted on Windows too) to test whether the cross-platform wix build then succeeds.
The WiX Toolset can create MSI databases on Windows only: it warns "only supports Windows" and, on Linux/macOS, fails with WIX0389 on every Directory name and (with no name) on a missing msi.dll (the Windows Installer library). Verified locally on macOS with WiX 5.0.2, 6.0.1 and 7.0.0 - none can build. - opendj-packages/pom.xml: build opendj-msi only in the distribution-windows profile (removed from distribution-unix and distribution-mac). - build.yml: run the WiX setup step only on Windows; drop macOS setup-dotnet. - package.wxs: keep the Windows-proven backslash glob in <Files Include>. - .gitattributes: force LF for *.wxs/*.wxi.
The MSI can only be built on Windows. Instead of rebuilding opendj-server-legacy on a Windows runner: - deploy.yml: reuse the MSI already built by the triggering Build run (download the windows-latest-11 artifact, re-upload as "OpenDJ MSI Package"); both steps continue-on-error; drop wine (rpm only). - release.yml: release-maven uploads the released server zip as an artifact; new release-msi job (windows, continue-on-error so an MSI failure does not break the release) installs the zip into the local Maven repository and only packages :opendj-msi-standard (no -am), then attaches the MSI to the GitHub release; drop the .msi from the ubuntu release file list; drop wine.
…e legacy service - package.wxs: refuse to install when no Java is detectable (JAVA_HOME\bin\java.exe or java-looking PATH entries; MSI cannot scan PATH, so substring heuristic) with a message pointing at https://adoptium.net; detect an existing install (InstallDir registry value written by this package, else the legacy x86 default directory) and apply it only when OPENDJ is not set explicitly; drop the legacy windows-service.bat "OpenDJ Server" service before InstallServices (its display name collides with the MSI ServiceInstall). - build.yml: test-msi asserts the installer fails without a JRE before one is set up; new test-win-upgrade installs the released 5.1.1 x86 MSI, configures an instance with the legacy service, upgrades with the newly built x64 MSI without OPENDJ and asserts same directory, intact data, MSI-managed service, then runs upgrade.bat, starts the service and searches the pre-upgrade data. - docs: Java is required by the installer (adoptium.net link); MSI upgrade autodetects the directory and replaces the service; uninstall removes the service automatically.
- _script-util.bat: quote -Djava.io.tmpdir="%OPENDJ_TMP_DIR%" - any install directory containing spaces (including the x64 default C:\Program Files\OpenDJ and the legacy Program Files (x86)) broke the CheckJVMVersion probe and every script with "The detected Java version could not be used". - build.yml test-msi: the no-JRE negative test now hides Java at MACHINE scope (the Windows Installer service evaluates launch conditions with the machine environment) and restarts msiserver, restoring everything afterwards; the positive install now goes to the spaced x64 default directory end-to-end. - build.yml test-msi-upgrade (renamed from test-win-upgrade): the released 5.1.1 scripts cannot run from a spaced directory, so the old install lives in C:\opendj and the upgrade passes OPENDJ explicitly; added a fresh-install scenario asserting the legacy default directory is auto-detected when OPENDJ is not given, plus uninstall/service cleanup checks. - ADNotificationRequestControl: escape && in the javadoc code sample (javadoc "invalid input: '&'" warnings).
…aces) _script-util.bat appends -Djava.io.tmpdir=%OPENDJ_TMP_DIR% to OPENDJ_JAVA_ARGS without quotes, so in any install directory containing spaces (for example C:\Program Files (x86)\OpenDJ) the CheckJVMVersion probe - and with it setup and every other command-line tool - fails with "The detected Java version could not be used with the set of Java arguments". Quote the value: -Djava.io.tmpdir="%OPENDJ_TMP_DIR%".
…cp.bat) _script-util.bat passed unquoted paths to setcp.bat, and setcp.bat compared arguments with if ""%1""=="""". The argument-joining hack survived spaces, but a parenthesis in the path (C:\Program Files (x86)\OpenDJ - the default MSI directory) breaks the cmd parser with "... was unexpected at this time", so setup.bat and every tool exit with 255. Quote the setcp.bat arguments at the three call sites and switch setcp.bat to %~1 with quoted comparisons.
…e "%VAR%" == "" comparisons
After -Djava.io.tmpdir="%OPENDJ_TMP_DIR%" is appended, OPENDJ_JAVA_ARGS
contains embedded quotes, and the subsequent if "%OPENDJ_JAVA_ARGS%" == ""
checks blow up the cmd parser ('...\tmp"" was unexpected at this time', every
tool exits 255) regardless of whether the install path has spaces. Compare
with "if defined", which does not expand the value.
…h quoting
- The <Files> harvest ships files only (heat had -ke), so the empty instance
directories (bak, changelogDb, classes, db, import-tmp, ldif, locks, logs,
tmp and the template ones) were missing from the installed tree and the
server could not create its lock/pid files - setup aborted with "error
stopping server". Create them with explicit CreateFolder components.
- [OPENDJ] ends with a backslash which escaped the closing quote in the
service ImagePath ('start "C:\opendj\"' -> broken argv), so the MSI-managed
service failed to start (NET HELPMSG 2186). Append an extra backslash so
\\" parses as backslash + closing quote.
Components whose KeyPath is a directory cannot use auto-generated GUIDs, so the empty-instance-directory components broke wix build with WIX0230. Assign fixed GUIDs.
The tmp-cleanup block expands %OPENDJ_TMP_DIR% unquoted inside a ( ) compound statement, so a parenthesis in the install path - C:\Program Files (x86)\OpenDJ - terminates the block at parse time and start-ds fails; the Windows service then dies with a service-specific error -1 while setup (which never runs this block) succeeds. Quote the three path expansions.
opendj_service.exe located its own service entry by comparing the raw SCM ImagePath with the exact string it builds itself, so a service registered by the MSI (exe unquoted, instance dir with a trailing backslash) was never found and "net start" failed with error 2186. getServiceName now compares the executable path, the subcommand and the normalized instance dir token by token instead. The MSI ServiceInstall argument becomes 'start "[OPENDJ]."' and the sc-config workaround is not needed.
|
Thanks for the seventh pass — and for the four withdrawals, which saved me from re-litigating them. Everything here is accepted: both blockers were mine, and both landed yesterday in the same commit that was supposed to be closing this class of problem. Addressed in 1cf5a88; notes follow your headings. Worth recording alongside your review: the Build run on the head you reviewed never reached the MSI jobs — Refusal-message assertion never matchesTaken as suggested — the grep is now on Relocation scenario cannot exercise the guardConceded completely, and it is worse than a mistake of fact: Rewritten along your suggestion, with both branches of the guard covered: 5.1.2 goes into its native legacy default (its own ProductCode, so the new package is a genuine upgrade), then Legacy directory adopted during an upgradeConfirmed, including the criticism of the comment: Fixed as you suggested: Your note about CI is the part that stings: the Service started without waiting for the lockConfirmed. Extracted as you suggested rather than duplicated: the helper is now Launcher commit can abort the whole deployConfirmed, with one correction: Took the retry, and then some: three attempts with Nits
Still outstanding before merge, unchanged from last round plus one: the committed |
The job checks out the ref of the run that triggered it, and the workflow_run branches filter matches that run's head branch NAME - which a fork can also call master. What keeps the ref trusted is event == 'push': a Build run for a pull request carries event 'pull_request', and a push to a fork runs the fork's own workflows rather than this repository's. Check head_repository as well so the boundary is written down instead of re-derived, and so relaxing the event condition later cannot quietly open it. CodeQL flags the checkout (actions/untrusted-checkout, alert 1262) on the pattern rather than on the conditions, so it will keep flagging it; the alert is a false positive for this configuration.
maximthomas
left a comment
There was a problem hiding this comment.
Thanks for the eighth pass' worth of fixes — the C side is down to three hunks and the guards are close. Two findings from my last comment I am withdrawing after re-verification: the OPENDJ_REG ~<> OPENDJ trailing-separator worry (Microsoft guarantees a Directory-table property ends in a separator after CostFinalize, and the guard is scheduled after it, so the round trip is a fixed point — strict equality is the right trade) and the head_repository job condition (documented required field, compared against github.repository, so forks are fine). The service.c comment point drops to a nit: enumeration failure already read as "not enabled" to all three Java callers on master, so nothing regressed.
What is left is two silent-failure defects, both in the upgrade guards, and both compounding: in the custom-directory state below, OPENDJ_SVC_IMAGEPATH ~>< OPENDJ fails, so the service guard is disarmed as well.
The service stop guard accepts every non-Running state (blocker)
opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs:197-211 samples the SCM once, the instant net.exe returns — no delay, no retry:
<CustomAction Id="StopServiceBeforeUpgrade" ... ExeCommand="... net.exe" stop "OpenDJ Server"" Return="ignore"/>
<CustomAction Id="CheckServiceStopped" ... ExeCommand="... -Command "if ((Get-Service 'OpenDJ Server' -ErrorAction SilentlyContinue).Status -eq 'Running') { exit 1 }; exit 0"" Return="check"/>StopPending and StartPending are distinct enum values, so -eq 'Running' is $false and the CA exits 0. Two reachable routes:
- the service is already pending when the CA fires —
ControlServicefails immediately withERROR_SERVICE_CANNOT_ACCEPT_CTRL,Return="ignore"eats it, the sample passes.chap-upgrade.adoc:260tells administrators tonet stopfirst, so this is the documented flow. StartPending—service.c:648sets a 300000 ms start wait andservice.c:667-672reports pending with hintwait + 30000, sojava.execan holdlib\*.jarfor ~5.5 minutes while the guard waves the upgrade through.
Consequence is exactly what the comment at package.wxs:170-177 describes, except worse: the jars are unversioned (opendj-archive-component.xml:26), so old and new share identical paths and PendingFileRenameOperations deletes the freshly installed jars at the next reboot. Install exits 0; the server breaks later.
Correction to my previous comment: my "net stop gives up at ~10 s" mechanism was wrong. service.c:854-876 increments dwCheckPoint every ~2 s under the 10 s hint, which is the documented "still making progress" signal, and the wrapper always resolves to STOPPED or RUNNING within ~61 s (service.c:1491, 1520). The defect survives because the check is an instantaneous sample, not because the state is durable.
The repo already owns the right probe — .github/scripts/wait-server-stopped.ps1:26-38 takes a byte-range lock on locks\server.lock, and its own comment cites #768 as "winlauncher.exe reported success without having stopped the server" — but the MSI never uses it. service.c:2256-2271 budgets 90 s for the same wait; the MSI budgets zero. Minimum fix, null-safe because a poll makes the missing-service case reachable:
$deadline = (Get-Date).AddSeconds(90)
do {
$s = Get-Service 'OpenDJ Server' -ErrorAction SilentlyContinue
if (-not $s -or $s.Status -eq 'Stopped') { exit 0 }
Start-Sleep -Seconds 2
} while ((Get-Date) -lt $deadline)
exit 1A custom-directory upgrade still destroys an unrelated installation (blocker)
OPENDJ_LEGACY_INSTALL (package.wxs:75-79) proves a setup.bat exists in a default directory — not that it belongs to the product being upgraded. Start state: 5.1.x at C:\opendj-custom, plus any foreign OpenDJ tree (zip, manual copy) in a Program Files default. msiexec /i new.msi /quiet, no OPENDJ:
SetOpendjFromLegacyDir(package.wxs:113-115) fires — the foreign tree satisfies the new gate — and adoptsC:\Program Files (x86)\OpenDJ\.RequireDirOnCustomUpgrade(:204-205):OPENDJ ~= OPENDJ_DEFAULTis false. Does not fire.RefuseRelocatingUpgrade(:206-207):OPENDJ_REGis empty andOPENDJ_LEGACY_DEFAULT ~<> OPENDJis false — they are identical. Does not fire.
The x64 mirror slips the other guard: with a foreign tree in C:\Program Files\OpenDJ, NOT OPENDJ_X64DEFAULT at :205 is false, and the legacy branch never arms because OPENDJ_LEGACY_INSTALL is unset.
Both end the same way, with exit code 0: RemoveExistingProducts runs 5.1.x's own uninstall and guts C:\opendj-custom, while InstallFiles overwrites the foreign tree — whose old jars are not removed, and _script-util.bat:62 globs lib\*.jar, so that tree ends up with a duplicate-version classpath. HKLM\SOFTWARE\OpenDJ\InstallDir and ARP then point at the foreign tree, so a later msiexec /x deletes out of it.
Every premise checks out: git show 5.1.2:...package.wxs carries the same UpgradeCode="A3E82AC0-88E6-4DEE-9D8C-5AE3B7853274", 5.1.2 accepts a custom OPENDJ (your own build.yml:939 does it), and it writes no registry value. The refusal scenario at build.yml:926-949 passes only because it deletes both default directories first (:936-937) — the guard's correctness is conditional on the decoy being absent.
Two service side-effects fall out of the same hole: if the foreign instance owns a running OpenDJ Server, StopServiceBeforeUpgrade stops an unrelated production server; if the real 5.1.x owns it, OPENDJ_SVC_IMAGEPATH ~>< OPENDJ fails and CheckServiceStopped never runs at all.
The x64 half looks closable with one term — NOT OPENDJ_REG in place of NOT OPENDJ_X64DEFAULT at :205 keeps the legitimate 5.2.0→5.2.1 default-directory upgrade passing while making this case refuse (checked against every scenario in build.yml:785-1042, but please verify). The legacy branch is harder: a setup.bat existence test cannot distinguish a server from the server, so it is either "refuse when WIX_UPGRADE_DETECTED AND NOT OPENDJ_REG and no explicit OPENDJ was given", or state the residual. What it cannot stay is chap-upgrade.adoc:264, which promises unconditionally that the package "refuses to install into a directory other than the detected one".
The guard rework has never run green in CI (major)
test-msi and test-msi-upgrade last completed at 252d5ba9b (2026-08-10). Every Build run on the five commits since — 24b1b7ec4, b2de06075, 92b30bd20, 1cf5a88546, 10c99b94cf — was cancelled by the next push. RefuseRelocatingUpgrade did not exist at 252d5ba9b, so it has never executed in a completed run in any form; the same goes for the new refusal, relocation and empty-legacy scenarios and for wait-server-stopped.ps1. The head's run is ~60 min in with the MSI jobs still gated behind the ubuntu matrix — the same gate that swallowed them last round.
Worth pinning the MSI jobs to something that cannot be starved by the ubuntu matrix (or diagnosing the six-hour ubuntu-latest, 25 job) before this merges. Right now green CI is not available as evidence for any of the guard work.
An unguarded git fetch can abort the whole deploy (minor)
.github/workflows/deploy.yml:131 is a bare command in a for body. The step runs under bash --noprofile --norc -eo pipefail plus an explicit set -e (:101) and has no continue-on-error, and the retry loop wraps only the rebase (:132) and the push (:137) — so a transient fetch failure never reaches iteration 2 and kills the step. It is step 5 of ~19: mvn deploy, all eight artifact uploads, the MSI attach, the wiki push and the doc-site push are skipped. That is the exact loss 1cf5a88546's commit message says the rework prevents.
if ! git fetch --quiet origin "$BRANCH"; then
echo "::warning::could not fetch $BRANCH; skipping the launcher refresh"
exit 0
fiRare in practice — /Brepro means :109 only lets the block run when the launcher bytes actually changed. My earlier claim about the cp at :106 was wrong; the ls check at :102-105 guards it.
Nits
wait-server-stopped.ps1:41catches everything: the blanketcatchtreatsUnauthorizedAccessExceptionas "still locked", so a permissions problem under Program Files spins the full 60 s and then reports a lock that was never held. CatchIOExceptionand let the rest surface.build.yml:688full checkout for one file:actions/checkout@v6clones the whole tree on a Windows runner to read a 38-line script, then the artifact is unpacked over it.sparse-checkout: .github/scriptsremoves both costs.build.yml:976asserts nothing about the guard: the empty-legacy scenario checks only$p.ExitCode -ne 0, so it passes on any failure. Both neighbours grep the message; this one should greplocation could not be determinedtoo.build.yml:999inherits the previous step's teardown: the relocation scenario stopped clearingC:\Program Files (x86)\OpenDJitself, so it depends on:983— the coupling you removed from the refusal scenario one round ago.deploy.yml:138claims a push that may not have happened: an already-landed refresh makes the rebase drop the commit and the push a no-op, and the log still says "Refreshed launchers pushed to master." CompareHEADagainstFETCH_HEADfirst.service.c:2444comment: pre-existing, not introduced here — master's enumeration failure returned1(DISABLED) and the same three callers read that as not-enabled, so2changes nothing for them. Worth one clause noting the uninstaller still skips--disableServicein that state.
…pgrade guards CheckServiceStopped sampled the SCM once and accepted every state that was not Running, so a stop that had not taken effect - StopPending after an administrator's own 'net stop', StartPending for the five and a half minutes service.c allows a start - waved the upgrade through under a running JVM. The jars are unversioned, so the delete-on-reboot entries the nested uninstall leaves behind name the paths the new jars occupy. The action now polls: gone or Stopped passes, StopPending is waited out for 90 s, anything else refuses immediately. RequireDirOnCustomUpgrade stood down whenever a setup.bat existed in the x64 default directory, which any unrelated OpenDJ tree provides. A 5.1.x in a custom directory recorded nothing, so RemoveExistingProducts gutted it while InstallFiles landed on the stranger, at exit code 0. The guard now keys on "nothing recorded a location", with an explicitly named directory as the way through for an old server that really does live in the default one: OpendjDirGiven is read before AppSearch and sequenced "first", so the wizard keeps its refusal. The same decoy in the legacy default directory stays undetectable - the signal that would separate it does not exist for a registry-less 5.1.x - and is documented in the install guide instead. test-msi and test-msi-upgrade no longer wait for the whole build matrix. They need one artifact, published by the Windows leg after about fifteen minutes; waiting for the two-hour ubuntu legs is what let five consecutive pushes cancel them before they ran. Also: guard the launcher-refresh fetch in deploy.yml so a transient failure cannot skip the deploy, stop reporting a push that was a no-op, catch only IOException while waiting for the server lock, assert the guard message in the empty-legacy scenario, make the relocation scenario clear its own starting state, and sparse-checkout the one script test-msi needs.
|
Thanks for the eighth pass, and for re-verifying the two withdrawals rather than leaving them standing — the The service stop guard accepts every non-
|
RM maps the JVM holding lib\*.jar to the "OpenDJ Server" service at InstallValidate, stops it before StopServiceBeforeUpgrade runs, and restarts it at the end of the install: the upgraded server comes up against instance data upgrade.bat has not migrated, and the post-upgrade state the package promises - registered but Stopped - is broken (test-msi-upgrade caught it as StartPending). RM also disarms the starting-service refusal by stopping a StartPending service before CheckServiceStopped samples it. /norestart only suppresses reboots, so disable RM for this package: the service holder is handled deterministically by the custom actions, and any other holder falls back to the WiX3-era in-use behaviour.
The "old server in the default directory" scenario arranged itself with msiexec /i opendj-5.1.2.msi OPENDJ="C:\Program Files\OpenDJ", which cannot work: Windows Installer resolves a 32-bit package's [ProgramFilesFolder] to Program Files (x86) whatever the directory property says, so msiexec exited 0 while the files landed in the legacy default and the step failed on its own precondition - silently, because the verbose log is dumped only when msiexec itself reports a failure. Install 5.1.2 into a custom directory and move the tree instead. That leaves exactly what the scenario is about: an upgradable 5.1.x registration, no recorded location, no legacy directory, and a real old server sitting in C:\Program Files\OpenDJ. The registration left pointing at the emptied directory costs nothing - no guard reads it, and RemoveExistingProducts tolerates the files being gone. The assertions had to move with it. 5.1.x ships lib\opendj_service.exe itself, so its presence no longer separates the new payload from the old tree: both halves are now judged on what only the new package produces - the wrapper's content and the InstallDir registry value - and the named upgrade additionally proves that the one OPENDJ value in this workflow carrying spaces reaches the elevated server context intact. A precondition msiexec accepts and then ignores now prints the log and both Program Files directories.
maximthomas
left a comment
There was a problem hiding this comment.
Reviewed ff54e5f on top of the 8th round — the two new commits (20a77b6 Restart Manager, ff54e5f 32-bit MSI / default-directory tests) plus a full pass over the rest of the diff.
The guard sequencing, the RemoveExistingProducts placement, the Formatted-field brace handling, the service.c exit-code mapping and the Maven/deploy.yml wiring all check out. Four things below, one of which blocks the documented upgrade path.
Relocation guard refuses the workaround its own comment prescribes (Blocker)
package.wxs:174-181 documents the residual and its remedy:
The install guide carries the workaround instead: pass OPENDJ explicitly when another OpenDJ tree sits in the legacy default directory.
package.wxs:269 then refuses exactly that:
Condition="WIX_UPGRADE_DETECTED AND ((OPENDJ_REG AND (OPENDJ_REG ~<> OPENDJ))
OR (NOT OPENDJ_REG AND OPENDJ_LEGACY_INSTALL AND (OPENDJ_LEGACY_DEFAULT ~<> OPENDJ)))"
OPENDJ_LEGACY_INSTALL only proves some server sits in C:\Program Files (x86)\OpenDJ — not that it is the product being upgraded. So with a 5.1.x server at C:\opendj and any unrelated tree (zip install, old instance) in the legacy default:
msiexec /i opendj.msi OPENDJ="C:\opendj"
OPENDJ_REG unset, OPENDJ_LEGACY_INSTALL set by the stray tree, C:\Program Files (x86)\OpenDJ\ ~<> C:\opendj\ → refused, "cannot move an existing installation". The message names neither the stray tree nor a way through.
Guard 1 (:267) already has the escape hatch — AND NOT OpendjDirGiven. Guard 2 has none. Either give branch (b) the same bypass, or key it on the resolved directory instead of OPENDJ_LEGACY_DEFAULT when OPENDJ was named.
The CI scenario meant to cover this ("a stray OpenDJ tree in the default directory must not be adopted") puts its decoy in the x64 default and deletes the legacy directory first, so branch (b) is never exercised with a decoy.
Non-service servers lose all in-use detection (Major)
package.wxs:258 turns Restart Manager off:
<Property Id="MSIRESTARTMANAGERCONTROL" Value="Disable"/>The reasoning for the service case is right. But the stop-and-verify pair replacing it is gated on a registered service:
Condition="WIX_UPGRADE_DETECTED AND (OPENDJ_SVC_IMAGEPATH ~>< OPENDJ)"
…and this PR promotes the non-service mode to the primary tested path — test-msi does setup --doNotStart then start-ds.bat, no service. For that server OPENDJ_SVC_IMAGEPATH is unset, so no guard runs, and with RM off nothing detects the JVM holding lib\*.jar (headless, no top-level window, so legacy FilesInUse misses it too). The upgrade proceeds silently into reboot-deferred file replacement.
Worth refusing on locks\server.lock being held, or at minimum documenting it in chap-upgrade.adoc next to the service instructions.
45-minute artifact wait is shorter than the queue it waits on (Major)
.github/workflows/build.yml:705 and :836:
$deadline = (Get-Date).AddMinutes(45)
...
throw 'windows-latest-11 was not published within 45 minutes'45 min covers the Windows leg's runtime, not its queue time. In run 31578978374 that leg started at +39 min and finished at +52 min — both MSI jobs would have failed the run with nothing actually wrong.
It also feeds itself: each waiter holds a windows-latest runner from t=0 for up to 45 min, competing for the capacity the leg they wait on needs. Either raise the budget substantially, or key the wait on the leg's status (queued/in_progress) rather than a fixed timer.
Custom-upgrade refusal advertises a GUI route that cannot work (Minor)
package.wxs:199 says "Run the installer again and select the existing installation directory". That relies on OpendjDirGiven, set at package.wxs:118:
<SetProperty Id="OpendjDirGiven" Before="AppSearch" Sequence="first"
Value="1" Condition="OPENDJ"/>Before="AppSearch" means OPENDJ can only hold a command-line value there; InstallDirDlg runs far later and never sets it. So for a server in C:\Program Files\OpenDJ whose registry value is gone (key deleted, machine re-imaged), re-running the wizard and selecting that same directory hits the identical refusal — OPENDJ still equals OPENDJ_DEFAULT, OpendjDirGiven still unset. Only the command line works.
Same promise in the comment at :111-113 ("the browse dialog as its way out"). Either set OpendjDirGiven from InstallDirDlg's Next, or say the directory must be passed on the command line in this case.
Nits
openScmnull check (pre-existing):service.ctestsscm == NULLinstead of*scm == NULL, so it never reports failure. Untouched by this PR, but the newSERVICE_LIST_UNAVAILABLEbranch is reachable through it.- Duplicated wait block:
build.yml:700-722and:831-848are the same ~20 lines of pwsh. A composite action would keep the two deadlines from drifting apart.
…gate RefuseRelocatingUpgrade's legacy branch refused the workaround its own comment and the install guide prescribe: with a stray OpenDJ tree in the legacy default directory, naming the real one - the documented way past a decoy the searches cannot tell from the product being upgraded - read as a relocation, so that installation had no upgrade path at all, silent or named. The branch now makes an exception for a named directory that holds a server (OpendjDirGiven AND OpendjGivenInstall, a new setup.bat search on the command-line value of OPENDJ). Naming an empty directory is still a relocation and still refused, and the registry branch is untouched. Restart Manager was disabled outright, which disabled its detection with it, leaving a server started by start-ds.bat - the mode this package ships, since it registers no service - with nothing watching the jars it holds: the legacy in-use scan only finds windowed applications. DisableShutdown keeps the detection and leaves the shutting down to us, and a new CheckServerNotRunning refuses the upgrade while the byte-range lock on locks\server.lock is held, in /quiet too, where no FilesInUse dialog can appear. The MSI jobs waited 45 minutes for an artifact whose leg is queued as well as run - in run 31578978374 it appeared at +52 minutes, so both would have failed a healthy build - and each held a windows-latest runner from t=0 doing it. The wait becomes one ubuntu gate job they depend on, which retires the duplicated block as well. RequireDirOnCustomUpgrade promised a wizard route that cannot exist: OpendjDirGiven is read before AppSearch and InstallDirDlg never sets it, so an old server in the x64 default can only be named on the command line. The message, the comments and the install guide say so now. openScm tested scm instead of *scm, so a failed OpenSCManager was never reported (pre-existing; the error still surfaced from the NULL handle, without the message). CI covers both new behaviours: a decoy in the legacy default with the real 5.1.x elsewhere, and an upgrade over a running non-service server.
|
Thanks for the ninth pass. All four are confirmed and fixed in 99a0116, and the blocker is worse than you wrote it: I have not taken either of your suggested fixes for it as written, nor the first one for the GUI route — notes follow your headings, with the reasoning in each case. Relocation guard refuses the workaround its own comment prescribesConfirmed, and the contradiction is not confined to the comment: Your CI observation is right too: the decoy sits in the x64 default and the legacy directory is deleted at Not the first alternative, though: giving branch (b) So the second one, keyed on the named directory rather than on the fact that one was named: a new search asks whether Two details worth stating. The search reads Residual, stated in the comment: naming a directory that holds some OpenDJ tree while the product being upgraded lives elsewhere now proceeds and strands that installation. Directory evidence cannot tell those apart at all, and between refusing the documented workaround and trusting an explicit instruction I take the instruction. You were also right that the message named neither the stray tree nor a way through — it now describes both. New scenario covers the branch with a decoy where it belongs: 5.1.x at Non-service servers lose all in-use detectionConfirmed, and the comment at I did not simply put RM back. It has no clean way to stop a headless JVM — no window to close, not RM-aware — so it terminates it, and an unclean stop of a directory server is worse than the refusal. The refusal is Documented next to the service instructions in Scenario added: 5.1.2 in the legacy default, 45-minute artifact wait is shorter than the queue it waits onConfirmed, and I checked the run you cited: it started 08:34:59, Rather than raise the budget or key on the leg's status inside a Windows job, the wait moved out: a small
Custom-upgrade refusal advertises a GUI route that cannot workConfirmed. The same promise is in Not the first fix: publishing So option two: the message, the comment at Nits
Verification, so it is not overstated: XML and YAML parse, and the strings the CI assertions grep for still match on both sides. Nothing is built — the MSI needs Windows — so |
…ening it WIX0012: a property AppSearch fills cannot be private, so OpendjGivenInstall does not compile - which was the whole of the previous commit's answer to "a public property can be handed in on the command line and switch the guard off". OPENDJ_GIVEN_INSTALL is public, and the guard stops testing it for truth: the search sets it to the full path of the setup.bat it found, so requiring that path to contain the resolved OPENDJ is both the "a server is there" test and the reason OPENDJ_GIVEN_INSTALL=1 says nothing. A value spelled out as a path inside the named directory still passes - deliberate construction rather than a slip, and the comment says so.
|
Correction to the first heading above: the property is public, not private, and the paragraph arguing that privacy is what closes the command-line bypass was wrong twice over — Fixed in 777b773 by making the test value-based instead of truth-based. The search stores the full path of the One thing that run did establish, on the third heading: the gate job caught the Windows leg finishing without the artifact at +8 minutes and skipped both MSI jobs, instead of a 45-minute timer expiring on two held Windows runners. The rest of that heading — and all of the first, second and fourth — is still waiting on the run now in flight. |
Measured, not reasoned. DisableShutdown was chosen to keep RM's detection - "The installer still uses the Restart Manager to detect files in use by applications" - on the reading that only the shutting down and the restarting were disabled. Run 31615584455 says otherwise: the headline upgrade scenario finished with the service StartPending after an otherwise successful install, so the installer restarts what the RM session listed at InstallValidate whether RM stopped it or CheckServiceStopped did. That is the failure the guards exist to prevent - the upgraded server coming up before upgrade.bat has migrated the instance data. So Disable, and the detection goes with it. CheckServerNotRunning is what replaces it and why the trade is affordable: the byte-range lock on locks\server.lock answers for both the service and the start-ds.bat modes, and answers in /quiet, where a FilesInUse dialog could not be shown. What stays uncovered is some other process holding payload files - the WiX3-era behaviour, now stated as the residual it is.
|
Second correction, to the "Non-service servers lose all in-use detection" heading: The reading was that it keeps RM's detection — "The installer still uses the Restart Manager to detect files in use by applications" — while disabling both the shutting down and the restarting. Only the first half is true. In run 31615584455 the headline upgrade scenario finished with the service StartPending after an otherwise successful install: the guards stopped it, and the installer restarted it at the end anyway, because what the RM session listed at Which means your finding is closed by Where the evidence stands after two runs on this branch. Green: the MSI builds, |
Review: requesting changes — one blocker, and the new CI scenarios can't catch itReviewed at 1. Blocker —
|
| wizard action | result |
|---|---|
browse to the correct D:\opendj |
OpendjDirGiven unset → branch (b) TRUE → refused |
| accept the pre-populated default | OPENDJ_LEGACY_DEFAULT ~<> OPENDJ false → no guard fires, installs into the decoy while RemoveExistingProducts empties D:\opendj |
Fix — publish the "directory was named" signal so it survives into the execute sequence
(public Secure="yes", or re-derive it there from OPENDJ vs OPENDJ_LEGACY/OPENDJ_DEFAULT
plus OPENDJ_GIVEN_INSTALL ~>< OPENDJ). Worth noting all 75 msiexec calls in build.yml use
/qn, so no wizard behaviour is regression-tested at all.
Checked and clean
service.c—SERVICE_LIST_UNAVAILABLEappended last, no renumbering, handled at all six
getServiceNamecall sites.ControlPanelInfo.java:420,Uninstaller.java:1352and
DirectoryServer.java:5284test only== SERVICE_STATE_ENABLED, soSERVICE_STATE_ERROR
reads as "not enabled" just asSERVICE_STATE_DISABLEDdid. Only nit: thegetServiceNamedoc
comment atservice.c:977-978still says "SERVICE_RETURN_ERROR otherwise".openScm—if (*scm == NULL)is a real fix; the old test compared the address of the
caller's variable, which is never NULL.- Guard balance and precedence,
~=/~<>/~><semantics, trailing-backslash construction on
both sides of every comparison, the Formatted-field brace rules (no{}group in the command
holds a[property]), andFindRelatedProducts→AppSearchordering with the execute-sequence
re-run — all correct.
Suggested order
- Fix the CI assertions first, before touching a guard, and re-run against this head — the
suite should go red on №1. If it doesn't, the assertions still aren't real. - Then the typed catch, and watch the now-honest suite go green.
- Consider moving the PowerShell out of the XML attribute so it can be unit-tested — a
Binary
table entry, or a.ps1as source of truth inlined at build time. I reproduced №1 in a
container in about five minutes; that loop belongs in CI. It's also the direct cause here —
wait-server-stopped.ps1has the typed catch, and the inline copy lost it on the way into the
attribute. - One full-UI scenario for №3.
- Possibly split:
service.candopenScmare independent and verified clean, and could go in
now while the MSI guard work continues separately.
maximthomas
left a comment
There was a problem hiding this comment.
see the comment above #701 (comment)
CheckServerNotRunning could not refuse anything: PowerShell wraps an exception thrown out of a .NET method or constructor in a MethodInvocationException, so the catch-all's $_.Exception.GetType().FullName -ne 'System.IO.IOException' test held for every exception, including the byte-range lock the action exists to detect - the exit 1 was unreachable. Use the typed catch wait-server-stopped.ps1 has always had, with the Formatted-field escapes ([\[] and [\]]) an ExeCommand needs so the type literal is not substituted away. The scenario covering it passed regardless: it asserted a non-zero exit code, which the fail-open outcome satisfies (3010, locked jars deferred to a reboot), and grepped for an action name the log carries whether the action refused or not. Assert the exact 1603, the action's "Return value 3", and that nothing was scheduled for delete-on-reboot; likewise for the StartPending scenario. OpendjDirGiven was private, and a private property set in the UI sequence never reaches the execute sequence, which is processed in the installer service rather than the client. Every full-UI session therefore read it as empty at the guards - including one started with OPENDJ= on the command line, the single route both the refusal message and the install guide prescribe - so the wizard refused the documented workaround and the relocation guard's exception could never apply. No scenario could see it: all 42 msiexec calls in build.yml are /qn. Replace it with a public, secure OPENDJ_GIVEN holding the named path, which the guards require the resolved OPENDJ to start with, so it is not a switch. Add a test-msi step that reads the built MSI: guard conditions may name only public properties, OPENDJ and OPENDJ_GIVEN must be secure, and CheckServerNotRunning's command, resolved the way msiexec resolves it, must exit 0 with no lock file, 0 with an unlocked one, and 1 while another process holds the lock. getServiceName's doc comment now names SERVICE_LIST_UNAVAILABLE, and the upgrade chapter notes that OPENDJ can be passed with or without /quiet.
|
All three confirmed, all three fixed in 59b7570. №1 and №2 turned out to be provable from the run that was already sitting on №1 — the guard could not refuse. Confirmed twiceReproduced the mechanism the way you describe it (containers, pwsh 7.4.6 and 6.2.4, identical): with a catch-all, Then the same command end to end, extracted from the wxs and MSI-formatted, against a real cross-process byte-range lock:
And the proof it already happened in production CI — run 3010 is One trap in the fix as prescribed. Semantics are preserved, near enough to be worth stating: №2 — the assertions. Confirmed by the same logThe job was green while the guard did nothing, exactly as you say. Both scenarios now assert the exact On your suggested order — assertions first, then the guard — the red run already exists in the form of that №3 — confirmed, and the mechanism is worse than the scheduling bitYou are right about
So the comment I had written — "the guards are immediate actions running client-side, where the property is visible" — was wrong in its premise: immediate means impersonated, not client-side. Two independent mechanisms each empty the flag in a full-UI session, and Fixed by replacing the private Your wizard table, row by row, on this head: row 2 is unchanged — a decoy in the legacy default with nothing named is the residual documented at №4 — the loop you asked for, in CINew
It costs ~70 s and would have failed on the previous head. Moving the script out of the XML attribute entirely (your Nits
On splitting Verified locally before pushing: WiX 5.0.2 compiles the authoring (on Linux it gets as far as the Windows-only |
The step added with the guard fixes could not read the built MSI: the WindowsInstaller.Installer COM view returned no rows in pwsh 7 without raising anything, so the first assertion failed as "RequireDirOnCustomUpgrade is not scheduled in InstallExecuteSequence" while test-msi-upgrade proved on the same head that it is scheduled and fires. Take the two guard conditions, the Secure declarations and CheckServerNotRunning's ExeCommand from package.wxs instead - wix copies all three into the tables verbatim, and it is test-msi-upgrade that judges the artifact, now on the refusal's own 1603 and "Return value 3". The lock probe itself is unchanged.
|
Run The guard refuses now
So on the same scenario that printed Correction: the new step reads the authoring, not the MSI tablesI said it was "extracted from the built MSI". It was, and it did not work: the What that costs is a wix-level transformation between authoring and package that nobody has ever seen happen for these three fields; what still judges the artifact is the scenario suite above, and it now judges it on the refusal's own If you would rather have the assertions read the package itself, that is a follow-up with a properly debugged reader — P/Invoke on |
maximthomas
left a comment
There was a problem hiding this comment.
Reviewed at 6bcada5. The three blockers from the last round are genuinely fixed, and I confirmed the typed catch, the ~<</~<>/~>< operand order, OPENDJ_GIVEN public+Secure, the service.c SCM fixes, and the release.yml/deploy.yml publish paths. Three things should be settled before merge — one of them outside the MSI entirely.
Release drops opendj-msi from the reactor, freezing the MSI POM versions (blocker)
opendj-packages/pom.xml removes <module>opendj-msi</module> from distribution-unix and distribution-mac, and the replacement profile activates only on Windows with WiX present:
<activation>
<os><family>windows</family></os>
<file><exists>${env.USERPROFILE}/.dotnet/tools/wix.exe</exists></file>
</activation>.github/workflows/release.yml runs release:prepare release:perform on ubuntu-latest with no -P, and maven-release-plugin only rewrites ${reactorProjects}. So opendj-packages/opendj-msi/pom.xml and .../opendj-msi-standard/pom.xml — both hard-coding parent 5.2.0-SNAPSHOT — will never be version-bumped again. Every past release (5.1.0/5.1.1/5.1.2) did rewrite them.
Maven does not fail on parent version skew; it silently falls back to the repository copy with no warning, and opendj-packages:5.2.0-SNAPSHOT + opendj-server-legacy:5.2.0-SNAPSHOT are both currently published. After the next release the MSI module pins itself to 5.2.0-SNAPSHOT, unpacks the pre-release server zip, and emits opendj-5.2.0-SNAPSHOT.msi — which release.yml attaches to the 5.2.0 release via target/*.msi. continue-on-error: true hides it.
Suggested fix: keep opendj-msi in the Linux module list and move the WiX/OS activation down onto opendj-msi-standard, so the release plugin still sees both POMs.
A relocating upgrade is unguarded when nothing recorded the old location (major)
opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs: RequireDirOnCustomUpgrade triggers on OPENDJ ~= OPENDJ_DEFAULT, and RefuseRelocatingUpgrade's second branch needs OPENDJ_LEGACY_INSTALL. With 5.1.x in a custom directory, no registry entry and no legacy directory (the topology already in .github/workflows/build.yml), a mistyped target slips past both:
msiexec /i opendj.msi OPENDJ=C:\opendj-custmo /qn
Guard 1: resolved dir ≠ x64 default → silent. Guard 2: no OPENDJ_REG, no OPENDJ_LEGACY_INSTALL → silent. Exit 0, RemoveExistingProducts strips the program files from C:\opendj-custom, the new tree lands at the typo path, and config/db/logs are stranded with no server and no upgrade.bat. The same typo is refused when an unrelated tree happens to sit in the legacy default, so today the protection depends on an incidental leftover directory.
Related: OPENDJ_GIVEN is a prefix test, so OPENDJ_GIVEN=C disarms guard 1 outright — the comment's "a value that does pass has spelled out the directory the install resolved to" holds for 1 but not for C, C: or C:\. Low on its own (it reaches nothing the documented OPENDJ= route doesn't), but it falls out of the same fix.
Giving guard 1 the evidence test guard 2 already uses closes both, with guard 2 and the SetProperty untouched:
<Custom Action="RequireDirOnCustomUpgrade" After="CostFinalize"
Condition="WIX_UPGRADE_DETECTED AND NOT OPENDJ_REG AND NOT OPENDJ_LEGACY_INSTALL AND NOT ((OPENDJ_GIVEN_INSTALL ~>< OPENDJ) AND ((OPENDJ ~<> OPENDJ_DEFAULT) OR (OPENDJ_GIVEN AND (OPENDJ ~<< OPENDJ_GIVEN))))"/>Read as: during an upgrade where nothing recorded a location and no server sits in the legacy default, refuse unless the target holds a server and was actively chosen. NOT OPENDJ_LEGACY_INSTALL is load-bearing — without it the headline upgrade scenario and the two Return value 3 scenarios refuse. I walked this against all 43 msiexec calls in build.yml; none change outcome. It also closes the wizard form (browse to an empty directory over a registry-less custom install).
opendj_service.exe does not match service.c (major)
The committed binary was last refreshed in 252d5ba9b8; opendj-server-legacy/src/build-tools/windows/service.c has changed in six commits since. It still contains a function the PR itself reverted away:
$ git show HEAD:opendj-server-legacy/lib/opendj_service.exe | strings | grep isMsiManagedService
isMsiManagedService: no HKLM\SOFTWARE\OpenDJ key, treating '%s' as orphaned.
Refusing to remove the MSI-managed service '%s'.
$ git grep isMsiManagedService -- opendj-server-legacy/src
(no match)
and lacks the new string from serviceState() ("no service list"), while its sibling "Service '%s' is enabled." is present.
The functional delta is small — the openScm NULL check and the SERVICE_LIST_UNAVAILABLE → 2 mapping, both diagnostic-only; removeService() returning 3 already ships. And deploy.yml's new commit-back supersedes the bytes on the first post-merge push. But that step has never run, all its failure paths are continue-on-error, and a Release dispatch before the first Package/Deploy would ship the stale binary to Maven Central (it copies lib/*.exe verbatim into the zip, deb, rpm and Docker image). One more refresh commit, as the branch has already done seven times.
Worth noting the stale exe is an intermediate branch state carrying two behaviours this PR deliberately reverted, so it is not equivalent to any reviewed source.
Nits
build.ymlOPENDJ_GIVEN=1scenario asserts only a non-zero exit: it is the only one of eight refusal scenarios with no message assertion, and-eq 0also accepts3010— the fail-open outcome the neighbouring scenarios use-ne 1603to exclude. The guard it expects already has a grep three lines above:"location could not be determined".chap-upgrade.adocover-promises: "The installer further refuses to install into a directory other than the detected one" is false in the case above, and contradicts the NOTE ten lines below telling the reader to name a directory. Worth scoping once the guard is fixed.- Stale comments in
package.wxs: "build.yml asserts that in the built MSI" and "extracted from the built MSI" — since6bcada5the step readspackage.wxsitself, so nothing inspects the built package. Also "all 42 msiexec calls" — there are 43 (that count was off when written). Sequence="first"is untested: all 43 msiexec calls are/qn, so reverting it tobothis invisible to CI even though it is what keeps theOPENDJ_GIVENprefix test from being a tautology in a UI install. One regex in the step that already reads the.wxswould pin it.- Maintenance-mode in-use detection (pre-existing, follow-up):
CheckServerNotRunningis gated onWIX_UPGRADE_DETECTED, andFindRelatedProductsdoes not run in maintenance mode, so repair/reinstall/uninstall get no in-use check. Not a regression — master had no guards and RM could not shut down the detached JVM either — andMSIRESTARTMANAGERCONTROL=Disablefixes a reproduced failure, so it should stay. If widened later, note that a bareOR Installedwould make a running server un-uninstallable; it needs to excludeREMOVE="ALL".
maven-release-plugin rewrites the versions of ${reactorProjects}, and
release.yml runs release:prepare on ubuntu-latest with no -P. Gating
opendj-msi on Windows plus an installed wix.exe therefore froze both MSI poms
at whatever parent version they happened to carry. Maven does not fail on that
skew - it resolves the parent from the repository instead - so the module would
have kept building as the previous version: unpacking that line's published
snapshot rather than the server zip built beside it, naming the package after
it, and handing parse-version the numbers ProductVersion is built from, which
is what FindRelatedProducts keys the upgrade on. The first release would have
attached opendj-<version>-SNAPSHOT.msi built from pre-release bits, and the
next development cycle would have pointed the MSI tests at a stale published
zip instead of at the branch under test.
What cannot run outside Windows is the wix invocation, not the module, so that
is what the profile gates now: opendj-msi is back in the unix, mac and windows
module lists, and distribution-windows-msi moved into opendj-msi-standard
around its <build>. Inactive, the module is a pom that builds nothing - the
same "no MSI artifact, no 'Cannot run program wix'" behaviour as before.
maven.deploy.skip keeps it out of Maven Central, which reactor membership
would otherwise put it back into.
Verified with release:prepare -DdryRun=true: both MSI poms are rewritten to
the release version, and the set of rewritten poms matches the active reactor.
RequireDirOnCustomUpgrade only asked whether the resolved directory was the x64 default, so with nothing in the registry and no server in the legacy default a mistyped target - msiexec /i opendj.msi OPENDJ=C:\opendj-custmo over a server in C:\opendj-custom - passed it, and RefuseRelocatingUpgrade has nothing to compare against in that topology either. RemoveExistingProducts emptied one directory and InstallFiles populated another one letter away; the only thing that had ever refused it was an unrelated tree happening to sit in the legacy default. Key the guard on the registry-less, legacy-less case as a whole and let it through on directory evidence instead: the target holds a setup.bat AND either it is not the fallback default or it was named. NOT OPENDJ_LEGACY_INSTALL keeps the headline legacy-default upgrade out of it, which guard (2) watches for relocation. That also settles OPENDJ_GIVEN: with the evidence a conjunct rather than an alternative, a hand-passed prefix as short as "C" - which matches every resolved path - disarms nothing, because the directory still has to hold a server. And it covers the wizard for the first time: browsing to the real installation carries its own evidence, browsing to an empty directory is refused like the typo. CI gets the typo scenario on the topology the neighbouring one already sets up. The OPENDJ_GIVEN=1 scenario now asserts 1603 and the guard's own message rather than a non-zero exit code, and the step that reads the authoring pins Sequence="first" - without it OPENDJ_GIVEN is re-set after CostFinalize in a full-UI install and its prefix test compares a value with itself, which no /qn scenario can see. The refusal message and chap-upgrade.adoc now say what the guard actually does, and three comments claiming build.yml reads the built MSI are corrected to the authoring it has read since 6bcada5.
|
All three confirmed and two of them fixed — 1. Reactor — fixed, and your one-liner needed two additionsConfirmed exactly as described: master carried Keeping Second addition: reactor membership would have put both poms straight back into the Central deploy this PR removed them from, so Verified with The same run also demonstrates the mechanism you named: the poms that got a Two details worth adding to the record, because they make the blocker worse than "the MSI is misnamed":
2. Guard 1 — your condition, taken as writtenCondition="WIX_UPGRADE_DETECTED AND NOT OPENDJ_REG AND NOT OPENDJ_LEGACY_INSTALL AND NOT ((OPENDJ_GIVEN_INSTALL ~>< OPENDJ) AND ((OPENDJ ~<> OPENDJ_DEFAULT) OR (OPENDJ_GIVEN AND (OPENDJ ~<< OPENDJ_GIVEN))))"I walked it against every scenario in You are right about CI gets the typo case on the topology the neighbouring scenario already builds: msiexec /i opendj.msi OPENDJ=C:\opendj-custmo # over a server in C:\opendj-customasserted at exactly The refusal message now says the directory holds no OpenDJ server and suggests checking for a typo, and 3.
|
maximthomas
left a comment
There was a problem hiding this comment.
Round 11 items 1 and 2 look right — ad4ba90 keeps both MSI poms versioned, and c22a4da closes the mistyped/relocating upgrade hole (I re-derived the guard state space from scratch; it holds, including the OPENDJ_GIVEN=1/=C bypasses, both decoy topologies, and the documented workaround).
One major in the new reactor fix: the mechanism that is supposed to keep the modules off Maven Central does nothing.
Maven Central guard is inert (major)
opendj-packages/opendj-msi/pom.xml:43 relies on maven.deploy.skip, but that property is dead in this build. pom.xml:336-345 declares central-publishing-maven-plugin:0.8.0 with <extensions>true</extensions>, whose lifecycle participant unconditionally wipes the deploy plugin's executions:
private void maybeSkipMavenDeployPlugin(Model, Plugin);
7: ifnull 24 // only condition: plugin absent
14: invokeinterface List.clear:()V
21: invokespecial setUpCentralPublishingExecution // publish @ deploy
maven.deploy.skip gated only maven-deploy-plugin:deploy, which no longer has a binding — the string does not occur anywhere in the plugin jar. Reproduced at head: mvn deploy -pl :opendj-msi-standard never runs deploy:deploy, and central-publishing:publish is still reached.
Effect on a 5.2.0 release — opendj-msi-standard is now an unconditional module and the wine install is gone, so Central gets:
opendj-msi/5.2.0/opendj-msi-5.2.0.pom
opendj-msi-standard/5.2.0/opendj-msi-standard-5.2.0.pom
and no .msi. So the PR's stated outcome — "the Maven coordinate stays at its last published version (5.1.2)" — is not what happens. opendj-msi-standard-5.1.2.msi resolves today; a consumer moving to 5.2.0:msi gets a resolvable pom and a hard Could not find artifact ...:msi, rather than the clean "version does not exist" the PR intends.
Fix: excludeArtifacts on the existing root plugin config. It is matched against the bare artifactId (excludeArtifacts.contains(artifact.getArtifactId())), so groupId:artifactId would silently never match:
<excludeArtifacts>
<excludeArtifact>opendj-msi</excludeArtifact>
<excludeArtifact>opendj-msi-standard</excludeArtifact>
</excludeArtifacts>Verified: this yields [INFO] No files to stage! and no bundle, with the rest of the reactor still published. Bumping the plugin to >= 0.9.0 (where skipPublishing becomes per-module) also works. Please do not use skipPublishing at 0.8.0 — it is session-global there: in a non-last module it is a no-op, and in the last module it skips the entire release with BUILD SUCCESS.
Uninstall still reports "already disabled" when the SCM is unreadable (minor)
opendj-server-legacy/src/build-tools/windows/service.c:2553 now returns 3 when the service list cannot be read, which fixes the no-service path. The path where a service is found still fails open: removeServiceWithServiceName calls serviceNameInUse(), whose own failure branch (service.c:1990) returns SERVICE_RETURN_ERROR, and everything that is not SERVICE_IN_USE maps to exit 1 / "Service does not exist" — which ConfigureWindowsService.disableService reads as SERVICE_ALREADY_DISABLED.
If the SCM becomes unreadable between the getServiceName enumeration and the serviceNameInUse enumeration, --disableService claims the service was already disabled and uninstall proceeds, leaving a registered auto-start service pointing at a removed tree — the same lie service.c:2499 set out to remove.
Launcher refresh rebases onto unvalidated commits (minor)
.github/workflows/deploy.yml:140 runs git rebase FETCH_HEAD before the Maven publish at .github/workflows/deploy.yml:196 and before the artifact uploads. If the branch moved after checkout, the tree fast-forwards onto commits outside workflow_run.head_sha that the triggering Build never validated, and those bits are what get deployed. Two pushes landing close together are enough. Either move the refresh after the Maven steps, or reset the worktree back to HEAD_SHA for the build.
Nits
- Stale
package.wxscomment:opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs:125still says additional instances fall back to WiX3-era delete-on-reboot.CheckServerNotRunningis not ImagePath-gated (package.wxs:455-459) and fires onWIX_UPGRADE_DETECTEDalone, so a runningOpenDJ Server-2holdslocks\server.lockand the upgrade is refused with 1603 instead. Same for ako/zh_TWservice registered asOpenDS. The residual is only that the stop is not attempted — a refusal, not damage. - Stale
-Pjustification:.github/workflows/release.yml:225says-Pguards against "Could not find the selected project in the reactor". Since the profile moved intoopendj-msi-standard's<build>, that failure can no longer occur. What-Pnow buys is forcing the plugins on whenwix.exeis not under%USERPROFILE%\.dotnet\tools; without it the job builds a pom that produces nothing andfail_on_unmatched_filesis the only symptom. - Unrelated plugin bump: dropping
<version>1.3.2</version>from themib-generationexecution atopendj-server-legacy/pom.xml:1406moves it onto the managedexec-maven-plugin3.6.3. That execution lives in thesnmpprofile, which auto-activates whenever${opendmk.lib.dir}/jdmkrt.jarexists — i.e. on nearly every build on every platform, not just the MSI leg. Worth its own commit or a line in the description.
Modernizes the Windows MSI end to end. Builds on #664 (branched from it) and includes the Windows script quoting fixes from #671 (they merge cleanly once #671 lands).
Toolchain: WiX 3.11 + wine → WiX v5, Windows-native
wixtool): heat/candle/light + wine + winetricks + dotnet40 collapse into a singlewix build -arch x64. WiX v5 is the newest version without the Open Source Maintenance Fee (v7 refuses to run without accepting the OSMF EULA).WIX0389for anyDirectory/@Nameand, with no names at all, on the missingmsi.dll— the MSI database is written by the Windows Installer library, so no cross-platform build exists. wine is removed frombuild.yml,deploy.ymlandrelease.yml.package.wxsrewritten to the v4+ schema: single<Package>,MediaTemplate CompressionLevel="high",InstallerVersion=500,<Files>harvest from Ant-staged payloads, empty instance directories shipped viaCreateFoldercomponents (the harvest ships files only).opendj-msimodules stay in every reactor, and thedistribution-windows-msiprofile that requires Windows plus an installedwixtool sits insideopendj-msi-standard, around its<build>: inactive, the module is a pom that builds nothing, so a plainmvn installon a contributor's Windows machine no longer fails mid-reactor with "Cannot run program wix". Gating the module list instead - as an earlier revision of this PR did - would have frozen both MSI poms at their parent version forever, because maven-release-plugin only rewrites${reactorProjects}andrelease.ymlprepares the release onubuntu-latestwith no-P; Maven does not fail on that skew, it resolves the parent from the repository, so the module would have kept building as the old version - unpacking the previous line's published snapshot instead of the server zip built beside it.maven.deploy.skipkeeps the modules out of Maven Central, which reactor membership would otherwise put them back into.Installer behavior
C:\Program Files\OpenDJ(was x86 /Program Files (x86)).windows-service.bat --enableService(orsetup) step, and the service that results belongs to the administrator — the package never creates, removes or reconfigures one. An earlier revision of this PR registered anOpenDJservice from the installer; it was reverted in the sixth review round because it silently switched every MSI install into service mode, sostart-ds.batdispatched to the SCM andstart-ds.bat -Nfailed outright even for a server that was never meant to run as a service.RemoveExistingProductsempties the old tree during the sequence pass, and the JVM openslib\*.jarwithoutFILE_SHARE_DELETE(JDK-8224794, Won't Fix), so a running instance turns the nested uninstall into delete-on-reboot entries. An immediate best-effortnet stopruns first, then an immediate check that lets the upgrade continue only when the service is gone orStoppedand otherwise refuses it (error 1722 namingCheckServiceStopped). The check polls rather than sampling the SCM once:net.exereturns the instant a control cannot be accepted, which is the case in every pending state, so a single "is it Running" test passed exactly the situations the stop had not handled — an administrator's ownnet stopstill in flight, or a service still starting, which the wrapper gives 300 s while its JVM holdslib\*.jar. A stop already under way is waited out for 90 s; every other state refuses immediately. Both actions are gated on the service's ownImagePathpointing into the directory being installed to, so an unrelated instance's service is never touched. The stop succeeds from an elevated console, SCCM, SYSTEM or CI and cannot under the filtered token of a UAC double-click — which is exactly why the check exists rather than a silent half-upgrade.start-ds.batis the default way an MSI-installed server runs, and that JVM holdslib\*.jarexactly as a service-hosted one does. A third check,CheckServerNotRunning, probes the exclusive byte-range lock the server holds onlocks\server.lock— the same evidencewait-server-stopped.ps1uses in CI, because a zero exit code fromstop-dsis not proof the JVM let go (Flaky Test on Windows: winlauncher.exe stop can return success without stopping the server #768) — and refuses the upgrade while it is held, allowing 60 s for a stop already under way. It is gated on nothing butWIX_UPGRADE_DETECTED, so it covers both modes. Until the tenth review round it could not refuse at all: the inlinecatchcompared$_.Exception.GetType().FullNamewithSystem.IO.IOException, and PowerShell wraps anything thrown out of a .NET method or constructor in aMethodInvocationException, so every exception — the held lock included — took the fail-open branch and theexit 1was unreachable. It is a typedcatch [System.IO.IOException]now, with the[\[]/[\]]escapes a Formatted field needs so the type literal is not substituted away, and CI runs the command directly against a held lock instead of inferring it from an install. Restart Manager is set toDisable: RM must not stop the service — it would restart the upgraded server beforeupgrade.bathas migrated the instance data, and would take aStartPendingservice away from the check above — andDisableShutdownis not a middle ground, which is measured rather than reasoned. It keeps RM's detection, which is exactly why it was tried, but the restart is not part of what it disables: the headline upgrade scenario came out of that run with the serviceStartPending, because the installer restarts whatever the RM session listed whether RM stopped it or the guards did. Turning RM off takes its detection with it, leaving the legacy in-use scan, which finds windowed applications and therefore never a headless JVM — so the lock check is what covers both modes instead, including/quiet, where a FilesInUse dialog could not be shown anyway.config,db,logs) is preserved; an explicitOPENDJ=...always wins. During an upgrade the legacy directory has to prove it holds a server (asetup.batin it) before it is adopted:Installedis unset throughout a major upgrade, so a bare existence test would have routed the new tree into a leftover empty directory whileRemoveExistingProductsemptied the real installation somewhere else. A registered service survives untouched: it names the wrapper inside the installation directory, which the upgrade replaces in place, so the registration keeps working against the refreshed server. Verified in CI by upgrading from the released 5.1.2 MSI at its real x86 default with noOPENDJgiven, with the service left running.AllowSameVersionUpgradeskeeps a same-version hotfix re-release from producing two ARP entries.CostFinalize— the one point whereOPENDJis final whether the installer was started silently or with full UI:RemoveExistingProductsempties the old tree would strandconfig/db/logs. The refusal covers the registry-less case as a whole — nothing recorded a location and no server sits in the legacy default — and lets an upgrade through only on directory evidence: the target holds asetup.batand either it is not the fallback default or it was named. Somsiexec /i ... /quietnaming nothing still refuses; so does a directory that was named but holds no server, which is what a typo looks like (OPENDJ=C:\opendj-custmoover a server inC:\opendj-customused to pass both guards and strand the data); while a GUI administrator browses to the existing directory inInstallDirDlgand proceeds on the evidence that directory carries. (An earlier revision aborted in the UI sequence beforeWelcomeDlg, leaving a GUI-only administrator with nothing but a command line to retype.) The one case the wizard cannot resolve is an old server in the default directory itself: the "was it named" flag is read beforeAppSearchandInstallDirDlgnever sets it, so browsing to the default leaves the guard with the values a "Next, Next, Install" session produces. The refusal message and the install guide say so and point at the command line, rather than promising a dialog that cannot help. That flag isOPENDJ_GIVEN, public and secure and holding the named path rather than a boolean: it was a private property until the tenth review round, and a private property set in the UI sequence never reaches the execute sequence — which is processed in the installer service, not in the client — so every full-UI session read it as empty at the guards, including one started withOPENDJ=on the command line, and the wizard therefore refused the very workaround the message prescribes.OPENDJcan now be passed with or without/quiet. The guards require the resolved directory to start with the value, which keeps the property from being a switch:OPENDJ_GIVEN=1disarms nothing, and a value that does pass has spelled out the directory the install resolved to. Evidence alone is deliberately not enough in the default directory: any unrelated OpenDJ tree — a zip install, a copy — satisfies asetup.batsearch without being the product being upgraded, and keying the guard on that alone let such a decoy disarm it whileRemoveExistingProductsgutted the real installation elsewhere, which is what the "or it was named" half is for. Naming the directory is the way through, including for an old server that really does live in the default one, which a registry-less 5.1.x is indistinguishable from the decoy without being told. The same decoy in the legacy default directory remains undetectable when nothing is named — nothing a registry-less 5.1.x left behind separates the two, and refusing every registry-less legacy-default upgrade would refuse the documented main upgrade path with it — so the install guide carries the workaround instead: name the real directory, which the relocation guard below now lets through.OPENDJ=<somewhere else>over a detected installation is not a move — the old tree is emptied while the new one is installed elsewhere. Refused whenever the existing location is known authoritatively (the recorded registry value, or a legacy default directory that really holds a server) and the requested directory is not the same one. The comparison is an equality test, not a substring one: "contains" would have admittedC:\opendj\v2over a recordedC:\opendj, which is the relocation being guarded against. The legacy branch makes one exception, without which the two guards contradicted each other: a directory that was named on the command line and holds a server is taken as the installation being upgraded, not as a relocation target. That is exactly the workaround the decoy residual above prescribes, and refusing it left such a host with no upgrade path at all. Naming an empty or new directory is still a relocation and still refused, and the registry branch keeps refusing unconditionally — a location this package recorded is authoritative. The residual: a named directory holding some other OpenDJ tree is now accepted, which directory evidence cannot distinguish from the real one; between refusing the documented workaround and trusting an explicit instruction, the instruction wins.JAVA_HOME/PATH launch condition was tried and dropped — it blocked upgrades of working servers and never consultedOPENDJ_JAVA_HOME, see the review discussion and Add CI install-test for the Windows MSI + document MSI install/upgrade/uninstall #664).setupand the server require Java 11+; the install guide points at https://adoptium.net. The MSI is not code-signed (no certificate) — SmartScreen note added to the docs.Release / deploy
deploy.ymlreuses the MSI already built by the triggering Build run (no rebuild).release.yml: newrelease-msijob (windows,continue-on-error) installs the released server zip into the local repository and packages only:opendj-msi-standard, then attaches the MSI to the GitHub release.opendj-msi-standardis intentionally no longer published to Maven Central (maven.deploy.skip, since the module is in every reactor so the release plugin keeps versioning it): it was previously wine-built on Linux and deployed from the ubuntu deploy jobs; it is now built natively on Windows and distributed through GitHub Releases and the Package/DeployOpenDJ MSI Packageartifact. The Maven coordinate stays at 5.1.2 as its last published version. This also applies tosustaining/4.10.x: Package/Deploy always runs master's workflow, which no longer installs wine, while the 4.10.x pom still builds its MSI through it — accepted; the 4.10.x MSI remains available up to its last published release.opendj-server-legacy/lib/*.exeis what every Linux-built server zip ships — and with it the tagged releases and the Maven Central artifacts — while only a Windows job can rebuild them, so a native source change that was never re-committed shipped the old wrapper while CI stayed green (mastercarried exactly that gap from April to July). Package/Deploy now downloadswindows-exe-11from the triggering Build run and commits the binaries back when their bytes differ. It runs only after a green push build on a release branch and already holdscontents: writefor the wiki push, sobuild-mavenstays read-only;/Breproin the Makefile keeps the output a function of the sources, without which it would commit on every run; and pushes made withGITHUB_TOKENstart no workflow run, so it cannot loop. The step sits ahead of the Maven deploy so the published snapshot carries the fresh launchers, which also means it must never be what costs that deploy: it retries the push three times over a rebase and then warns and lets the job continue. The warning-only comparison step and the standaloneNative launchersworkflow are removed.CI
test-msi: installs into the spaced x64 default directory, asserts the package registered no service and thatlib\opendj_service.exewas laid down, runssetup --doNotStart, then starts and stops the server withstart-ds.bat/stop-ds.bat— proving an MSI install that never asked for service mode behaves like a zip one. Only then does it enable the service withwindows-service.bat, start it,ldapsearch, stop and disable it;uninstall.bat --climust remove the instance and disable a service it finds;msiexec /xmust leave nothing behind. The wait-for-the-lock helper the zip test uses afterstop-ds(a zero exit code is not evidence the JVM let go — Flaky Test on Windows: winlauncher.exe stop can return success without stopping the server #768) moved to.github/scripts/wait-server-stopped.ps1so both jobs share one copy.test-msialso checks the guards without installing anything, before the install steps: it reads the MSI authoring — wix copies sequence conditions andExeCommandinto the tables verbatim, and reading the built package's tables through theWindowsInstaller.InstallerCOM API returned no rows at all under pwsh 7 — and asserts that the two refusal conditions name only public properties (a lower-case letter in a condition is a private property, which reads as empty in the installer service and silently inverts the guard), thatOPENDJandOPENDJ_GIVENare declaredSecure, thatOPENDJ_GIVENis still captured withSequence="first"(without it the property is re-set afterCostFinalizein a full-UI install and the guards' prefix test compares a value with itself), and then runsCheckServerNotRunning's command exactly as msiexec runs it — Formatted field resolved, handed tocmd.exe, so Windows PowerShell 5.1 executes it — against a real held byte-range lock: exit 0 with no lock file, 0 with an unlocked one, 1 while a second process holds it, and not before the 60 s grace is up. The scenario suite could see neither failure on its own: it reaches the inline PowerShell only through a ten-minute install and can then judge it by msiexec's exit code alone, and all 42 of itsmsiexeccalls are/qn, which runs no UI sequence at all.test-msi-upgrade: released 5.1.2 x86 MSI at its nativeProgram Files (x86)\OpenDJdefault → configure → service registered the pre-MSI way and left running → upgrade with the new x64 MSI with noOPENDJgiven: the installer stops the running service (the stop+check pair, elevated here), keeps the same directory, leaves the data intact and the registration in place, thenupgrade.bat, start through that same service, search, stop. A repair (REINSTALL=ALL) must not disturb the registration; disabling the service before uninstalling must leave no orphan. Plus fresh-install and refusal scenarios: the legacy default directory is auto-detected, the registry-recorded location wins over it, a silent upgrade from an undetectable custom directory refuses with guidance and the same upgrade succeeds once the directory is named, a leftover empty legacy directory is not adopted as the target of an upgrade, an upgrade that would relocate to another directory is refused on both branches of the guard, an install into a third directory leaves a zip instance'sOpenDJ Serveruntouched despite a leftover legacy directory, an unrelated OpenDJ tree in the x64 default directory is not adopted as the target of an upgrade whose real installation is elsewhere (refused, then completed once the directory is named), a 5.1.x installed in that default directory refuses the silent upgrade and accepts the named one, an upgrade started while the service isStartPendingis refused byCheckServiceStoppedwith the instance intact — that state produced deterministically by holdingbat\start-ds.batopen, which keeps the wrapper reportingSERVICE_START_PENDING— a decoy in the legacy default directory does not block the documented workaround (naming an empty directory is still refused as a relocation, naming the real installation upgrades it), and an upgrade over a server running without a service is refused byCheckServerNotRunningand then succeeds oncestop-ds.bathas run. The relocation scenarios install the released 5.1.2 package first: reinstalling this MSI over itself is maintenance mode, whereFindRelatedProductsdoes not run, soWIX_UPGRADE_DETECTEDwould never be set and the guard could not fire. The two check-based refusals assert the exact1603and the failing action'sReturn value 3in the log, and the running-server one also fails on any newPendingFileRenameOperationsentry:-ne 0accepts3010, which is precisely what a guard that fails open produces, and the action's name appears in the log whether it refused or waved the upgrade through — which is how a completely inertCheckServerNotRunningstayed green for two rounds. An upgrade passingOPENDJ_GIVEN=1must still be refused, as must one naming a directory that holds no server - the mistyped-target case, asserted on the topology the undetectable-directory scenario already builds.build-mavenany more. The matrix's ubuntu legs run for about two hours, so every push inside that window cancelled the run before these two-minute jobs started — which is how several rounds of guard work reached review with no completed run behind them. Both now depend on a smallwait-msi-artifactjob that polls for thewindows-latest-11artifact and gives up early if the Windows leg finishes without publishing it. That job runs on ubuntu: waiting onwindows-latestheld a Windows runner from t=0 and competed for the capacity the leg being waited for needs (in run 31578978374 the windows/11 leg queued for 38 minutes while windows/26 started within one), and its fixed 45-minute budget covered the leg's runtime but not its queue time — in that same run the artifact appeared at +52 minutes, so both jobs would have failed a healthy build. One gate job also means one copy of the wait instead of two.wix msi validate -sice ICE61(ICE):wix builditself runs no ICE validation, so a green build alone would not catch sequencing mistakes like ICE63. ICE61 is suppressed becauseAllowSameVersionUpgradesauthors it by design.Windows script fixes surfaced by these tests (in #671)
Install paths with spaces/parentheses (
C:\Program Files (x86)\OpenDJ— the old default!) broke the batch scripts in four places: unquotedjava.io.tmpdir,setcp.batargument parsing,"%VAR%" == ""checks with the now-quoted tmpdir, and thestart-ds.battmp-cleanup block. All fixed and exercised by the MSI tests in this PR.Deferred
masterwhose Build is red — and can be added on request.The launcher binaries under
opendj-server-legacy/libwere refreshed from thewindows-exe-11artifact in 24b1b7e.service.chas changed in code since —openScmtestedscmwhere it meant*scm, so a failedOpenSCManagerwas never reported (pre-existing; the error still surfaced from the NULL handle, only without the message) — so the committed binaries are one fix behind their sources until Package/Deploy re-commits them on the next release-branch push. The Windows build leg compiles them itself before packaging, so the MSI and the Windows-built zip already carry the fix. The GUI wizard path (license-dialog skip,InstallDirDlgpre-population, the two upgrade refusals) is not covered by CI — one manual run before merge is planned; the property-visibility half of it, a guard reading something the installer service never receives, is now asserted statically against the built MSI.The guards have now executed in finished runs, and one of those runs is what exposed the fail-open described above. Run
31619923691(12 Aug, heade6f0a44) finished withtest-msi,test-msi-upgradeandwix msi validate -sice ICE61green — the only red job wasbuild-docker-alpine, which has nothing to do with the MSI — and its log shows the running-server scenario printingexit 3010where every other refusal prints1603: the upgrade went through and deferred the locked jars to a reboot, while a-ne 0assertion accepted it.Run
31703288399(13 Aug, head6bcada5) is green end to end -test-msi,test-msi-upgrade, docker and the ICE validation - with the guard probe reportingCheckServerNotRunning: 0 with no lock file, 0 unlocked, 1 while held (after 61s). The run before it,31689733634(head59b7570), is the one that answered the fail-open:test-msi-upgradegreen withUpgrade refused while a non-service server was running (exit 1603), the failing action'sReturn value 3in the log and no delete-on-reboot entry left behind.test-msifailed in that run on the new guard step's MSI-table read rather than on anything it was testing; that read is replaced in 6bcada5, and the run on this head is what confirms it. The gate job described under CI is what made finished runs reachable in ~20 minutes instead of two hours.