From b5cb2b48d17d114eece4c1bec2e1126eade5d2af Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Sat, 05 Sep 2026 18:27:43 +0000
Subject: [PATCH] Modernize the Windows MSI: WiX v5, x64, upgrade from 5.1.x (#701)
---
.github/workflows/build.yml | 852 +++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 796 insertions(+), 56 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index efb811e..c009b5c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -46,19 +46,12 @@
- { os: 'windows-latest', java: '26' }
fail-fast: false
steps:
- - name: Install wine+rpm for distribution
+ - name: Install rpm for distribution
if: runner.os == 'Linux'
shell: bash
run: |
- sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
- sudo dpkg --add-architecture i386
- sudo mkdir -pm755 /etc/apt/keyrings && sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key
- sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/$(lsb_release -c -s)/winehq-$(lsb_release -c -s).sources
sudo apt-get update
- sudo apt install --install-recommends winehq-stable || sudo apt install --install-recommends winehq-staging
- wine --version
- version="9.4.0"; sudo wget "https://dl.winehq.org/wine/wine-mono/$version/wine-mono-$version-x86.msi" -O /tmp/wine-mono.msi
- wine msiexec /i /tmp/wine-mono.msi
+ sudo apt-get install -y rpm
- uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -88,15 +81,45 @@
shell: cmd
run: |
cd opendj-server-legacy\src\build-tools\windows
- nmake all
- xcopy /Y *.exe ..\..\..\lib\
+ nmake all || exit /b 1
+ xcopy /Y *.exe ..\..\..\lib\ || exit /b 1
git status
+ # Also the source of truth for the committed opendj-server-legacy/lib/*.exe: on a
+ # successful push build, deploy.yml downloads windows-exe-11 from this very run and
+ # commits its contents back to the branch. Nothing here compares them with what is
+ # committed - an MSVC toolchain bump on the runner image changes the bytes on its own,
+ # so a byte-for-byte gate would fire without a source change.
+ - name: Upload Windows exe artifacts
+ if: runner.os == 'Windows'
+ uses: actions/upload-artifact@v7
+ with:
+ name: windows-exe-${{ matrix.java }}
+ retention-days: 5
+ path: opendj-server-legacy/src/build-tools/windows/*.exe
- name: Set Integration Test Environment
id: failsafe
if: runner.os == 'Linux'
run: |
echo "MAVEN_PROFILE_FLAG=-P precommit" >> $GITHUB_OUTPUT
+ - name: Setup WiX (.NET tool) for MSI
+ # Only the java 11 job's MSI is consumed downstream (test-msi*, deploy.yml); without
+ # wix installed the distribution-windows-msi profile stays inactive, so the other
+ # Windows jobs skip the MSI build entirely instead of producing an artifact nothing
+ # uses.
+ if: runner.os == 'Windows' && matrix.java == '11'
+ shell: bash
+ run: |
+ # The MSI builds on Windows only (WiX cannot author MSIs on Linux/macOS). WiX 5 ships as a
+ # net6.0 tool; allow it to run on the newer .NET runtime present on the runner.
+ echo "DOTNET_ROLL_FORWARD=Major" >> "$GITHUB_ENV"
+ export DOTNET_ROLL_FORWARD=Major
+ dotnet tool install --global wix --version 5.0.2 || dotnet tool update --global wix --version 5.0.2
+ echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
+ export PATH="$HOME/.dotnet/tools:$PATH"
+ wix --version
+ wix extension add -g WixToolset.UI.wixext/5.0.2 || true
+
# The per-module javadoc:jar that runs during verify only ever sees one
# module's sources, so it cannot report a package declared by two modules
# at once. Only the aggregate on the root reactor can, and it used to run
@@ -116,6 +139,21 @@
env:
MAVEN_OPTS: -Dhttps.protocols=TLSv1.2 -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 -Dmaven.wagon.http.retryHandler.requestSentEnabled=true -Dmaven.wagon.http.retryHandler.count=10
run: mvn --batch-mode --errors --update-snapshots verify ${{ steps.javadoc.outputs.MAVEN_JAVADOC_GOAL }} --file pom.xml ${{ steps.failsafe.outputs.MAVEN_PROFILE_FLAG }}
+ - name: Validate the MSI (ICE)
+ # wix build runs no ICE validation (only MSBuild projects or an explicit validate
+ # do), so a green build alone proves the authoring compiles, not that it validates -
+ # e.g. the ICE63 rule about script-generating actions sequenced before
+ # RemoveExistingProducts would go unnoticed without this step.
+ if: runner.os == 'Windows' && matrix.java == '11'
+ shell: bash
+ run: |
+ msi=$(ls opendj-packages/opendj-msi/opendj-msi-standard/target/*.msi)
+ # ICE61 fires by design: AllowSameVersionUpgrades authors an UpgradeVersion row
+ # whose range includes the product's own version, which is exactly what makes a
+ # rebuilt hotfix at the same 3-part version upgrade rather than install alongside.
+ # Left unsuppressed it is permanent noise this step could not tell from a real
+ # regression.
+ wix msi validate -sice ICE61 "$msi"
- name: Test on Unix
if: runner.os == 'Linux'
run: |
@@ -311,21 +349,7 @@
- name: Test on Windows
if: runner.os == 'Windows'
run: |
- # Verify a stop took effect before moving on: wait until the server
- # releases the exclusive byte-range lock it holds on locks\server.lock.
- # The explicit Lock(0, 1) probe is required: a byte-range lock does not
- # prevent opening the file, so a bare Open() would always succeed.
- function Wait-ServerStopped($lockFile) {
- $lockFile = Join-Path $PWD $lockFile
- for ($i = 0; $i -lt 30; $i++) {
- if (-not (Test-Path $lockFile)) { return }
- try {
- $fs = [System.IO.File]::Open($lockFile, 'Open', 'ReadWrite', 'ReadWrite')
- try { $fs.Lock(0, 1); $fs.Unlock(0, 1); return } finally { $fs.Close() }
- } catch { Start-Sleep -Seconds 2 }
- }
- throw "The server still holds the lock on ${lockFile}: the stop did not take effect"
- }
+ . .github\scripts\wait-server-stopped.ps1
set OPENDJ_JAVA_ARGS="-server -Xmx512m"
opendj-server-legacy\target\package\opendj\setup.bat -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --sampleData 5000 --cli --acceptLicense --no-prompt
opendj-server-legacy\target\package\opendj\bat\status.bat --hostname localhost --bindDN "cn=Directory Manager" --bindPassword password --trustAll
@@ -385,14 +409,6 @@
if ($LASTEXITCODE -ne 0) { throw "net stop 'OpenDJ Server' failed with exit code $LASTEXITCODE" }
opendj-server-legacy\target\package\opendj\bat\windows-service.bat --disableService
- - name: Upload Windows exe artifacts
- if: runner.os == 'Windows'
- uses: actions/upload-artifact@v7
- with:
- name: windows-exe-${{ matrix.java }}
- retention-days: 5
- path: opendj-server-legacy/src/build-tools/windows/*.exe
-
- name: Upload artifacts OpenDJ Server
uses: actions/upload-artifact@v7
with:
@@ -915,9 +931,258 @@
rpm -e opendj
'
+ # The gate both MSI jobs wait on. Deliberately not "needs: build-maven": that waits for
+ # the whole matrix, whose ubuntu legs run for about two hours, so any push landing inside
+ # that window cancels the run before those two-minute jobs have started - which is how the
+ # MSI work reached its eighth review round with no completed run behind it. The only input
+ # they have is the windows-latest-11 artifact, so wait for exactly that.
+ #
+ # On ubuntu, and in a job of its own, for two reasons. A waiter on windows-latest holds a
+ # Windows runner from t=0 for the whole wait, competing for the capacity the leg it is
+ # waiting for needs - in run 31578978374 the windows-latest-11 leg sat in the queue for 38
+ # minutes while windows-latest-26 started within one. And the wait was duplicated
+ # verbatim in both jobs, with a 45-minute budget measured from t=0 that 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 perfectly healthy build.
+ wait-msi-artifact:
+ runs-on: 'ubuntu-latest'
+ # Well past any queue seen so far, and only reached if the Windows leg neither publishes
+ # nor finishes; the usual exits are the artifact appearing or the leg failing.
+ timeout-minutes: 130
+ permissions:
+ contents: read
+ # Listing the run's artifacts and jobs, which the wait below polls.
+ actions: read
+ steps:
+ - name: Wait for the Windows build artifact
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ deadline=$(( $(date +%s) + 120 * 60 ))
+ while true; do
+ if gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts" \
+ --jq '.artifacts[].name' | grep -qx 'windows-latest-11'; then
+ echo 'windows-latest-11 is available'
+ exit 0
+ fi
+ # Stop waiting the moment the leg that would publish it has finished without
+ # doing so, instead of sitting out the deadline.
+ # || true: a transient API error is a reason to poll again, not to fail the run.
+ conclusion=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs?per_page=100" \
+ --jq '.jobs[] | select(.name | startswith("build-maven (windows-latest, 11)")) | .conclusion' || true)
+ if [ -n "$conclusion" ] && [ "$conclusion" != "null" ]; then
+ echo "the Windows build leg finished as '$conclusion' without publishing windows-latest-11" >&2
+ exit 1
+ fi
+ if [ "$(date +%s)" -ge "$deadline" ]; then
+ echo 'windows-latest-11 was not published within 120 minutes' >&2
+ exit 1
+ fi
+ sleep 30
+ done
+
test-msi:
- needs: build-maven
+ needs: wait-msi-artifact
runs-on: 'windows-latest'
+ permissions:
+ contents: read
+ steps:
+ # Only for .github/scripts/wait-server-stopped.ps1 and the MSI authoring the guard
+ # step below reads, and it has to come first: checkout cleans the workspace the
+ # artifact is unpacked into. Sparse because those two are the entire reason for it.
+ - uses: actions/checkout@v6
+ with:
+ sparse-checkout: |
+ .github/scripts
+ opendj-packages/opendj-msi/opendj-msi-standard/resources/msi
+ - name: Download artifacts
+ uses: actions/download-artifact@v8
+ with:
+ name: windows-latest-11
+ - name: Set up Java
+ uses: actions/setup-java@v5
+ with:
+ java-version: '25'
+ distribution: 'zulu'
+ - name: The upgrade guards must hold up on their own
+ shell: pwsh
+ run: |
+ # Two things no install scenario can see, both of which have already gone wrong here.
+ # The first is what a guard is allowed to READ: the execute sequence is processed in
+ # the installer service, so a private property set in the UI sequence is empty by the
+ # time the guard evaluates - which inverts it in exactly the full-UI sessions that no
+ # /qn scenario runs. The second is the inline PowerShell itself: the scenarios reach
+ # it only through a ten-minute install and can then only look at msiexec's exit code,
+ # so a guard that always exits 0 looks the same as one that never has to refuse.
+ # Read from the authoring rather than from the built MSI's tables: wix copies both
+ # the sequence conditions and ExeCommand across verbatim, and it is test-msi-upgrade
+ # that judges the artifact - those scenarios now assert the refusal's own 1603 and
+ # "Return value 3" rather than any non-zero exit code.
+ $wxs = Get-Content -Raw opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs
+ $sequence = [regex]::Match($wxs, '(?s)<InstallExecuteSequence>(.*?)</InstallExecuteSequence>')
+ if (-not $sequence.Success) { throw "package.wxs has no InstallExecuteSequence" }
+ foreach ($action in @('RequireDirOnCustomUpgrade', 'RefuseRelocatingUpgrade')) {
+ $scheduled = [regex]::Match($sequence.Groups[1].Value, '(?s)<Custom Action="' + $action + '"(.*?)/>')
+ if (-not $scheduled.Success) { throw "$action is not scheduled in InstallExecuteSequence" }
+ $found = [regex]::Match($scheduled.Groups[1].Value, '(?s)Condition="([^"]*)"')
+ if (-not $found.Success) { throw "$action is scheduled without a condition" }
+ $condition = [System.Net.WebUtility]::HtmlDecode($found.Groups[1].Value)
+ # Property names in these conditions are upper case, and so are AND/NOT: a
+ # lower-case letter is a private property, which reads as empty in the service.
+ if ($condition -cmatch '[a-z]') { throw "$action reads a private property, which never reaches the installer service: $condition" }
+ Write-Host "$action : $condition"
+ }
+ foreach ($property in @('OPENDJ', 'OPENDJ_GIVEN')) {
+ if ($wxs -notmatch ('<Property Id="' + $property + '"[^>]*Secure="yes"')) { throw "$property must be Secure: the guards read it in the service, where a non-administrator's value is dropped otherwise" }
+ }
+ # Sequence="first" is what keeps OPENDJ_GIVEN meaning "the directory was named":
+ # without it the action re-runs in the execute sequence of a full-UI install, where
+ # OPENDJ has long been resolved, and the guards' prefix test would be comparing a
+ # value with itself. Every msiexec call in this workflow is /qn, which runs no UI
+ # sequence, so nothing else here would notice it going.
+ if ($wxs -notmatch '<SetProperty Id="OPENDJ_GIVEN"[^>]*Sequence="first"') { throw 'OPENDJ_GIVEN must be captured with Sequence="first", or it stops meaning "named" in a full-UI install' }
+ # CheckServerNotRunning, run exactly as msiexec runs it: the Formatted field
+ # resolved - [\[] and [\]] are its escapes for literal brackets, [property]
+ # references become their values - and handed to cmd.exe. Windows PowerShell 5.1 is
+ # what the command names, so that is what this exercises.
+ $found = [regex]::Match($wxs, '(?s)<CustomAction Id="CheckServerNotRunning".*?ExeCommand="([^"]*)"')
+ if (-not $found.Success) { throw "CheckServerNotRunning has no ExeCommand" }
+ $target = [System.Net.WebUtility]::HtmlDecode($found.Groups[1].Value)
+ $probeRoot = Join-Path $PWD 'guard-probe'
+ New-Item -ItemType Directory -Force (Join-Path $probeRoot 'locks') | Out-Null
+ $command = $target.Replace('[\[]', '[').Replace('[\]]', ']').Replace('[System64Folder]', "$env:SystemRoot\System32\").Replace('[OPENDJ]', "$probeRoot\")
+ Set-Content -Encoding Ascii -Path guard-probe.cmd -Value $command
+ Write-Host $command
+ function Invoke-Guard { (Start-Process cmd.exe -Wait -PassThru -ArgumentList '/c', 'guard-probe.cmd' -WorkingDirectory $PWD).ExitCode }
+ $lock = Join-Path $probeRoot 'locks\server.lock'
+ Remove-Item $lock -Force -ErrorAction SilentlyContinue
+ $rc = Invoke-Guard
+ if ($rc -ne 0) { throw "no lock file at all must let the upgrade proceed, got $rc" }
+ Set-Content -Path $lock -Value ''
+ $rc = Invoke-Guard
+ if ($rc -ne 0) { throw "an unlocked server.lock must let the upgrade proceed, got $rc" }
+ # A running server holds a mandatory byte-range lock over the whole file
+ # (LockFileManager: RandomAccessFile "rw" plus tryLock) and opens it shared, so the
+ # open succeeds by design and only the Lock call raises IOException - the exception
+ # PowerShell hands to the catch wrapped in a MethodInvocationException.
+ Set-Content -Path guard-holder.ps1 -Value @(
+ '$fs = [System.IO.File]::Open($env:GUARD_PROBE_LOCK, ''Open'', ''ReadWrite'', ''ReadWrite'')',
+ '$fs.Lock(0, [Int64]::MaxValue)',
+ 'New-Item -ItemType File -Force -Path "$env:GUARD_PROBE_LOCK.held" | Out-Null',
+ 'Start-Sleep -Seconds 300'
+ )
+ $env:GUARD_PROBE_LOCK = $lock
+ Remove-Item "$lock.held" -Force -ErrorAction SilentlyContinue
+ $holder = Start-Process pwsh -PassThru -ArgumentList '-NoProfile', '-File', 'guard-holder.ps1' -WorkingDirectory $PWD
+ for ($i = 0; $i -lt 30 -and -not (Test-Path "$lock.held"); $i++) { Start-Sleep -Seconds 1 }
+ if (-not (Test-Path "$lock.held")) { throw "the holder process never took the lock" }
+ $started = Get-Date
+ $rc = Invoke-Guard
+ $elapsed = [int]((Get-Date) - $started).TotalSeconds
+ Stop-Process -Id $holder.Id -Force -ErrorAction SilentlyContinue
+ if ($rc -ne 1) { throw "a held server.lock must refuse the upgrade (exit 1), got $rc after ${elapsed}s" }
+ # Refusing before the grace is up would mean the loop exited for some other reason.
+ if ($elapsed -lt 55) { throw "the refusal came after ${elapsed}s, so the 60 s grace was not polled through" }
+ Write-Host "CheckServerNotRunning: 0 with no lock file, 0 unlocked, 1 while held (after ${elapsed}s)"
+ Remove-Item -Recurse -Force $probeRoot, guard-probe.cmd, guard-holder.ps1 -ErrorAction SilentlyContinue
+ - name: Install MSI (silent)
+ shell: pwsh
+ run: |
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ if (-not $msi) { throw "MSI not found in the windows-latest-11 artifact" }
+ Write-Host "MSI: $msi"
+ # No OPENDJ property: exercise the x64 default C:\Program Files\OpenDJ (a path with
+ # spaces, which the server scripts must handle).
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v install.log"
+ if ($p.ExitCode -ne 0) { Get-Content install.log -Tail 80; throw "msiexec /i failed: $($p.ExitCode)" }
+ $root = "C:\Program Files\OpenDJ"
+ if (-not (Test-Path "$root\setup.bat")) { Get-Content install.log -Tail 80; throw "OpenDJ not installed into the x64 default $root" }
+ Write-Host "Installed to $root"
+ # The package lays the files down and registers no service: enabling one stays
+ # the administrator's explicit step (windows-service.bat / setup), exactly as
+ # for the zip distribution, so start-ds.bat keeps starting the server directly.
+ if (Get-Service OpenDJ -ErrorAction SilentlyContinue) { throw "the package must not register a service of its own" }
+ if (Get-Service "OpenDJ Server" -ErrorAction SilentlyContinue) { throw "the package must not register a service of its own" }
+ # The SCM wrapper is harvested with the rest of lib; windows-service.bat needs it.
+ if (-not (Test-Path "$root\lib\opendj_service.exe")) { throw "lib\opendj_service.exe missing from the MSI install" }
+ # Custom extension jars go into lib\extensions; the server warns on startup
+ # (WARN_ADMIN_NO_EXTENSIONS_DIR) when it is missing.
+ if (-not (Test-Path "$root\lib\extensions")) { throw "lib\extensions missing from the MSI install" }
+ "OPENDJ_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append
+ - name: Setup, then start and stop the server without a service
+ shell: pwsh
+ run: |
+ # An MSI install that was never asked for service mode must behave exactly like
+ # a zip one: start-ds.bat starts the server in this very session rather than
+ # dispatching to the SCM, and needs no elevation to do it.
+ $root = $env:OPENDJ_ROOT
+ $env:OPENDJ_JAVA_ARGS = "-server -Xmx512m"
+ & "$root\setup.bat" -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --addBaseEntry --cli --acceptLicense --no-prompt --doNotStart
+ if ($LASTEXITCODE -ne 0) { throw "setup.bat failed: $LASTEXITCODE" }
+ & "$root\bat\start-ds.bat"
+ if ($LASTEXITCODE -ne 0) { throw "start-ds.bat failed: $LASTEXITCODE" }
+ for ($i=0; $i -lt 12; $i++) { try { $c = New-Object System.Net.Sockets.TcpClient('localhost', 1636); $c.Close(); break } catch { Start-Sleep -Seconds 5 } }
+ & "$root\bat\ldapsearch.bat" --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
+ if ($LASTEXITCODE -ne 0) { throw "ldapsearch failed: $LASTEXITCODE" }
+ & "$root\bat\stop-ds.bat"
+ if ($LASTEXITCODE -ne 0) { throw "stop-ds.bat failed: $LASTEXITCODE" }
+ # A zero exit code from stop-ds does not mean the JVM let go of the lock (#768),
+ # and the next step registers and starts the service against this same instance.
+ . .github\scripts\wait-server-stopped.ps1
+ Wait-ServerStopped "$root\locks\server.lock"
+ - name: Enable, start, stop and disable the Windows service
+ shell: pwsh
+ run: |
+ # Service mode is opt-in and driven entirely by windows-service.bat, the same
+ # command the zip distribution uses. The service it registers takes the display
+ # name "OpenDJ Server" for the first instance on the host.
+ $root = $env:OPENDJ_ROOT
+ & "$root\bat\windows-service.bat" --enableService
+ if ($LASTEXITCODE -ne 0) { throw "--enableService failed: $LASTEXITCODE" }
+ if (-not (Get-Service -DisplayName "OpenDJ Server" -ErrorAction SilentlyContinue)) { sc.exe query; throw "--enableService did not register the service" }
+ net start "OpenDJ Server"
+ if ($LASTEXITCODE -ne 0) { throw "net start failed: $LASTEXITCODE" }
+ for ($i=0; $i -lt 12; $i++) { try { $c = New-Object System.Net.Sockets.TcpClient('localhost', 1636); $c.Close(); break } catch { Start-Sleep -Seconds 5 } }
+ & "$root\bat\ldapsearch.bat" --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
+ if ($LASTEXITCODE -ne 0) { throw "ldapsearch (as a service) failed: $LASTEXITCODE" }
+ net stop "OpenDJ Server"
+ if ($LASTEXITCODE -ne 0) { throw "net stop failed: $LASTEXITCODE" }
+ & "$root\bat\windows-service.bat" --disableService
+ if ($LASTEXITCODE -ne 0) { throw "--disableService failed: $LASTEXITCODE" }
+ if (Get-Service -DisplayName "OpenDJ Server" -ErrorAction SilentlyContinue) { throw "--disableService left the service registered" }
+ - name: uninstall.bat disables the service it finds and removes the instance
+ shell: pwsh
+ run: |
+ # Re-enable the service so the uninstaller exercises its disable path: nothing
+ # in the package owns the service, so removing it is the uninstaller's job.
+ $root = $env:OPENDJ_ROOT
+ & "$root\bat\windows-service.bat" --enableService
+ if ($LASTEXITCODE -ne 0) { throw "--enableService failed: $LASTEXITCODE" }
+ & "$root\uninstall.bat" --cli --remove-all --no-prompt --forceOnError --quiet
+ if ($LASTEXITCODE -ne 0) { throw "uninstall.bat failed: $LASTEXITCODE" }
+ if (Test-Path "$root\config\config.ldif") { throw "uninstall.bat did not remove the instance files" }
+ if (Get-Service -DisplayName "OpenDJ Server" -ErrorAction SilentlyContinue) { throw "uninstall.bat left the service registered" }
+ Write-Host "uninstall.bat removed the instance and disabled the service"
+ exit 0
+ - name: Uninstall MSI
+ shell: pwsh
+ run: |
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall.log -Tail 80; throw "msiexec /x failed: $($p.ExitCode)" }
+ if (Test-Path "$env:OPENDJ_ROOT\lib\opendj_service.exe") { throw "msiexec /x left the payload behind" }
+ Write-Host "Uninstalled OK"
+
+ # Upgrade path: released 5.1.2 x86 MSI (wine-built, WiX3) -> this build's x64 MSI.
+ # Verifies the new installer detects the legacy Program Files (x86) install, keeps the
+ # instance data in place, stops the running service for the file replacement and leaves
+ # its registration alone, and that the upgraded server starts with the old data.
+ test-msi-upgrade:
+ needs: wait-msi-artifact
+ runs-on: 'windows-latest'
+ permissions:
+ contents: read
steps:
- name: Download artifacts
uses: actions/download-artifact@v8
@@ -928,41 +1193,516 @@
with:
java-version: '25'
distribution: 'zulu'
- - name: Install MSI (silent)
+ - name: Install released 5.1.2 MSI and configure an instance
+ shell: pwsh
+ run: |
+ $uri = "https://github.com/OpenIdentityPlatform/OpenDJ/releases/download/5.1.2/opendj-5.1.2.msi"
+ for ($i = 1; $i -le 5; $i++) {
+ try { Invoke-WebRequest -Uri $uri -OutFile opendj-5.1.2.msi; break }
+ catch { if ($i -eq 5) { throw }; Write-Host "download attempt $i failed, retrying"; Start-Sleep -Seconds (10 * $i) }
+ }
+ # 5.1.2 already contains the script-quoting fixes from #671 (the tag post-dates
+ # the merge), so install it into its own x86 default - spaces and parentheses
+ # included - to reproduce the real upgrade starting point.
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i opendj-5.1.2.msi /quiet /qn /norestart /l*v install-old.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-old.log -Tail 80; throw "msiexec /i (5.1.2) failed: $($p.ExitCode)" }
+ $root = "C:\Program Files (x86)\OpenDJ"
+ if (-not (Test-Path "$root\setup.bat")) { Get-Content install-old.log -Tail 80; throw "5.1.2 install root not found at $root" }
+ $env:OPENDJ_JAVA_ARGS = "-server -Xmx512m"
+ & "$root\setup.bat" -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --addBaseEntry --cli --acceptLicense --no-prompt --doNotStart
+ if ($LASTEXITCODE -ne 0) { throw "setup.bat (5.1.2) failed: $LASTEXITCODE" }
+ # Register the service the pre-MSI way, prove it works, and LEAVE IT RUNNING:
+ # the upgrade itself must stop it (StopServiceBeforeUpgrade runs elevated here)
+ # before CheckServiceStopped would otherwise refuse.
+ & "$root\bat\windows-service.bat" --enableService
+ if ($LASTEXITCODE -ne 0) { throw "windows-service --enableService failed: $LASTEXITCODE" }
+ net start "OpenDJ Server"
+ if ($LASTEXITCODE -ne 0) { throw "net start (5.1.2) failed: $LASTEXITCODE" }
+ - name: Upgrade with the newly built MSI (no OPENDJ - location auto-detected)
shell: pwsh
run: |
$msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
if (-not $msi) { throw "MSI not found in the windows-latest-11 artifact" }
- Write-Host "MSI: $msi"
- # No OPENDJ property: use the default install directory (a path with spaces),
- # which the server scripts must handle.
- $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v install.log"
- if ($p.ExitCode -ne 0) { Get-Content install.log -Tail 80; throw "msiexec /i failed: $($p.ExitCode)" }
- $root = @("C:\Program Files (x86)\OpenDJ","C:\Program Files\OpenDJ") | Where-Object { Test-Path "$_\setup.bat" } | Select-Object -First 1
- if (-not $root) { Get-Content install.log -Tail 80; throw "OpenDJ install root with setup.bat not found" }
- Write-Host "Installed to $root"
+ # The headline upgrade path: no OPENDJ property, the installer must find the
+ # legacy default directory on its own.
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v upgrade.log"
+ if ($p.ExitCode -ne 0) { Get-Content upgrade.log -Tail 120; throw "msiexec /i (upgrade) failed: $($p.ExitCode)" }
+ $root = "C:\Program Files (x86)\OpenDJ"
+ # New package files landed in the old directory, not the x64 default
+ if (-not (Test-Path "$root\setup.bat")) { throw "upgrade did not keep the old install dir" }
+ if (Test-Path "C:\Program Files\OpenDJ") { throw "upgrade unexpectedly installed into the x64 default dir" }
+ # Instance data survived
+ if (-not (Test-Path "$root\config\config.ldif")) { throw "instance data (config\config.ldif) lost by the upgrade" }
+ # The service registration is the administrator's, not the package's: the upgrade
+ # stopped it to free the jars, and must have left it registered and pointing at
+ # the same tree - the wrapper it names has just been replaced in place.
+ $svc = Get-Service -DisplayName "OpenDJ Server" -ErrorAction SilentlyContinue
+ if (-not $svc) { sc.exe query; throw "the upgrade unregistered the administrator's service" }
+ if ($svc.Status -ne "Stopped") { throw "the upgrade left the service $($svc.Status), expected Stopped" }
+ if (Get-Service OpenDJ -ErrorAction SilentlyContinue) { sc.exe query; throw "the package registered a service of its own" }
+ sc.exe qc "$($svc.Name)"
"OPENDJ_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append
- - name: Setup and start/stop the Windows service
+ - name: Run upgrade.bat and start the upgraded server through the service
shell: pwsh
run: |
$root = $env:OPENDJ_ROOT
$env:OPENDJ_JAVA_ARGS = "-server -Xmx512m"
- & "$root\setup.bat" -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --addBaseEntry --cli --acceptLicense --no-prompt --doNotStart
- if ($LASTEXITCODE -ne 0) { throw "setup.bat failed: $LASTEXITCODE" }
- & "$root\bat\windows-service.bat" --enableService
- if ($LASTEXITCODE -ne 0) { throw "windows-service --enableService failed: $LASTEXITCODE" }
+ & "$root\upgrade.bat" --no-prompt --acceptLicense --force
+ if ($LASTEXITCODE -ne 0) { throw "upgrade.bat failed: $LASTEXITCODE" }
+ # The service registered before the upgrade still drives the refreshed tree.
net start "OpenDJ Server"
- if ($LASTEXITCODE -ne 0) { throw "net start failed: $LASTEXITCODE" }
+ if ($LASTEXITCODE -ne 0) { throw "net start (upgraded) failed: $LASTEXITCODE" }
for ($i=0; $i -lt 12; $i++) { try { $c = New-Object System.Net.Sockets.TcpClient('localhost', 1636); $c.Close(); break } catch { Start-Sleep -Seconds 5 } }
+ # The pre-upgrade data must still be served
& "$root\bat\ldapsearch.bat" --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
- if ($LASTEXITCODE -ne 0) { throw "ldapsearch failed: $LASTEXITCODE" }
+ if ($LASTEXITCODE -ne 0) { throw "ldapsearch after upgrade failed: $LASTEXITCODE" }
net stop "OpenDJ Server"
- if ($LASTEXITCODE -ne 0) { throw "net stop failed: $LASTEXITCODE" }
+ if ($LASTEXITCODE -ne 0) { throw "net stop (upgraded) failed: $LASTEXITCODE" }
+ - name: Repair must leave the service registration alone
+ shell: pwsh
+ run: |
+ # Nothing in the package controls a service, so a repair must not disturb the
+ # registration the administrator made - neither the one this upgrade inherited
+ # nor one belonging to an unrelated instance.
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ $before = (Get-Service -DisplayName "OpenDJ Server").Name
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" REINSTALL=ALL REINSTALLMODE=vomus /quiet /qn /norestart /l*v repair.log"
+ if ($p.ExitCode -ne 0) { Get-Content repair.log -Tail 80; throw "repair failed: $($p.ExitCode)" }
+ $after = Get-Service -DisplayName "OpenDJ Server" -ErrorAction SilentlyContinue
+ if (-not $after) { throw "repair unregistered the service" }
+ if ($after.Name -ne $before) { throw "repair changed the service key name: $before -> $($after.Name)" }
+ Write-Host "Repair left the '$before' service in place"
+ - name: Disabling the service before uninstalling leaves no orphan
+ shell: pwsh
+ run: |
+ # msiexec /x removes the files it installed and nothing else - as the WiX3-era
+ # package did. Disabling the service is the administrator's step (the install
+ # guide says so, and uninstall.bat does it too); skipping it would leave an
+ # auto-start service pointing at a deleted tree.
+ $root = $env:OPENDJ_ROOT
& "$root\bat\windows-service.bat" --disableService
- - name: Uninstall MSI
+ if ($LASTEXITCODE -ne 0) { throw "--disableService failed: $LASTEXITCODE" }
+ if (Get-Service -DisplayName "OpenDJ Server" -ErrorAction SilentlyContinue) { throw "--disableService left the service registered" }
+ - name: Auto-detect the legacy default directory on a fresh install
shell: pwsh
run: |
$msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
- $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall.log"
- if ($p.ExitCode -ne 0) { Get-Content uninstall.log -Tail 80; throw "msiexec /x failed: $($p.ExitCode)" }
- Write-Host "Uninstalled OK"
+ # Clean up the previous scenario first.
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall1.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall1.log -Tail 80; throw "msiexec /x failed: $($p.ExitCode)" }
+ # An existing legacy default directory must be picked up when OPENDJ is not given.
+ New-Item -ItemType Directory -Force "C:\Program Files (x86)\OpenDJ" | Out-Null
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v install-autodetect.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-autodetect.log -Tail 80; throw "msiexec /i (autodetect) failed: $($p.ExitCode)" }
+ if (-not (Test-Path "C:\Program Files (x86)\OpenDJ\setup.bat")) { Get-Content install-autodetect.log -Tail 80; throw "installer did not auto-detect the legacy default dir" }
+ if (Test-Path "C:\Program Files\OpenDJ") { throw "installer used the x64 default dir despite an existing legacy dir" }
+ Write-Host "Legacy default directory auto-detected OK"
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall2.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall2.log -Tail 80; throw "msiexec /x (cleanup) failed: $($p.ExitCode)" }
+ - name: Registry install-location detection on a fresh install
+ shell: pwsh
+ run: |
+ # The InstallDir registry value must be picked up when OPENDJ is not given, and it
+ # must beat the legacy Program Files (x86) directory (explicit SetProperty order).
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ New-Item -ItemType Directory -Force "C:\opendj-registry" | Out-Null
+ New-Item -ItemType Directory -Force "C:\Program Files (x86)\OpenDJ" | Out-Null
+ New-Item -Path HKLM:\SOFTWARE\OpenDJ -Force | Out-Null
+ Set-ItemProperty -Path HKLM:\SOFTWARE\OpenDJ -Name InstallDir -Value 'C:\opendj-registry\'
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v install-registry.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-registry.log -Tail 80; throw "msiexec /i (registry detect) failed: $($p.ExitCode)" }
+ if (-not (Test-Path "C:\opendj-registry\setup.bat")) { Get-Content install-registry.log -Tail 80; throw "installer did not use the registry InstallDir" }
+ if (Test-Path "C:\Program Files (x86)\OpenDJ\setup.bat") { throw "legacy directory beat the registry InstallDir" }
+ Write-Host "Registry install location detected OK"
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall3.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall3.log -Tail 80; throw "msiexec /x (registry cleanup) failed: $($p.ExitCode)" }
+ - name: Silent upgrade from an undetectable directory must refuse with guidance
+ shell: pwsh
+ run: |
+ # A 5.1.x at a custom directory wrote no registry value: a /quiet upgrade
+ # without OPENDJ used to relocate to the default while RemoveExistingProducts
+ # emptied the old tree. The installer must refuse instead.
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ # No detection signals at all - the guard under test is the one that fires when
+ # none resolves, so clear every one of them here rather than rely on the
+ # preceding step's uninstall having removed the registry value.
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i opendj-5.1.2.msi /quiet /qn /norestart OPENDJ=C:\opendj-custom /l*v install-custom.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-custom.log -Tail 80; throw "msiexec /i (5.1.2 custom dir) failed: $($p.ExitCode)" }
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v upgrade-custom.log"
+ if ($p.ExitCode -eq 0) { Get-Content upgrade-custom.log -Tail 80; throw "upgrade without OPENDJ must refuse when the old location cannot be determined" }
+ # Match on the part of the message that states the condition, not on the
+ # instruction: the wording of the guidance has already been reworded once.
+ if (-not (Select-String -Path upgrade-custom.log -Pattern "location could not be determined" -Quiet)) { Get-Content upgrade-custom.log -Tail 60; throw "expected the explicit-OPENDJ guidance message in the log" }
+ if (-not (Test-Path "C:\opendj-custom\setup.bat")) { throw "the refused upgrade damaged the original install" }
+ Write-Host "Upgrade refused with guidance, original install untouched (exit $($p.ExitCode))"
+ # A named directory that is not the installation strands exactly as much data as
+ # naming none. With no registry value and no server in the legacy default, a typo
+ # used to satisfy this guard - the resolved directory is not the default - and the
+ # relocation guard had nothing to compare it against, so RemoveExistingProducts
+ # emptied C:\opendj-custom while the new tree landed one letter away.
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" OPENDJ=C:\opendj-custmo /quiet /qn /norestart /l*v upgrade-typo.log"
+ if ($p.ExitCode -ne 1603) { Get-Content upgrade-typo.log -Tail 120; throw "a named directory holding no server must be refused (expected 1603, got $($p.ExitCode))" }
+ if (-not (Select-String -Path upgrade-typo.log -Pattern "location could not be determined" -Quiet)) { Get-Content upgrade-typo.log -Tail 60; throw "expected the explicit-OPENDJ guidance message in the log" }
+ if (Test-Path "C:\opendj-custmo") { throw "the refused upgrade still created the mistyped directory" }
+ if (-not (Test-Path "C:\opendj-custom\setup.bat")) { throw "the refused upgrade damaged the original install" }
+ Write-Host "A mistyped target was refused, original install untouched (exit $($p.ExitCode))"
+ # ...and naming the directory makes the very same upgrade proceed. This is what
+ # a GUI administrator does by browsing to it in InstallDirDlg, which the refusal
+ # must leave reachable: it fires on the resolved directory, not on the absence
+ # of a command-line property.
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" OPENDJ=C:\opendj-custom /quiet /qn /norestart /l*v upgrade-custom-ok.log"
+ if ($p.ExitCode -ne 0) { Get-Content upgrade-custom-ok.log -Tail 120; throw "upgrade with an explicit OPENDJ must succeed: $($p.ExitCode)" }
+ if (-not (Test-Path "C:\opendj-custom\lib\opendj_service.exe")) { throw "the upgrade did not land in C:\opendj-custom" }
+ if (Test-Path "C:\Program Files\OpenDJ") { throw "the upgrade installed into the default directory as well" }
+ Write-Host "Upgrade into the named directory succeeded"
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall-custom.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall-custom.log -Tail 80; throw "msiexec /x (custom cleanup) failed: $($p.ExitCode)" }
+ Remove-Item -Recurse -Force "C:\opendj-custom" -ErrorAction SilentlyContinue
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ - name: An empty legacy directory must not be adopted during an upgrade
+ shell: pwsh
+ run: |
+ # NOT Installed holds during a major upgrade too, so the legacy-directory
+ # fallback used to fire on a leftover EMPTY Program Files (x86)\OpenDJ: the new
+ # tree would land there while RemoveExistingProducts emptied the real install
+ # somewhere else. During an upgrade the directory must prove it holds a server.
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i opendj-5.1.2.msi /quiet /qn /norestart OPENDJ=C:\opendj-old /l*v install-old-custom.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-old-custom.log -Tail 80; throw "msiexec /i (5.1.2 at C:\opendj-old) failed: $($p.ExitCode)" }
+ New-Item -ItemType Directory -Force "C:\Program Files (x86)\OpenDJ" | Out-Null
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v upgrade-emptylegacy.log"
+ if ($p.ExitCode -eq 0) { Get-Content upgrade-emptylegacy.log -Tail 120; throw "an upgrade with no determinable location must be refused, not routed to an empty legacy directory" }
+ # A non-zero exit code on its own only says msiexec failed; both neighbouring
+ # scenarios name the guard they are about, and so must this one.
+ if (-not (Select-String -Path upgrade-emptylegacy.log -Pattern "location could not be determined" -Quiet)) { Get-Content upgrade-emptylegacy.log -Tail 60; throw "expected the explicit-OPENDJ guidance message in the log" }
+ if (Test-Path "C:\Program Files (x86)\OpenDJ\setup.bat") { throw "the upgrade installed into the empty legacy directory" }
+ if (-not (Test-Path "C:\opendj-old\setup.bat")) { throw "the refused upgrade damaged the original install" }
+ Write-Host "Empty legacy directory not adopted, upgrade refused (exit $($p.ExitCode))"
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x opendj-5.1.2.msi /quiet /qn /norestart /l*v uninstall-old-custom.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall-old-custom.log -Tail 80; throw "msiexec /x (5.1.2 cleanup) failed: $($p.ExitCode)" }
+ Remove-Item -Recurse -Force "C:\opendj-old" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ - name: An upgrade that would relocate the installation must refuse
+ shell: pwsh
+ run: |
+ # Passing a different OPENDJ over a detected installation is not a move:
+ # RemoveExistingProducts would empty the old tree while the new one is installed
+ # elsewhere, stranding config/db/logs (and any service registration) behind.
+ #
+ # The older package has to be a different ProductCode for this to be an upgrade
+ # at all: reinstalling this very MSI over itself is maintenance mode, where
+ # FindRelatedProducts does not run, so WIX_UPGRADE_DETECTED would never be set
+ # and the guard could not fire. Hence the released 5.1.2 package, installed at
+ # its native legacy default so both branches of the guard have something to
+ # compare against.
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ # Clear every location this scenario reasons about itself, rather than inheriting
+ # the previous step's teardown: the starting state is what the guard is judged
+ # against, so it belongs in the step that makes the judgement.
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\opendj-b" -ErrorAction SilentlyContinue
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i opendj-5.1.2.msi /quiet /qn /norestart /l*v install-old-legacy.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-old-legacy.log -Tail 80; throw "msiexec /i (5.1.2) failed: $($p.ExitCode)" }
+ if (-not (Test-Path "C:\Program Files (x86)\OpenDJ\setup.bat")) { throw "5.1.2 did not install into the legacy default" }
+ # (a) the recorded-location branch: a host that came through a 5.2.0-or-later
+ # package has its install directory in the registry.
+ New-Item -Path HKLM:\SOFTWARE\OpenDJ -Force | Out-Null
+ Set-ItemProperty -Path HKLM:\SOFTWARE\OpenDJ -Name InstallDir -Value 'C:\Program Files (x86)\OpenDJ\'
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" OPENDJ=C:\opendj-b /quiet /qn /norestart /l*v relocate-reg.log"
+ if ($p.ExitCode -eq 0) { Get-Content relocate-reg.log -Tail 120; throw "a relocating upgrade must be refused (recorded location)" }
+ if (-not (Select-String -Path relocate-reg.log -Pattern "cannot move an existing installation" -Quiet)) { Get-Content relocate-reg.log -Tail 60; throw "expected the relocation guidance message in the log" }
+ if (Test-Path "C:\opendj-b") { throw "the refused relocation still created C:\opendj-b" }
+ # (b) the legacy-directory branch: no registry value, the old install proven by
+ # the setup.bat in the legacy default.
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" OPENDJ=C:\opendj-b /quiet /qn /norestart /l*v relocate-legacy.log"
+ if ($p.ExitCode -eq 0) { Get-Content relocate-legacy.log -Tail 120; throw "a relocating upgrade must be refused (legacy directory)" }
+ if (-not (Select-String -Path relocate-legacy.log -Pattern "cannot move an existing installation" -Quiet)) { Get-Content relocate-legacy.log -Tail 60; throw "expected the relocation guidance message in the log" }
+ if (Test-Path "C:\opendj-b") { throw "the refused relocation still created C:\opendj-b" }
+ if (-not (Test-Path "C:\Program Files (x86)\OpenDJ\setup.bat")) { throw "the refused relocation damaged the original install" }
+ Write-Host "Relocating upgrade refused on both branches, original install untouched"
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x opendj-5.1.2.msi /quiet /qn /norestart /l*v uninstall-old-legacy.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall-old-legacy.log -Tail 80; throw "msiexec /x (5.1.2 cleanup) failed: $($p.ExitCode)" }
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ - name: A stray OpenDJ tree in the default directory must not be adopted
+ shell: pwsh
+ run: |
+ # The product being upgraded is a 5.1.x in a custom directory, which recorded
+ # nothing, while some unrelated OpenDJ tree - a zip install, a copy - sits in the
+ # x64 default. A setup.bat existence test cannot tell the two apart, so a guard
+ # keyed on it stood down: RemoveExistingProducts gutted the real installation
+ # while InstallFiles landed on the stranger, and msiexec exited 0.
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files\OpenDJ" -ErrorAction SilentlyContinue
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i opendj-5.1.2.msi /quiet /qn /norestart OPENDJ=C:\opendj-real /l*v install-old-real.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-old-real.log -Tail 80; throw "msiexec /i (5.1.2 at C:\opendj-real) failed: $($p.ExitCode)" }
+ # The decoy: everything the installer is able to ask about a directory.
+ New-Item -ItemType Directory -Force "C:\Program Files\OpenDJ\lib" | Out-Null
+ Set-Content "C:\Program Files\OpenDJ\setup.bat" '@echo off'
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v upgrade-decoy.log"
+ if ($p.ExitCode -eq 0) { Get-Content upgrade-decoy.log -Tail 120; throw "an upgrade must not adopt a stray tree in the default directory" }
+ if (-not (Select-String -Path upgrade-decoy.log -Pattern "location could not be determined" -Quiet)) { Get-Content upgrade-decoy.log -Tail 60; throw "expected the explicit-OPENDJ guidance message in the log" }
+ if (Test-Path "C:\Program Files\OpenDJ\lib\opendj_service.exe") { throw "the refused upgrade installed into the stray tree" }
+ if (-not (Test-Path "C:\opendj-real\setup.bat")) { throw "the refused upgrade damaged the original install" }
+ # ...and naming the real directory gets the administrator through, decoy or not.
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" OPENDJ=C:\opendj-real /quiet /qn /norestart /l*v upgrade-decoy-ok.log"
+ if ($p.ExitCode -ne 0) { Get-Content upgrade-decoy-ok.log -Tail 120; throw "upgrade with an explicit OPENDJ must succeed: $($p.ExitCode)" }
+ if (-not (Test-Path "C:\opendj-real\lib\opendj_service.exe")) { throw "the upgrade did not land in C:\opendj-real" }
+ if (Test-Path "C:\Program Files\OpenDJ\lib\opendj_service.exe") { throw "the upgrade also installed into the stray tree" }
+ Write-Host "Stray default-directory tree ignored: refused, then upgraded where told"
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall-real.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall-real.log -Tail 80; throw "msiexec /x (decoy cleanup) failed: $($p.ExitCode)" }
+ Remove-Item -Recurse -Force "C:\opendj-real" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ - name: An old server in the default directory upgrades once it is named
+ shell: pwsh
+ run: |
+ # The price of the scenario above: a 5.1.x that really does live in the x64
+ # default recorded nothing either, so the package cannot tell it from the decoy
+ # and refuses the silent upgrade that would have gone through before. What it
+ # must not do is dead-end - the directory is a configurable property, and naming
+ # it (which is also what browsing to it in the wizard amounts to) has to work.
+ #
+ # The old tree gets there by hand rather than through OPENDJ=: the released
+ # 5.1.x package is x86, and a 32-bit package cannot install into the 64-bit
+ # Program Files at all - Windows Installer resolves its [ProgramFilesFolder] to
+ # Program Files (x86) whatever the directory property says, so msiexec exits 0
+ # while the files land in the legacy default, which is a different scenario (one
+ # the legacy-directory search resolves on its own). Installing into a custom
+ # directory and moving the tree leaves exactly what this one needs: an
+ # upgradable 5.1.x registration, no recorded location, no legacy directory, and
+ # a real old server sitting in C:\Program Files\OpenDJ. That the registration is
+ # left pointing at the directory the move emptied costs nothing - no guard reads
+ # it, and RemoveExistingProducts tolerates the files being gone.
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\opendj-x64src" -ErrorAction SilentlyContinue
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i opendj-5.1.2.msi /quiet /qn /norestart OPENDJ=C:\opendj-x64src /l*v install-old-x64.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-old-x64.log -Tail 80; throw "msiexec /i (5.1.2 at C:\opendj-x64src) failed: $($p.ExitCode)" }
+ # Exit code 0 says msiexec ran, not that it put the files where it was told, and
+ # a directory it silently declined to use is worth naming in the failure.
+ if (-not (Test-Path "C:\opendj-x64src\setup.bat")) { Get-ChildItem "C:\Program Files\OpenDJ","C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue | Select-Object -First 5 -ExpandProperty FullName; Get-Content install-old-x64.log -Tail 80; throw "5.1.2 did not install into C:\opendj-x64src" }
+ Move-Item "C:\opendj-x64src" "C:\Program Files\OpenDJ"
+ if (-not (Test-Path "C:\Program Files\OpenDJ\setup.bat")) { throw "the 5.1.2 tree did not move into C:\Program Files\OpenDJ" }
+ # 5.1.x ships lib\opendj_service.exe itself, so the file cannot say whose tree
+ # this is; its content can. Both halves below are judged on the two things only
+ # the new package produces: this payload, and the InstallDir registry value.
+ $oldWrapper = (Get-FileHash "C:\Program Files\OpenDJ\lib\opendj_service.exe").Hash
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v upgrade-x64-silent.log"
+ if ($p.ExitCode -eq 0) { Get-Content upgrade-x64-silent.log -Tail 120; throw "a silent upgrade with nothing recording the location must refuse" }
+ if (-not (Select-String -Path upgrade-x64-silent.log -Pattern "location could not be determined" -Quiet)) { Get-Content upgrade-x64-silent.log -Tail 60; throw "expected the explicit-OPENDJ guidance message in the log" }
+ if (Test-Path HKLM:\SOFTWARE\OpenDJ) { throw "the refused upgrade registered an install location" }
+ if ((Get-FileHash "C:\Program Files\OpenDJ\lib\opendj_service.exe").Hash -ne $oldWrapper) { throw "the refused upgrade overwrote the old server" }
+ # The signal that says "this directory was named" has to be public - a private
+ # property set in the UI sequence never reaches the installer service, where the
+ # guards run - so it must not be usable as a switch. It holds the named path and
+ # the guard requires the resolved directory to START WITH it: a flag-shaped value
+ # disarms nothing, and a value that does pass has spelled out the directory, which
+ # is naming it. It is not the only conjunct either - the target still has to hold a
+ # server - so even a matching prefix cannot stand in for that evidence.
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" OPENDJ_GIVEN=1 /quiet /qn /norestart /l*v upgrade-x64-switch.log"
+ if ($p.ExitCode -ne 1603) { Get-Content upgrade-x64-switch.log -Tail 120; throw "OPENDJ_GIVEN=1 must not switch the guard off (expected 1603, got $($p.ExitCode))" }
+ if (-not (Select-String -Path upgrade-x64-switch.log -Pattern "location could not be determined" -Quiet)) { Get-Content upgrade-x64-switch.log -Tail 60; throw "the refusal must still come from the same guard" }
+ if ((Get-FileHash "C:\Program Files\OpenDJ\lib\opendj_service.exe").Hash -ne $oldWrapper) { throw "the upgrade that OPENDJ_GIVEN=1 let through overwrote the old server" }
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" OPENDJ=`"C:\Program Files\OpenDJ`" /quiet /qn /norestart /l*v upgrade-x64-named.log"
+ if ($p.ExitCode -ne 0) { Get-Content upgrade-x64-named.log -Tail 120; throw "the named upgrade into the default directory must succeed: $($p.ExitCode)" }
+ if ((Get-FileHash "C:\Program Files\OpenDJ\lib\opendj_service.exe").Hash -eq $oldWrapper) { Get-Content upgrade-x64-named.log -Tail 120; throw "the upgrade did not land in C:\Program Files\OpenDJ" }
+ # The only OPENDJ in this workflow whose value carries spaces: what the package
+ # recorded proves it survived the command line and the elevation intact.
+ $recorded = (Get-ItemProperty -Path HKLM:\SOFTWARE\OpenDJ -Name InstallDir -ErrorAction SilentlyContinue).InstallDir
+ if ($recorded -notlike "C:\Program Files\OpenDJ*") { throw "the upgrade recorded '$recorded', not the directory it was told to use" }
+ Write-Host "Default-directory upgrade refused silently, accepted when named"
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall-x64.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall-x64.log -Tail 80; throw "msiexec /x (x64 default cleanup) failed: $($p.ExitCode)" }
+ Remove-Item -Recurse -Force "C:\Program Files\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\opendj-x64src" -ErrorAction SilentlyContinue
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ - name: Fresh install must not touch a service registered by another instance
+ shell: pwsh
+ run: |
+ # A leftover Program Files (x86)\OpenDJ plus an "OpenDJ Server" belonging to a
+ # zip instance elsewhere: installing to a third directory must leave that
+ # registration completely alone. The package controls no service at all, so this
+ # holds by construction - the scenario guards against reintroducing one.
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ New-Item -ItemType Directory -Force "C:\Program Files (x86)\OpenDJ" | Out-Null
+ sc.exe create "OpenDJ Server" binPath= "C:\zip-instance\lib\opendj_service.exe start ""C:\zip-instance.""" start= demand
+ if ($LASTEXITCODE -ne 0) { throw "sc create failed: $LASTEXITCODE" }
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" OPENDJ=C:\opendj-elsewhere /quiet /qn /norestart /l*v install-elsewhere.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-elsewhere.log -Tail 80; throw "msiexec /i (elsewhere) failed: $($p.ExitCode)" }
+ if (-not (Test-Path "C:\opendj-elsewhere\setup.bat")) { throw "install did not land in C:\opendj-elsewhere" }
+ if (-not (Get-Service "OpenDJ Server" -ErrorAction SilentlyContinue)) { throw "fresh install elsewhere deleted an unrelated instance's service" }
+ sc.exe delete "OpenDJ Server"
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall-elsewhere.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall-elsewhere.log -Tail 80; throw "msiexec /x (elsewhere cleanup) failed: $($p.ExitCode)" }
+ Write-Host "Fresh install elsewhere left the unrelated 'OpenDJ Server' service in place"
+ - name: An upgrade must refuse while the service is still starting
+ shell: pwsh
+ run: |
+ # The SCM takes no controls in a pending state: StopServiceBeforeUpgrade's
+ # 'net stop' fails instantly with ERROR_SERVICE_CANNOT_ACCEPT_CTRL and
+ # Return="ignore" eats it. StartPending is not 'Running', so a check that
+ # sampled the state once waved the upgrade through with a JVM coming up on the
+ # tree being replaced - and the jars are unversioned, so the delete-on-reboot
+ # entries left behind by the nested uninstall name the paths the NEW jars
+ # occupy. Reproducible because the wrapper reports START_PENDING for as long as
+ # bat\start-ds.bat runs, which service.c gives 300 s.
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files\OpenDJ" -ErrorAction SilentlyContinue
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i opendj-5.1.2.msi /quiet /qn /norestart /l*v install-old-pending.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-old-pending.log -Tail 80; throw "msiexec /i (5.1.2) failed: $($p.ExitCode)" }
+ $root = "C:\Program Files (x86)\OpenDJ"
+ $env:OPENDJ_JAVA_ARGS = "-server -Xmx512m"
+ & "$root\setup.bat" -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --addBaseEntry --cli --acceptLicense --no-prompt --doNotStart
+ if ($LASTEXITCODE -ne 0) { throw "setup.bat (5.1.2) failed: $LASTEXITCODE" }
+ & "$root\bat\windows-service.bat" --enableService
+ if ($LASTEXITCODE -ne 0) { throw "windows-service --enableService failed: $LASTEXITCODE" }
+ # Hold the start open: the wrapper waits for this script, reporting START_PENDING
+ # the whole time. No JVM is needed - the guard is being asked about a service
+ # state, not about a lock.
+ Copy-Item "$root\bat\start-ds.bat" "$root\bat\start-ds.bat.orig"
+ Set-Content "$root\bat\start-ds.bat" "@echo off`r`nping -n 240 127.0.0.1 >nul"
+ sc.exe start "OpenDJ Server" | Out-Null
+ for ($i = 0; $i -lt 15; $i++) {
+ $st = (Get-Service "OpenDJ Server" -ErrorAction SilentlyContinue).Status
+ if ($st -eq 'StartPending') { break }
+ Start-Sleep -Seconds 1
+ }
+ $st = (Get-Service "OpenDJ Server" -ErrorAction SilentlyContinue).Status
+ if ($st -ne 'StartPending') { throw "expected the service to be StartPending, got '$st'" }
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v upgrade-pending.log"
+ # A refusal from a Return="check" custom action is 1722 in the log and 1603 out of
+ # msiexec, and nothing else. "-ne 0" would also accept 3010 - which is the FAIL-OPEN
+ # outcome, the upgrade going through and leaving the files it could not replace to a
+ # reboot - so the exact code is what gets asserted. Same reasoning for the log: the
+ # action NAME appears whether it ran and passed, ran and failed, or was skipped by
+ # its condition, so the return value has to be part of the pattern.
+ if ($p.ExitCode -ne 1603) { Get-Content upgrade-pending.log -Tail 120; throw "the upgrade must refuse while the service is starting (expected 1603, got $($p.ExitCode))" }
+ if (-not (Select-String -Path upgrade-pending.log -Pattern "CheckServiceStopped\. Return value 3" -Quiet)) { Get-Content upgrade-pending.log -Tail 60; throw "the refusal must come from CheckServiceStopped" }
+ if (-not (Test-Path "$root\config\config.ldif")) { throw "the refused upgrade damaged the instance" }
+ if (-not (Test-Path "$root\setup.bat")) { throw "the refused upgrade damaged the installation" }
+ Write-Host "Upgrade refused while the service was StartPending (exit $($p.ExitCode))"
+ # Teardown: the wrapper is still sitting on the held-open start.
+ Stop-Process -Name opendj_service -Force -ErrorAction SilentlyContinue
+ Get-Process -Name PING -ErrorAction SilentlyContinue | Stop-Process -Force
+ Start-Sleep -Seconds 5
+ Move-Item -Force "$root\bat\start-ds.bat.orig" "$root\bat\start-ds.bat"
+ & "$root\bat\windows-service.bat" --disableService
+ if (Get-Service "OpenDJ Server" -ErrorAction SilentlyContinue) { sc.exe delete "OpenDJ Server" }
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x opendj-5.1.2.msi /quiet /qn /norestart /l*v uninstall-pending.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall-pending.log -Tail 80; throw "msiexec /x (5.1.2 cleanup) failed: $($p.ExitCode)" }
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ - name: A decoy in the legacy default must not block the documented workaround
+ shell: pwsh
+ run: |
+ # The mirror of "a stray OpenDJ tree in the default directory must not be adopted",
+ # with the stray tree in the LEGACY default instead - where the install guide says
+ # to pass OPENDJ, and where the relocation guard used to refuse that very command:
+ # the legacy directory holds A server, the named directory is not it, refuse. The
+ # installation then had no upgrade path at all, silent or named.
+ # Both halves are asserted here, because the exception that fixes it is narrow: the
+ # named directory has to hold a server. Naming an empty one is still a relocation.
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\opendj-b" -ErrorAction SilentlyContinue
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i opendj-5.1.2.msi /quiet /qn /norestart OPENDJ=C:\opendj-mine /l*v install-old-mine.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-old-mine.log -Tail 80; throw "msiexec /i (5.1.2 at C:\opendj-mine) failed: $($p.ExitCode)" }
+ if (-not (Test-Path "C:\opendj-mine\setup.bat")) { Get-Content install-old-mine.log -Tail 80; throw "5.1.2 did not install into C:\opendj-mine" }
+ # The decoy: a zip installation, a copy, a decommissioned instance - anything a
+ # setup.bat search cannot tell from the product being upgraded.
+ New-Item -ItemType Directory -Force "C:\Program Files (x86)\OpenDJ\lib" | Out-Null
+ Set-Content "C:\Program Files (x86)\OpenDJ\setup.bat" '@echo off'
+ # Naming a directory that holds no server is still a relocation, decoy or not.
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" OPENDJ=C:\opendj-b /quiet /qn /norestart /l*v relocate-decoy.log"
+ if ($p.ExitCode -eq 0) { Get-Content relocate-decoy.log -Tail 120; throw "naming an empty directory is a relocation and must be refused" }
+ if (-not (Select-String -Path relocate-decoy.log -Pattern "cannot move an existing installation" -Quiet)) { Get-Content relocate-decoy.log -Tail 60; throw "expected the relocation guidance message in the log" }
+ if (Test-Path "C:\opendj-b") { throw "the refused relocation still created C:\opendj-b" }
+ # ...and naming the real one is the workaround the install guide prescribes.
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" OPENDJ=C:\opendj-mine /quiet /qn /norestart /l*v upgrade-mine.log"
+ if ($p.ExitCode -ne 0) { Get-Content upgrade-mine.log -Tail 120; throw "the documented workaround must upgrade the named installation: $($p.ExitCode)" }
+ if (-not (Test-Path "C:\opendj-mine\lib\opendj_service.exe")) { throw "the upgrade did not land in C:\opendj-mine" }
+ if (Test-Path "C:\Program Files (x86)\OpenDJ\lib\opendj_service.exe") { throw "the upgrade also installed into the decoy" }
+ Write-Host "Legacy-default decoy: empty target refused, named installation upgraded"
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall-mine.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall-mine.log -Tail 80; throw "msiexec /x (decoy cleanup) failed: $($p.ExitCode)" }
+ Remove-Item -Recurse -Force "C:\opendj-mine" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ - name: An upgrade must refuse while a server runs without a service
+ shell: pwsh
+ run: |
+ # The mode this package ships by default: setup registers no service, so the
+ # server started by bat\start-ds.bat is a plain JVM holding lib\*.jar. There is no
+ # service key for the ImagePath-gated pair to match, and Restart Manager is not
+ # allowed to shut anything down, so CheckServerNotRunning - the byte-range lock on
+ # locks\server.lock - is the only thing standing between a running server and
+ # RemoveExistingProducts renaming its jars into delete-on-reboot entries.
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force "C:\Program Files\OpenDJ" -ErrorAction SilentlyContinue
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i opendj-5.1.2.msi /quiet /qn /norestart /l*v install-old-running.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-old-running.log -Tail 80; throw "msiexec /i (5.1.2) failed: $($p.ExitCode)" }
+ $root = "C:\Program Files (x86)\OpenDJ"
+ $env:OPENDJ_JAVA_ARGS = "-server -Xmx512m"
+ & "$root\setup.bat" -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --addBaseEntry --cli --acceptLicense --no-prompt --doNotStart
+ if ($LASTEXITCODE -ne 0) { throw "setup.bat (5.1.2) failed: $LASTEXITCODE" }
+ & "$root\bat\start-ds.bat"
+ if ($LASTEXITCODE -ne 0) { throw "start-ds.bat failed: $LASTEXITCODE" }
+ for ($i=0; $i -lt 12; $i++) { try { $c = New-Object System.Net.Sockets.TcpClient('localhost', 1636); $c.Close(); break } catch { Start-Sleep -Seconds 5 } }
+ if (Get-Service "OpenDJ Server" -ErrorAction SilentlyContinue) { throw "this scenario is about a server with NO service registered" }
+ # The headline auto-detected upgrade, which would otherwise proceed straight into
+ # the running server's tree. The refusal costs the full 60 s grace inside the
+ # check: a server that is genuinely up never releases the lock.
+ $pendingKey = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager"
+ $pendingBefore = @((Get-ItemProperty -Path $pendingKey -Name PendingFileRenameOperations -ErrorAction SilentlyContinue).PendingFileRenameOperations)
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v upgrade-running.log"
+ # Exactly 1603 (custom action 1722), for the reason spelled out in the StartPending
+ # scenario above: 3010 is what a guard that fails open produces here, and "-ne 0"
+ # cannot tell the two apart. The action name alone cannot either - it is written to
+ # the log whether the action refused or waved the upgrade through.
+ if ($p.ExitCode -ne 1603) { Get-Content upgrade-running.log -Tail 120; throw "the upgrade must refuse while a server is running out of the tree (expected 1603, got $($p.ExitCode))" }
+ if (-not (Select-String -Path upgrade-running.log -Pattern "CheckServerNotRunning\. Return value 3" -Quiet)) { Get-Content upgrade-running.log -Tail 60; throw "the refusal must come from CheckServerNotRunning" }
+ # The signature of the fail-open, and the damage it does: RemoveExistingProducts
+ # cannot rename a jar the JVM holds, so it leaves a delete-on-reboot entry naming
+ # the path the new jar occupies. A refusal leaves none.
+ $pendingAfter = @((Get-ItemProperty -Path $pendingKey -Name PendingFileRenameOperations -ErrorAction SilentlyContinue).PendingFileRenameOperations)
+ $pendingNew = $pendingAfter | Where-Object { $_ -and $pendingBefore -notcontains $_ }
+ if ($pendingNew) { throw "the refused upgrade still scheduled files for delete-on-reboot: $($pendingNew -join '; ')" }
+ if (-not (Test-Path "$root\config\config.ldif")) { throw "the refused upgrade damaged the instance" }
+ & "$root\bat\ldapsearch.bat" --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
+ if ($LASTEXITCODE -ne 0) { throw "the refused upgrade disturbed the running server" }
+ Write-Host "Upgrade refused while a non-service server was running (exit $($p.ExitCode))"
+ # Stopping it makes the very same upgrade proceed - and the 60 s grace inside the
+ # check is what absorbs the gap between stop-ds returning and the JVM releasing
+ # the lock, so no wait is needed here to keep this half honest.
+ & "$root\bat\stop-ds.bat"
+ if ($LASTEXITCODE -ne 0) { throw "stop-ds.bat failed: $LASTEXITCODE" }
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v upgrade-stopped.log"
+ if ($p.ExitCode -ne 0) { Get-Content upgrade-stopped.log -Tail 120; throw "the upgrade must proceed once the server is stopped: $($p.ExitCode)" }
+ if (-not (Test-Path "$root\lib\opendj_service.exe")) { throw "the upgrade did not land in $root" }
+ if (-not (Test-Path "$root\config\config.ldif")) { throw "the upgrade lost the instance data" }
+ Write-Host "The same upgrade proceeded once the server was stopped"
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall-running.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall-running.log -Tail 80; throw "msiexec /x (running-server cleanup) failed: $($p.ExitCode)" }
+ Remove-Item -Recurse -Force "C:\Program Files (x86)\OpenDJ" -ErrorAction SilentlyContinue
+ Remove-Item -Path HKLM:\SOFTWARE\OpenDJ -Recurse -Force -ErrorAction SilentlyContinue
--
Gitblit v1.10.0