.gitattributes
@@ -1,3 +1,8 @@ # WiX sources must stay LF so the WiX toolset parses them consistently across runners. *.wxs text eol=lf # The checked-in Windows launcher/service binaries are actively maintained: never diff, # merge or eol-convert them. *.exe binary # Keep HTML checked out with LF on all platforms so javadoc doclint # (JDK 25/26) does not treat CR (from CRLF) as part of a multi-line tag name. *.html text eol=lf .github/scripts/wait-server-stopped.ps1
New file @@ -0,0 +1,42 @@ # The contents of this file are subject to the terms of the Common Development and # Distribution License (the License). You may not use this file except in compliance with the # License. # # You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the # specific language governing permission and limitations under the License. # # When distributing Covered Software, include this CDDL Header Notice in each file and include # the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL # Header, with the fields enclosed by brackets [] replaced by your own identifying # information: "Portions copyright [year] [name of copyright owner]". # # Copyright 2026 3A Systems, LLC. # Verify a stop took effect before moving on: wait until the server releases the exclusive # byte-range lock it holds on locks\server.lock. Checking the exit code of stop-ds is not a # substitute - #768 was exactly the case where winlauncher.exe reported success without # having stopped the server - and starting the service on a lock the old JVM still holds # fails in ways that look like flakiness. # # 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. # # Dot-source this file to use it: . .github\scripts\wait-server-stopped.ps1 function Wait-ServerStopped($lockFile) { # Callers pass either a workspace-relative path (the zip build) or an absolute one (an # installed tree), so only resolve the relative ones. if (-not [System.IO.Path]::IsPathRooted($lockFile)) { $lockFile = Join-Path $PWD $lockFile } for ($i = 0; $i -lt 30; $i++) { if (-not (Test-Path $lockFile)) { return } # IOException only - that is what both a held byte-range lock and a sharing # violation raise. A blanket catch would also swallow UnauthorizedAccessException, # spin out the full minute on a permissions problem under Program Files and then # report a lock that was never held; let anything else surface with its own message. try { $fs = [System.IO.File]::Open($lockFile, 'Open', 'ReadWrite', 'ReadWrite') try { $fs.Lock(0, 1); $fs.Unlock(0, 1); return } finally { $fs.Close() } } catch [System.IO.IOException] { Start-Sleep -Seconds 2 } } throw "The server still holds the lock on ${lockFile}: the stop did not take effect" } .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 .github/workflows/deploy.yml
@@ -26,36 +26,155 @@ # contents: write is required to push the generated documentation to the project wiki # with github.token. The doc site push uses a separate PAT, not this token. # actions: read is required to download the MSI artifact from the triggering Build run # (a permissions block sets every unlisted scope to none). permissions: contents: write actions: read jobs: package-deploy-maven: if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event=='push'}} # head_repository states the trust boundary instead of leaving it to be re-derived. # The checkout below takes its ref from the triggering run, and the branches filter # above matches that run's head branch NAME - which a fork can also call master. What # actually 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, # never ours. The repository check makes that explicit for the next reader, and for # the next person tempted to relax the event condition. if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_repository.full_name == github.repository }} runs-on: 'ubuntu-latest' steps: - name: Print github context env: GITHUB_CONTEXT: ${{ toJSON(github) }} run: echo "$GITHUB_CONTEXT" - 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 submodules: recursive ref: ${{ github.event.workflow_run.head_branch }} # The committed opendj-server-legacy/lib/*.exe are what every Linux-built server zip # ships - the snapshots this job publishes, and later the tagged releases and their # Maven Central artifacts - while only a Windows job can rebuild them. Nothing used # to make the two meet, so a native source change that was never re-committed as a # refreshed binary shipped the old wrapper while CI stayed green (master carried such # a gap for weeks). The triggering Build run compiled them from source already, so # take its binaries and commit them here rather than rebuild. # # Here rather than in build.yml: this workflow already holds contents: write for the # wiki push, so build-maven - which runs the whole Maven plugin tree - stays # read-only, and it only runs at all once the Build succeeded on a push to a release # branch. The cost is latency: the refresh lands after the full matrix, not minutes # into it. Committing before the Maven steps below also means the snapshot zip this # job publishes carries the fresh launchers. # # This only works because the Makefile passes /Brepro to both cl and link: the output # is a function of the sources, not of the build time. Without it every run would # produce different bytes and this would commit on every push. An MSVC toolchain bump # on the runner image does change them, and that refresh commit is correct - the # committed binary then matches what CI verifies. Pushes made with GITHUB_TOKEN do # not start new workflow runs, so this cannot loop; a PAT would break that. - name: Download the launchers built by the triggering Build run continue-on-error: true uses: actions/download-artifact@v8 with: name: windows-exe-11 run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} path: ${{ runner.temp }}/windows-exe - name: Commit the rebuilt launchers shell: bash env: # NOT github.ref: on a workflow_run event that is the default branch, not the # branch the triggering run was for. BRANCH: ${{ github.event.workflow_run.head_branch }} BUILT: ${{ runner.temp }}/windows-exe HEAD_SHA: ${{ github.event.workflow_run.head_sha }} RUN_ID: ${{ github.event.workflow_run.id }} run: | set -e if ! ls "$BUILT"/*.exe >/dev/null 2>&1; then echo "::warning title=No launcher binaries from the Build run::windows-exe-11 could not be downloaded, leaving opendj-server-legacy/lib/*.exe as committed." exit 0 fi cp "$BUILT"/*.exe opendj-server-legacy/lib/ # status --porcelain, not diff: it reports a brand-new launcher that was never # git-added just as well as a modified one. if [ -z "$(git status --porcelain -- opendj-server-legacy/lib)" ]; then echo "Committed launchers already match the sources." exit 0 fi git status --porcelain -- opendj-server-legacy/lib git config user.name "Open Identity Platform Community" git config user.email "open-identity-platform-opendj@googlegroups.com" git add -- opendj-server-legacy/lib git commit --quiet \ -m "Refresh the Windows native launchers" \ -m "Rebuilt from opendj-server-legacy/src/build-tools/windows for ${HEAD_SHA} by the Build workflow (run ${RUN_ID})." # The checkout is of the branch, which may have moved on since the Build run, and # it can move again while we push: rebase onto the current tip and retry. An # identical refresh already there leaves an empty commit that rebase drops, and # the push then has nothing to send. # # This step runs before the Maven deploy, the package uploads and the wiki push, # so it must not be the thing that costs them: a refresh that cannot be landed # warns and lets the job carry on. The next push to this branch retries it, and # nothing downstream depends on the committed binaries being current - the Build # run that produced them compiled its own. # # The Maven steps below must build the tree the triggering Build validated. The # rebase moves the worktree onto the branch tip, which can carry commits that # Build run never saw, so remember the refresh as it was made - the tree as # checked out, plus the launchers - and come back to it however this step ends. # (The checkout above takes the branch by NAME, not the triggering run's SHA, so # that tree is the tip as of a moment ago rather than HEAD_SHA itself; what this # keeps is the rebase from widening the gap.) REFRESHED=$(git rev-parse HEAD) # A trap rather than a line on the push-success path: all four ways out of the # loop below - fetch failure, rebase conflict, three lost races, and the push # that lands - can be taken after a rebase has already moved the worktree, and # 'git rebase --abort' returns to the state that rebase started from, which on # attempt 2 or 3 is the result of the previous attempt rather than $REFRESHED. # # --force matters: when the abort above fails - it is masked by '|| true' - a # plain 'checkout --detach' stops on "you need to resolve your current index # first" and leaves the worktree mid-rebase for the Maven steps. trap 'git checkout --quiet --force --detach "$REFRESHED" || echo "::warning title=Could not restore the validated tree::the build continues on the current $BRANCH tip."' EXIT for attempt in 1 2 3; do # Guarded like everything else in this block: bare, it is the one command left # that could still take the job down with it. The step runs under set -e with # no continue-on-error, so a transient fetch failure would skip the Maven # deploy, all nine artifact uploads, the MSI attachment and both documentation # pushes over a refresh that is allowed to fail. if ! git fetch --quiet origin "$BRANCH"; then echo "::warning title=Could not refresh the launcher binaries::$BRANCH could not be fetched. Refresh opendj-server-legacy/lib/*.exe from the windows-exe-11 artifact of Build run ${RUN_ID} and commit them." exit 0 fi if ! git rebase --quiet FETCH_HEAD; then git rebase --abort || true echo "::warning title=Could not refresh the launcher binaries::$BRANCH moved on and the rebuilt launchers conflict with it. Refresh opendj-server-legacy/lib/*.exe from the windows-exe-11 artifact of Build run ${RUN_ID} and commit them." exit 0 fi if git push --quiet origin "HEAD:refs/heads/$BRANCH"; then # A refresh that already landed leaves the rebase with nothing to replay and # the push with nothing to send, both of them silently successful: report # what happened rather than claiming a push that was a no-op. if [ "$(git rev-parse HEAD)" = "$(git rev-parse FETCH_HEAD)" ]; then echo "The launchers committed on $BRANCH already match the rebuilt ones." else echo "Refreshed launchers pushed to $BRANCH." fi exit 0 fi echo "$BRANCH moved while pushing - retrying ($attempt/3)." done echo "::warning title=Could not refresh the launcher binaries::$BRANCH kept moving under this job. Refresh opendj-server-legacy/lib/*.exe from the windows-exe-11 artifact of Build run ${RUN_ID} and commit them." - name: Set up Java for publishing to Maven Central Repository OSS uses: actions/setup-java@v5 with: @@ -114,11 +233,25 @@ with: name: OpenDJ RPM Package path: opendj-packages/opendj-rpm/opendj-rpm-standard/target/rpm/opendj/RPMS/noarch/*.rpm # The MSI can only be built on Windows; reuse the one already built by the triggering # Build run (windows-latest-11 artifact) instead of rebuilding it here. - name: Download Windows build artifact (contains the MSI) continue-on-error: true uses: actions/download-artifact@v8 with: name: windows-latest-11 run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} path: windows-build - name: Upload artifacts OpenDJ MSI Package continue-on-error: true uses: actions/upload-artifact@v7 with: name: OpenDJ MSI Package path: opendj-packages/opendj-msi/opendj-msi-standard/target/*.msi path: windows-build/opendj-packages/opendj-msi/opendj-msi-standard/target/*.msi # Make a silently-missing MSI visible: the step fails (job continues via # continue-on-error) instead of warning and publishing nothing. if-no-files-found: error - name: Upload artifacts OpenDJ Docker Packages uses: actions/upload-artifact@v7 with: @@ -193,3 +326,4 @@ git commit -a -m "upload ${{github.event.repository.name}} docs after deploy ${{ github.sha }}" git push --force https://github.com/OpenIdentityPlatform/doc.openidentityplatform.org.git fi .github/workflows/release.yml
@@ -45,18 +45,11 @@ env: GITHUB_CONTEXT: ${{ toJSON(github) }} run: echo "$GITHUB_CONTEXT" - name: Install wine+rpm for distribution - name: Install rpm for distribution 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 @@ -96,6 +89,18 @@ MAVEN_OPTS: -Dhttps.protocols=TLSv1.2 -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 -Dmaven.wagon.http.retryHandler.requestSentEnabled=true -Dmaven.wagon.http.retryHandler.count=10 if: ${{ env.MAVEN_USERNAME!='' && env.MAVEN_PASSWORD!='' }} run: mvn --batch-mode -Darguments="-Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }}" -DsignTag=true -DtagNameFormat="${{ github.event.inputs.releaseVersion }}" -DreleaseVersion=${{ github.event.inputs.releaseVersion }} -DdevelopmentVersion=${{ github.event.inputs.developmentVersion }} release:prepare release:perform --file pom.xml # Hand the just-released server zip to the release-msi job (the MSI can only be # built on Windows), so it does not have to rebuild opendj-server-legacy. - name: Upload the server zip for the MSI job continue-on-error: true uses: actions/upload-artifact@v7 with: name: release-server-zip retention-days: 1 path: target/checkout/opendj-server-legacy/target/package/*.zip # A missing zip means release-msi cannot build: fail this step (the job keeps # going thanks to continue-on-error, but the loss is visible). if-no-files-found: error - name: Release on GitHub uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: @@ -110,7 +115,6 @@ target/checkout/opendj-ldap-toolkit/target/*.zip target/checkout/opendj-packages/opendj-deb/opendj-deb-standard/target/*.deb target/checkout/opendj-packages/opendj-rpm/opendj-rpm-standard/target/rpm/opendj/RPMS/noarch/*.rpm target/checkout/opendj-packages/opendj-msi/opendj-msi-standard/target/*.msi target/checkout/opendj-packages/opendj-docker/target/Dockerfile.zip target/checkout/opendj-packages/opendj-openshift-template/*.yaml target/checkout/opendj-doc-generated-ref/target/*.zip @@ -156,6 +160,82 @@ git tag -f ${TAG_NAME} git push --quiet --force origin ${TAG_NAME} # The MSI can only be built on Windows. Reuses the server zip built by release-maven # (installed into the local repo), so only the opendj-msi-standard module is built here. # continue-on-error: an MSI failure must not break the release. release-msi: name: Windows MSI release runs-on: 'windows-latest' continue-on-error: true # contents: write is required by action-gh-release to attach the MSI to the release; # the workflow-level default above is contents: read. permissions: contents: write needs: - release-maven steps: - uses: actions/checkout@v6 with: ref: ${{ github.event.inputs.releaseVersion }} submodules: recursive - name: Set up Java uses: actions/setup-java@v5 with: java-version: '11' distribution: 'temurin' # restore, not the full cache action: the install:install-file below puts a # dependency-less generated pom for opendj-server-legacy into the local repository, # and saving that under the key build-maven restores from would seed every later # Windows build with it. - name: Cache Maven packages uses: actions/cache/restore@v5 with: path: ~/.m2/repository key: ${{ runner.os }}-m2-repository-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2-repository - name: Setup WiX (.NET tool) shell: bash run: | 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 - name: Download the server zip built by release-maven uses: actions/download-artifact@v8 with: name: release-server-zip path: server-zip - name: Install the server zip into the local Maven repository shell: bash run: | # The artifact carries both zips and the slim one sorts first ('-' < '.'), so # filter it out: the slim zip lacks the JDBC/Cassandra backend drivers and the # MSI must be packaged from the full server zip. ZIP=$(ls server-zip/*.zip | grep -v -- '-slim\.zip$' | head -1) echo "Installing $ZIP as opendj-server-legacy:${{ github.event.inputs.releaseVersion }}:zip" mvn --batch-mode install:install-file -Dfile="$ZIP" \ -DgroupId=org.openidentityplatform.opendj -DartifactId=opendj-server-legacy \ -Dversion=${{ github.event.inputs.releaseVersion }} -Dpackaging=zip - name: Build the MSI (packaging only, no rebuild) env: MAVEN_OPTS: -Dhttps.protocols=TLSv1.2 -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 -Dmaven.wagon.http.retryHandler.requestSentEnabled=true -Dmaven.wagon.http.retryHandler.count=10 # -P: do not rely on the wix.exe file-activation of distribution-windows-msi. The # profile now lives in opendj-msi-standard and wraps its <build>, and the module is # part of every reactor, so -pl always resolves it; what -P buys is forcing the # plugins on when wix.exe is not under %USERPROFILE%\.dotnet\tools. Without it this # builds a pom that produces nothing, and fail_on_unmatched_files below is the only # symptom - the job's continue-on-error swallows everything else. run: mvn --batch-mode --errors -DskipTests package -pl :opendj-msi-standard -Pdistribution-windows-msi --file pom.xml - name: Attach the MSI to the GitHub release uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: tag_name: ${{ github.event.inputs.releaseVersion }} fail_on_unmatched_files: true files: opendj-packages/opendj-msi/opendj-msi-standard/target/*.msi release-docker: name: Docker release runs-on: 'ubuntu-latest' opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc
@@ -608,14 +608,16 @@ [#install-msi] .To Install With the Windows Installer (MSI) ==== On Windows you can install OpenDJ directory server from the `.msi` package. The installer only copies the server files to disk: it does not configure or start a server, it does not register a Windows service, and it does not install a Java runtime. On Windows you can install OpenDJ directory server from the `.msi` package. The installer only copies the server files to disk: it does not configure or start a server, it does not register a Windows service, and it does not install a Java runtime. Registering the server as a Windows service stays an explicit step you take with the `windows-service` command, exactly as for the cross-platform (.zip) delivery, and the service you register is yours: the package never creates, removes or reconfigures one. . Make sure a supported Java runtime is available, as described in xref:#before-you-install["To Prepare For Installation"]. + The installer does not check for Java. If your default Java environment is not appropriate, set `OPENDJ_JAVA_HOME` to the correct Java installation (or `OPENDJ_JAVA_BIN` to the absolute path of the `java` command), or make sure `java` is on the `PATH`, before you run `setup` or start the server. The installer itself does not check for or install Java, but `setup` and the server require it: install a JRE (for example link:https://adoptium.net[Eclipse Temurin, window=\_blank]) and set `JAVA_HOME` to your Java installation, or make sure the `java` executable is on the `PATH`. If your default Java environment is not the one OpenDJ should use, set `OPENDJ_JAVA_HOME` to the correct Java installation (or `OPENDJ_JAVA_BIN` to the absolute path of the `java` command) before you run `setup` or start the server. . Install the package, either with the GUI or silently: + The package is not code-signed, so Windows SmartScreen or User Account Control may warn about an unrecognized publisher; choose to run the installer anyway. + * GUI: double-click `opendj-{opendj-version}.msi` and follow the wizard. + * Silent: run the following command (optionally set the installation directory with the `OPENDJ` property): @@ -626,9 +628,11 @@ C:\> msiexec /i opendj-{opendj-version}.msi /quiet OPENDJ="C:\opendj" ---- + By default the package installs under `C:\Program Files\OpenDJ` (the 32-bit installer uses `C:\Program Files (x86)\OpenDJ` on 64-bit Windows). When `OPENDJ` is not given, the installer uses an existing OpenDJ installation directory when it detects one — the location recorded in the registry by a previous x64 package, or the legacy 32-bit default `C:\Program Files (x86)\OpenDJ` — and otherwise installs under `C:\Program Files\OpenDJ`. + The service runs as `LocalSystem`. A directory created directly under the drive root (for example `C:\opendj`) is writable by all authenticated users by default, which would let a standard user replace the server scripts that the service runs. Prefer the default location under `Program Files`, or restrict the ACL of a custom installation directory. . Configure OpenDJ directory server by running the `setup` command, described in xref:../reference/admin-tools-ref.adoc#setup-1[setup(1)] in the __Reference__, from the installation directory. Use `setup.bat` for the GUI wizard or `setup.bat --cli` for the command-line: . Configure OpenDJ directory server by running the `setup` command, described in xref:../reference/admin-tools-ref.adoc#setup-1[setup(1)] in the __Reference__, from the installation directory. Use `setup.bat` for the GUI wizard or `setup.bat --cli` for the command-line. When OpenDJ is installed under `Program Files`, run the command from an elevated (run as Administrator) prompt — the server writes into its installation directory: + [source, console] @@ -636,7 +640,7 @@ C:\path\to\opendj> setup.bat --cli ---- . (Optional) Register OpenDJ as a Windows service and start it. The MSI does not register the service; use the `windows-service` command: . (Optional) Register OpenDJ as a Windows service and start it. The MSI does not register the service; use the `windows-service` command from an elevated prompt: + [source, console] @@ -644,6 +648,8 @@ C:\path\to\opendj\bat> windows-service.bat --enableService C:\> net start "OpenDJ Server" ---- + The service takes the display name `OpenDJ Server`; when a host already runs another registered instance, the next one gets a key name such as `OpenDJ Server-2`. Remember to disable the service with `windows-service.bat --disableService` before you uninstall the package, or the registration is left pointing at removed files. ==== opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc
@@ -164,7 +164,7 @@ ==== Remove OpenDJ directory server installed from the `.msi` package like any other Windows program. . If OpenDJ is registered as a Windows service, remove the service first: . If OpenDJ is registered as a Windows service, remove the service first — the package does not manage it and leaves the registration behind, pointing at files that are about to be deleted: + [source, console] @@ -180,7 +180,7 @@ C:\> msiexec /x opendj-{opendj-version}.msi /quiet ---- + Uninstalling removes the files installed by the package. Your configured instance data under the installation directory (for example `config`, `db`, and `logs`) is not removed; delete the installation directory manually to remove all files. Uninstalling removes the files installed by the package. Your configured instance data under the installation directory (for example `config`, `db`, and `logs`) is not removed; delete the installation directory manually, or run `uninstall.bat` before removing the package, to remove all files. Running `uninstall.bat` also disables the Windows service if one is registered, which covers the first step above. ==== opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc
@@ -259,25 +259,26 @@ ==== Before starting this procedure, follow the steps in xref:#before-you-upgrade["Before You Upgrade"]. Installing the newer `.msi` performs a major upgrade that replaces the installed program files, so make a full file-system backup of the current installation first. . Stop the current OpenDJ server. . If OpenDJ is registered as a Windows service, disable the service: . Stop the current OpenDJ server; if it runs as a Windows service, stop the service with `net stop "OpenDJ Server"` from an elevated prompt and let the command finish. The installer also tries to stop it, but only succeeds when it is itself running elevated: started by double-click, it cannot, and refuses the upgrade with Windows Installer error 1722 naming the `CheckServiceStopped` action rather than replacing the files under a running server. It refuses in the same way while the service is still starting, and gives a stop that is already under way 90 seconds to complete. + [source, console] ---- C:\path\to\opendj\bat> windows-service.bat --disableService ---- A server started with `start-ds.bat` rather than as a service is refused in the same way, by the `CheckServerNotRunning` action: it holds the same program files, and the installer will not replace them underneath it. Stop it with `stop-ds.bat` and let the command finish. The check allows 60 seconds for a stop that is already under way, because a stop command returning is not the same as the server having released its files. . Back up the file-system directory where OpenDJ is installed. . Install the newer package (GUI or silent), using the same installation directory as the current server. Your configured instance data (`config`, `db`, `logs`) is kept; only the program files are replaced: . Install the newer package (GUI or silent). The installer detects the existing installation — the location recorded in the registry by a previous x64 package, or the default directory of the older 32-bit package (`C:\Program Files (x86)\OpenDJ`) — and installs into the same directory, so your configured instance data (`config`, `db`, `logs`) is kept and only the program files are replaced. If the older server was installed in a custom directory the installer cannot detect, select that directory in the wizard or pass it explicitly on the command line: rather than installing a fresh server into the default directory while emptying the old one, the installer refuses to continue whenever nothing has recorded where the old server lives and the directory it is about to install into holds no OpenDJ server -- which also catches a mistyped directory name. That refusal also covers an old server that really is installed in `C:\Program Files\OpenDJ`, because the 32-bit packages recorded no location at all — and that one case the wizard cannot resolve: choosing the default directory in the wizard leaves the installer with the same values it would have had if you had chosen nothing, so pass `OPENDJ` on the command line instead — it can be given with or without `/quiet`, so a wizard installation takes it just as a silent one does. The installer further refuses to install into a directory other than the one it detected, unless the directory you name holds an OpenDJ server itself (see the note below): it replaces an installation in place and cannot move one, so uninstall the existing server first if you want it somewhere else. + [source, console, subs="attributes"] ---- C:\> msiexec /i opendj-{opendj-version}.msi /quiet OPENDJ="C:\path\to\opendj" ---- + [NOTE] ====== Pass `OPENDJ` explicitly whenever an unrelated OpenDJ directory tree — a Zip installation, a copy, a decommissioned instance — sits in `C:\Program Files (x86)\OpenDJ` while the server you are upgrading lives somewhere else and was installed by a package that recorded no location (5.1.x and earlier). Detection can only ask whether that directory holds a server, not which server, so left to itself it adopts the stray tree: the upgrade then replaces the files there while removing the installation you meant to upgrade. Naming the directory settles it — the installer takes an explicitly named directory that holds a server as the one to upgrade, and only refuses the name if that directory holds no server, which is what relocating an installation looks like. ====== + A registered Windows service survives the upgrade untouched: it names the service wrapper inside the installation directory, which the upgrade replaces in place, so the registration — including a dedicated service account, recovery actions or dependencies you configured — keeps working against the refreshed server. No `--disableService`/`--enableService` cycle is needed. . Run the `upgrade` command, described in xref:../reference/admin-tools-ref.adoc#upgrade-1[upgrade(1)] in the __Reference__, to bring the configuration and application data up to date with the new binary and script files: + @@ -287,14 +288,12 @@ C:\path\to\opendj> upgrade.bat --no-prompt --acceptLicense ---- . Start the upgraded OpenDJ server. . If you disabled the Windows service, enable it again: . Start the upgraded OpenDJ server; if it is registered as a Windows service, start the service again: + [source, console] ---- C:\path\to\opendj\bat> windows-service.bat --enableService C:\> net start "OpenDJ Server" ---- ==== opendj-packages/opendj-msi/opendj-msi-standard/pom.xml
@@ -13,7 +13,7 @@ information: "Portions Copyright [year] [name of copyright owner]". Copyright 2015-2016 ForgeRock AS. Portions Copyright 2018 Open Identity Platform Community Portions Copyright 2018-2026 3A Systems, LLC --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> @@ -29,265 +29,155 @@ <name>OpenDJ MSI Standard Package</name> <description> This module generates an OpenDJ MSI package. This module generates an OpenDJ MSI package using the WiX Toolset v5 .NET tool (Windows only: WiX's build task P/Invokes msi.dll). The `wix` tool must be on the PATH with the UI extension installed: set DOTNET_ROLL_FORWARD=Major dotnet tool install --global wix --version 5.0.2 wix extension add -g WixToolset.UI.wixext/5.0.2 The module itself is part of every reactor so that the release plugin keeps its version in step with the rest of the build; the toolchain profile below is what turns the MSI build on, and only when %USERPROFILE%\.dotnet\tools\wix.exe exists. </description> <properties> <msi.resources>${basedir}/resources/msi</msi.resources> <package.dir>${project.build.directory}/${product.name.lowercase}</package.dir> <!-- Exclusions are done here (Ant), not in WiX <Files> (which only takes Include in WiX 5): stagingRoot = payload minus lib; stagingLib = lib minus the Unix scripts. --> <staging.root>${project.build.directory}/msi-staging</staging.root> <staging.lib>${project.build.directory}/msi-staging-lib</staging.lib> <msi.file>${project.build.directory}/${product.name.lowercase}-${project.version}.msi</msi.file> </properties> <dependencies> </dependencies> <dependencies> </dependencies> <!-- Everything that needs Windows lives in this profile rather than in the module list that pulls the project in: a module outside the reactor is a module the release plugin cannot version-bump (see the note in opendj-packages/pom.xml). Off, the module is a pom that builds nothing; on, it produces the MSI. The activation is the one the packages pom used to carry - Windows plus an installed WiX v5 .NET tool, both conditions required - so a plain `mvn install` on a contributor's Windows machine without wix no longer fails mid-reactor with "Cannot run program wix". Name it explicitly (-Pdistribution-windows-msi) when wix lives outside the dotnet global-tools directory; release.yml does exactly that. --> <profiles> <profile> <id>distribution-windows-msi</id> <activation> <os><family>windows</family></os> <file><exists>${env.USERPROFILE}/.dotnet/tools/wix.exe</exists></file> </activation> <build><finalName>${project.groupId}.${project.artifactId}</finalName> <plugins> <!-- Unpacks the opendj-server-legacy distribution (unpack-archive execution inherited from the opendj-packages parent pluginManagement). --> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.openidentityplatform.commons</groupId> <artifactId>maven-external-dependency-plugin</artifactId> <inherited>false</inherited> <configuration> <stagingDirectory> ${project.build.directory}/dependencies/ </stagingDirectory> <createChecksum>false</createChecksum> <skipChecksumVerification>true</skipChecksumVerification> <force>false</force> <artifactItems> <artifactItem> <groupId>openidentityplatform.org</groupId> <artifactId>wixtoolset</artifactId> <version>3.11.1</version> <packaging>zip</packaging> <downloadUrl> https://github.com/wixtoolset/wix3/releases/download/wix3111rtm/wix311-binaries.zip </downloadUrl> <deploy>false</deploy> </artifactItem> <artifactItem> <groupId>openidentityplatform.org</groupId> <artifactId>winetricks</artifactId> <version>LAST</version> <packaging>sh</packaging> <downloadUrl> https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks </downloadUrl> <deploy>false</deploy> </artifactItem> </artifactItems> </configuration> <executions> <execution> <id>clean-external-dependencies</id> <phase>clean</phase> <goals> <goal>clean-external</goal> </goals> </execution> <execution> <id>resolve-install-external-dependencies</id> <phase>process-resources</phase> <goals> <goal>resolve-external</goal> <goal>install-external</goal> </goals> </execution> <execution> <id>deploy-external-dependencies</id> <phase>deploy</phase> <goals> <goal>deploy-external</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>unpack-wix</id> <phase>package</phase> <goals> <goal>unpack</goal> </goals> <configuration> <artifactItems> <artifactItem> <groupId>openidentityplatform.org</groupId> <artifactId>wixtoolset</artifactId> <version>3.11.1</version> <type>zip</type> </artifactItem> </artifactItems> <outputDirectory> ${project.build.directory}/wix </outputDirectory> </configuration> </execution> <execution> <id>unpack-winetricks</id> <phase>package</phase> <goals> <goal>copy</goal> </goals> <configuration> <artifactItems> <artifactItem> <groupId>openidentityplatform.org</groupId> <artifactId>winetricks</artifactId> <version>LAST</version> <type>sh</type> </artifactItem> </artifactItems> <outputDirectory> ${project.build.directory}/winetricks </outputDirectory> </configuration> </execution> </executions> </plugin> <!-- Stage the payload to harvest: keep Windows files, drop macOS *.app and Unix scripts. --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> <executions> <execution> <id>build-msi-package-prepare</id> <id>stage-msi-payload</id> <phase>package</phase> <goals> <goal>run</goal> </goals> <configuration> <target> <delete dir="${user.home}/.msi" /> <mkdir dir="${user.home}/.msi" /> <copy toDir="${user.home}/.msi"> <!-- Everything except lib (and macOS/Unix bits) -> stagingRoot --> <delete dir="${staging.root}"/> <mkdir dir="${staging.root}"/> <copy toDir="${staging.root}"> <fileset dir="${package.dir}"> <!-- Exclude Mac apps --> <exclude name="QuickSetup.app/**" /> <exclude name="Uninstall.app/**" /> <!-- Exclude shell scripts --> <exclude name="bin/**" /> <exclude name="setup" /> <exclude name="uninstall" /> <exclude name="upgrade" /> <exclude name="**/*.sh" /> <exclude name="QuickSetup.app/**"/> <exclude name="Uninstall.app/**"/> <exclude name="bin/**"/> <exclude name="lib/**"/> <exclude name="setup"/> <exclude name="uninstall"/> <exclude name="upgrade"/> <exclude name="**/*.sh"/> </fileset> </copy> <copy file="${msi.resources}/package.wxs" toDir="${project.build.directory}" /> <copy file="${msi.resources}/opendjbanner.bmp" toDir="${project.build.directory}" /> <copy file="${msi.resources}/opendjdialog.bmp" toDir="${project.build.directory}" /> <exec osfamily="windows" executable="${project.build.directory}\wix\heat.exe" dir="${project.build.directory}" failifexecutionfails="false" resultproperty="heatStatusCode"> <arg value="dir" /> <arg value="${user.home}\.msi" /> <arg value="-nologo" /> <arg value="-cg" /><arg value="all" /> <arg value="-gg" /> <arg value="-sfrag" /> <arg value="-srd" /> <arg value="-ke" /> <arg value="-dr" /><arg value="OPENDJ" /> <arg value="-var" /><arg value="var.src" /> <arg value="-template" /><arg value="fragment" /> <arg value="-o" /><arg value="payload.wxs" /> <arg value="-v" /> </exec> <exec osfamily="windows" executable="${project.build.directory}\wix\candle.exe" dir="${project.build.directory}" resultproperty="candleStatusCode"> <arg value="-nologo" /> <arg value="-dsrc=${user.home}\.msi" /> <arg value="-dname=${product.name}" /> <arg value="-dmajor=${parsedVersion.majorVersion}" /> <arg value="-dminor=${parsedVersion.minorVersion}" /> <arg value="-dpoint=${parsedVersion.incrementalVersion}" /> <arg value="package.wxs" /> <arg value="payload.wxs" /> <arg value="-v" /> </exec> <exec osfamily="windows" executable="${project.build.directory}\wix\light.exe" dir="${project.build.directory}" resultproperty="lightStatusCode"> <arg value="-nologo" /> <arg value="-ext" /> <arg value="WixUIExtension" /> <arg value="-out" /> <arg value="${product.name.lowercase}-${project.version}.msi" /> <arg value="-sval" /> <arg value="-v" /> <arg value="package.wixobj" /> <arg value="payload.wixobj" /> </exec> <exec osfamily="unix" executable="wine" resultproperty="WineExitStatusCode" failifexecutionfails="false"> <arg value="wineboot" /> <arg value="--init" /> </exec> <exec osfamily="unix" executable="sh" resultproperty="WineTricksExitStatusCode" failifexecutionfails="false"> <arg value="${project.build.directory}/winetricks/winetricks-LAST.sh" /> <arg value="--unattended" /> <arg value="dotnet40" /> <arg value="dotnet_verifier" /> </exec> <property name="exec.heat" value="wine" /><property name="param.heat" value="${project.build.directory}/wix/heat.exe" /> <property name="exec.candle" value="wine" /><property name="param.candle" value="${project.build.directory}/wix/candle.exe" /> <property name="exec.light" value="wine" /><property name="param.light" value="${project.build.directory}/wix/light.exe" /> <echo>------------------- ${exec.heat} ${param.heat} -------------------</echo> <exec osfamily="unix" executable="${exec.heat}" dir="${project.build.directory}" failifexecutionfails="false" resultproperty="heatStatusCode"> <arg value="${param.heat}" /> <arg value="dir" /> <arg value="${user.home}/.msi" /> <arg value="-nologo" /> <arg value="-cg" /><arg value="all" /> <arg value="-gg" /> <arg value="-sfrag" /> <arg value="-srd" /> <arg value="-ke" /> <arg value="-dr" /><arg value="OPENDJ" /> <arg value="-var" /><arg value="var.src" /> <arg value="-template" /><arg value="fragment" /> <arg value="-o" /><arg value="payload.wxs" /> <arg value="-v" /> </exec> <echo>------------------- ${exec.candle} ${param.candle} -------------------</echo> <exec osfamily="unix" executable="${exec.candle}" dir="${project.build.directory}" resultproperty="candleStatusCode"> <arg value="${param.candle}" /> <arg value="-nologo" /> <arg value="-dsrc=${user.home}/.msi" /> <arg value="-dname=${product.name}" /> <arg value="-dmajor=${parsedVersion.majorVersion}" /> <arg value="-dminor=${parsedVersion.minorVersion}" /> <arg value="-dpoint=${parsedVersion.incrementalVersion}" /> <arg value="package.wxs" /> <arg value="payload.wxs" /> <arg value="-v" /> </exec> <echo>------------------- ${exec.light} ${param.light} -------------------</echo> <exec osfamily="unix" executable="${exec.light}" dir="${project.build.directory}" resultproperty="lightStatusCode"> <arg value="${param.light}" /> <arg value="-nologo" /> <arg value="-ext" /> <arg value="WixUIExtension" /> <arg value="-out" /> <arg value="${product.name.lowercase}-${project.version}.msi" /> <arg value="-sval" /> <arg value="-v" /> <arg value="package.wixobj" /> <arg value="payload.wixobj" /> </exec> <chmod file="${project.build.directory}/${product.name.lowercase}-${project.version}.msi" perm="ugo+r" verbose="true" /> <exec osfamily="unix" executable="ls" dir="${project.build.directory}"> <arg value="-laht" /> </exec> <delete dir="${user.home}/.msi" /> <attachartifact file="${project.build.directory}/${product.name.lowercase}-${project.version}.msi" /> <!-- lib (the Windows launchers included) -> stagingLib --> <delete dir="${staging.lib}"/> <mkdir dir="${staging.lib}"/> <copy toDir="${staging.lib}"> <fileset dir="${package.dir}/lib"> <exclude name="**/*.sh"/> </fileset> </copy> </target> </configuration> </execution> </executions> </plugin> <!-- Build the x64 MSI with the WiX v5 .NET tool (exec-maven-plugin version managed in the root pom). --> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <executions> <execution> <id>wix-build-msi</id> <phase>package</phase> <goals> <goal>exec</goal> </goals> <configuration> <executable>wix</executable> <workingDirectory>${project.build.directory}</workingDirectory> <arguments> <argument>build</argument> <argument>${msi.resources}/package.wxs</argument> <argument>-arch</argument><argument>x64</argument> <argument>-ext</argument><argument>WixToolset.UI.wixext</argument> <argument>-bindpath</argument><argument>${msi.resources}</argument> <argument>-d</argument><argument>name=${product.name}</argument> <argument>-d</argument><argument>major=${parsedVersion.majorVersion}</argument> <argument>-d</argument><argument>minor=${parsedVersion.minorVersion}</argument> <argument>-d</argument><argument>point=${parsedVersion.incrementalVersion}</argument> <argument>-d</argument><argument>stagingRoot=${staging.root}</argument> <argument>-d</argument><argument>stagingLib=${staging.lib}</argument> <argument>-o</argument><argument>${msi.file}</argument> </arguments> </configuration> </execution> </executions> </plugin> <!-- Attach the MSI produced by the wix execution above. Declared after it so the attachment happens once the file exists: `target` is not cleaned between runs, and attaching first would silently pick up a stale MSI if the wix build were skipped. --> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <executions> <execution> <id>attach-msi</id> <phase>package</phase> <goals> <goal>attach-artifact</goal> </goals> <configuration> <artifacts> <artifact><file>${msi.file}</file><type>msi</type></artifact> </artifacts> </configuration> </execution> </executions> </plugin> </plugins> </build> </project> </profile> </profiles> </project> opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs
@@ -15,45 +15,560 @@ ! Copyright 2013-2016 ForgeRock AS. ! Portions Copyright 2018-2026 3A Systems, LLC ! --> <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> <Product Id="*" Codepage="1252" Language="1033" Manufacturer="Open Identity Platform Community" Name="$(var.name) $(var.major).$(var.minor).$(var.point)" Version="$(var.major).$(var.minor).$(var.point)" UpgradeCode="A3E82AC0-88E6-4DEE-9D8C-5AE3B7853274"> <Package Id="*" Comments="This package contains $(var.name) $(var.major).$(var.minor).$(var.point)." Description="Open Identity Platform Community products" InstallerVersion="300" Languages="1033" Manufacturer="Open Identity Platform Community" Platform="x86" Compressed="yes"/> <Media Id="1" Cabinet="opendj.cab" DiskPrompt="Disk 1" EmbedCab="yes" CompressionLevel="none"/> <Property Id="DiskPrompt" Value="$(var.name) $(var.major).$(var.minor).$(var.point) Installation"/> <Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui"> <Package Name="$(var.name) $(var.major).$(var.minor).$(var.point)" Manufacturer="Open Identity Platform Community" Version="$(var.major).$(var.minor).$(var.point)" UpgradeCode="A3E82AC0-88E6-4DEE-9D8C-5AE3B7853274" Language="1033" Codepage="1252" InstallerVersion="500" Scope="perMachine"> <Property Id="ALLUSERS" Value="1"/> <SummaryInformation Description="Open Identity Platform Community products" Manufacturer="Open Identity Platform Community"/> <!-- AllowSameVersionUpgrades: a rebuilt MSI at the same 3-part version must still be detected as an upgrade (hotfix re-release), not produce a second ARP entry. Schedule=afterInstallInitialize keeps RemoveExistingProducts early (the late afterInstallExecute variant requires matching component GUIDs for matching paths, which the auto-generated GUIDs of the old WiX3 x86 package cannot guarantee - a late RemoveExistingProducts would then delete freshly installed files; removing the old product early is safe here because instance data (config, db, logs) is not part of any component set) while placing it inside the installation transaction: a failed upgrade rolls the removal back and restores the old product, which the afterInstallValidate default does not. --> <MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." AllowSameVersionUpgrades="yes" Schedule="afterInstallInitialize"/> <MediaTemplate EmbedCab="yes" CompressionLevel="high"/> <Property Id="ARPHELPLINK" Value="https://github.com/OpenIdentityPlatform/OpenDJ/wiki"/> <!-- Upgrade path detection: install over an existing OpenDJ instead of the default. Two separate searches with explicit SetProperty ordering (AppSearch row order is formally undefined). Precedence: an explicit OPENDJ (command line / UI) always wins; then the registry value written by this package (custom paths, new installs); then the legacy x86 default directory ([ProgramFilesFolder] is Program Files (x86) in an x64 package), fresh installs with no recorded location only. The registry action runs FIRST: each action is guarded by NOT OPENDJ, so whichever runs earlier wins and the registry has no NOT Installed guard (Remember Property pattern) so maintenance and uninstall resolve the real location. Type="raw", not "directory": an existence-validated search silently drops the value when the recorded directory is renamed or its volume is offline, un-registering a properly registered host - the value this package writes is REG_SZ, which raw returns unmangled. --> <Property Id="OPENDJ_REG" Secure="yes"> <RegistrySearch Id="InstallDirRegistrySearch" Root="HKLM" Key="SOFTWARE\$(var.name)" Name="InstallDir" Type="raw"/> </Property> <Property Id="OPENDJ_LEGACY" Secure="yes"> <DirectorySearch Id="LegacyX86InstallDir" Path="[ProgramFilesFolder]$(var.name)"/> </Property> <!-- The legacy default location, this time asked whether it actually holds a server rather than merely exists. OPENDJ_LEGACY above stays a bare existence test only because a fresh install may legitimately adopt an empty legacy directory; the decisions taken during an upgrade use this one instead, since "the directory is there" says nothing about a product living in it. What it still cannot say is WHICH product lives there - see the residual documented with the guards below. --> <Property Id="OPENDJ_LEGACY_INSTALL" Secure="yes"> <DirectorySearch Id="LegacyX86InstallCheck" Path="[ProgramFilesFolder]$(var.name)"> <FileSearch Id="LegacyX86Setup" Name="setup.bat"/> </DirectorySearch> </Property> <!-- The same question asked about the directory this install is aimed at: does it hold a server? It is what separates a relocation (the target is new or empty, and the instance data would be stranded) from an upgrade of a server the searches cannot see (the target holds it), which is the only distinction the second guard below is missing. AppSearch runs before every SetProperty below, so in a silent install [OPENDJ] here is what the command line put there - the same window OPENDJ_GIVEN is captured in. A full-UI session runs AppSearch twice: once in the client with the same command-line value, and again in the execute sequence, by which point OPENDJ has arrived from the client fully resolved. So the property means "the NAMED directory holds a server" silently and "the TARGET directory holds a server" in the wizard, and both readings are the question the guards ask: silently there is nothing but the command line to aim an upgrade at a directory no search can see, and in the wizard the browse dialog is that same instruction. When OPENDJ is empty the path does not expand to a full path and, with no Parent and the default depth of zero, the installer looks for setup.bat in the root of each fixed drive instead. That costs a handful of stat calls and cannot change a decision: the containment test below fails for a hit in a drive root, which is not inside the directory being asked about. Public, because a search property has to be (WIX0012: AppSearch cannot fill a private one), so it CAN be handed in on the command line, and AppSearch only overwrites what it finds. The guard below therefore does not test it for truth: it requires the value to be the setup.bat inside the directory concerned (OPENDJ_GIVEN_INSTALL ~>< OPENDJ, the search sets it to the full path of the file found). OPENDJ_GIVEN_INSTALL=1 no longer says anything. That makes it not a switch rather than a lock - a value spelled out as the path inside that directory would still pass, which is deliberate construction rather than a slip. Not Secure because it does not need to be: AppSearch re-runs in the execute sequence and fills it there, so unlike OPENDJ_GIVEN it depends on no client-to-server transfer. --> <Property Id="OPENDJ_GIVEN_INSTALL"> <DirectorySearch Id="GivenInstallCheck" Path="[OPENDJ]"> <FileSearch Id="GivenSetup" Name="setup.bat"/> </DirectorySearch> </Property> <!-- Evidence that the "OpenDJ Server" service - the one windows-service.bat registers for the FIRST instance on a host - belongs to the tree this install is about to replace: its ImagePath records where it points ('"<root>\lib\opendj_service.exe" start "<root>"'). Compared below with a case-insensitive substring match (~><) against the resolved [OPENDJ], gating the stop-and-verify pair: only a service that provably points into that tree is ever acted on. Unset (no such service) fails the comparison, so the actions correctly do nothing. Type="raw" and substring rather than equality on purpose: CreateService stores ImagePath as REG_EXPAND_SZ, which a raw search returns unexpanded and prefixed with '#%', so neither an equality test nor the prefix operator (<<) would ever match. The residual is that a bare directory name could also match a sibling (C:\dj vs C:\dj2) when OPENDJ is passed on the command line without a trailing backslash - accepted: the worst case is refusing an upgrade whose neighbouring instance is running. Additional instances get key names like "OpenDJ Server-2", which this search does not see - as does a ko/zh_TW installation whose service was registered under the localized "OpenDS" name - so their service is not stopped here. Such an upgrade is caught by CheckServerNotRunning below instead, which is deliberately not ImagePath-gated: a server running out of the tree holds locks\server.lock and the upgrade is refused with 1603 - a refusal, not damage, and the residual is that the stop is never attempted. What that guard cannot do is refuse on a lock it is not allowed to read at all; it fails open there, and says so where it is defined. The Services key is shared between registry views. --> <Property Id="OPENDJ_SVC_IMAGEPATH" Secure="yes"> <RegistrySearch Id="OpendjSvcImagePathSearch" Root="HKLM" Key="SYSTEM\CurrentControlSet\Services\OpenDJ Server" Name="ImagePath" Type="raw"/> </Property> <!-- The directory that was NAMED rather than guessed: the answer the first guard below accepts from an administrator whose old server really does live in the default directory. Captured before AppSearch, where OPENDJ can only hold what the command line put there - the searches have not run, no SetProperty below has fired, and CostFinalize has not yet resolved it as a directory property. Sequence="first" is what keeps that true in a UI session: the action runs in the UI sequence, and in the execute sequence only when no UI sequence ran at all (the msidbCustomActionTypeFirstSequence bit). Without it, a full-UI install would set the property every time - CostFinalize and InstallDirDlg have populated OPENDJ long before the execute sequence starts there - and the guards would never fire in the wizard. Public and Secure, holding the named path rather than a flag. An earlier version of this made it a PRIVATE property set to "1", on the reasoning that the guards are immediate actions and therefore run client-side, where a private property is visible. They do not: "the execute sequence table is processed in the installer service" whenever the service is registered, which it is by default, and immediate only means the action impersonates the invoking user inside that process. Private properties "cannot [be set] in the user interface phase of the installation and then pass[ed] ... to the execution phase", and Sequence="first" skips the action in the execute sequence once the UI sequence has run - so a private flag was empty at the guards in EVERY full-UI session, including one started with OPENDJ= on the command line, which is the single route this package's refusal messages and the install guide prescribe. The wizard therefore refused the documented workaround, and the exception in guard (2) below could never apply. No CI scenario could see it: every msiexec call in build.yml is /qn, and the UI sequence is not processed below full UI level - which is also why build.yml pins Sequence="first" by reading it out of this file. Public means the command line can hand it in, so the value carries the weight rather than the presence: the guards require the resolved OPENDJ to START WITH it (~<< in the condition, not ~=: the captured value is the raw command-line string and the resolved one is a directory property carrying a trailing backslash), the same construction OPENDJ_GIVEN_INSTALL above uses. OPENDJ_GIVEN=1 switches nothing off - no resolved path begins with "1" - and a value that does pass has spelled out the directory the install resolved to, which is precisely what naming it on the command line does. Starts-with rather than contains for the same reason: "1" appears inside plenty of real paths, at the start of none. What no property can do is confirm the DEFAULT directory from the wizard: this one is read before AppSearch, InstallDirDlg runs far later and never sets it, so a session that browses to [ProgramFiles64Folder]OpenDJ arrives at the guard with exactly the values a "Next, Next, Install" session has. An old server that really lives there therefore has one route only, the command line - which now works in the wizard as well as silently - and the refusal message says so rather than promising a dialog that cannot help. Publishing the property from InstallDirDlg would restore the promise and the data-loss path with it. --> <Property Id="OPENDJ_GIVEN" Secure="yes"/> <SetProperty Id="OPENDJ_GIVEN" Before="AppSearch" Sequence="first" Value="[OPENDJ]" Condition="OPENDJ"/> <SetProperty Id="OPENDJ" Action="SetOpendjFromRegistry" After="AppSearch" Sequence="both" Value="[OPENDJ_REG]" Condition="OPENDJ_REG AND NOT OPENDJ"/> <!-- NOT Installed is true during a major upgrade as well (the new ProductCode is not installed yet), so on its own it does not mean "fresh install": a leftover empty legacy directory would be adopted as the target while RemoveExistingProducts empties the real one somewhere else. During an upgrade the directory therefore has to prove it holds a server; on a fresh install mere existence still counts, which is how an administrator pre-creates the location. --> <SetProperty Id="OPENDJ" Action="SetOpendjFromLegacyDir" After="SetOpendjFromRegistry" Sequence="both" Value="[OPENDJ_LEGACY]" Condition="NOT Installed AND OPENDJ_LEGACY AND NOT OPENDJ AND (NOT WIX_UPGRADE_DETECTED OR OPENDJ_LEGACY_INSTALL)"/> <!-- The docs tell users to pass OPENDJ on the msiexec command line, so it must survive into the elevated server context. --> <Property Id="OPENDJ" Secure="yes"/> <!-- ARP/winget/SCCM report the install location from ARPINSTALLLOCATION. --> <SetProperty Id="ARPINSTALLLOCATION" Value="[OPENDJ]" After="CostFinalize" Sequence="execute"/> <!-- The directory this package installs into when nothing else resolves. Held as a property so the guards below can compare against it: MSI conditions do not expand [Directory] references inside string literals, SetProperty does. --> <SetProperty Id="OPENDJ_DEFAULT" Value="[ProgramFiles64Folder]$(var.name)\" After="AppSearch" Sequence="both"/> <!-- Same for the legacy default. Built here rather than compared against OPENDJ_LEGACY so that both sides of the equality below are known to carry a trailing backslash. --> <SetProperty Id="OPENDJ_LEGACY_DEFAULT" Value="[ProgramFilesFolder]$(var.name)\" After="AppSearch" Sequence="both"/> <!-- Two ways an upgrade can quietly destroy an installation, both refused rather than performed. Scheduled after CostFinalize in the EXECUTE sequence only, which is the one point where OPENDJ is final no matter how the installer was started: a full-UI session resolves it in the UI sequence and passes it on, a silent one resolves it here. Deliberately NOT in the UI sequence: a guard there fires before WelcomeDlg and leaves a GUI-only administrator with nothing but a command line to retype, even though WixUI_InstallDir ships the browse dialog that would have resolved it. (1) Nothing recorded where the old server lives, and the target cannot prove it is that server. 5.1.x wrote no registry value, so an upgrade from a custom directory resolves no search and OPENDJ falls back to the default - proceeding would install a fresh server there while RemoveExistingProducts empties the old tree, leaving config/db/logs stranded. A directory that WAS named loses exactly as much when it is not the installation: msiexec /i opendj.msi OPENDJ=C:\opendj-custmo /qn over a server in C:\opendj-custom passed this guard while it only asked whether the resolved directory was the default, and guard (2) has nothing to say either with no registry value and no server in the legacy default - so one typo emptied one directory and populated another, and the only thing that had ever refused it was an unrelated tree happening to sit in the legacy default. So the guard covers the registry-less, legacy-less case as a whole, and what lets an upgrade through is directory evidence: [OPENDJ] holds a setup.bat, AND either it is not the fallback default or it was named. The second half is what keeps a decoy from disarming it - a foreign OpenDJ tree in the default directory (a zip install, a manual copy) satisfies a setup.bat search without being the product being upgraded, so evidence alone would let it through while RemoveExistingProducts gutted the real installation elsewhere and InstallFiles landed on top of the stranger. Naming the DEFAULT directory is the way out for an old server that really does live there: 5.1.x installed into it recorded nothing, so the package cannot tell that tree from the decoy on its own and has to be told. Only the command line for THAT case: see OPENDJ_GIVEN above for why the wizard cannot confirm the default directory, and the message below for what it says instead. The command line does work WITH the wizard, which is what OPENDJ_GIVEN being public and secure buys: msiexec /i opendj.msi OPENDJ="..." and then click through the dialogs reaches this guard with the property intact. Every other directory the wizard can reach now carries its own evidence: browsing to the real installation proceeds, browsing to an empty directory is refused like the typo above. Evidence being a conjunct rather than an alternative is also what stops OPENDJ_GIVEN from being handed in as a bypass: a prefix as short as "C" does match the resolved path, but the directory still has to hold a server - and a target that holds one is one this guard was going to let through anyway. NOT OPENDJ_LEGACY_INSTALL is load-bearing rather than tidiness: a server in the legacy default is the headline upgrade path, resolved with nothing named and nothing recorded, and it is guard (2) that watches that one for relocation. Residual: the same decoy in the LEGACY default directory is not detectable when nothing is named. SetOpendjFromLegacyDir adopts it, so the resolved directory is neither the x64 default nor different from the legacy default, and neither guard fires. What would separate the two - "this tree is the product being upgraded" - is precisely what a registry-less 5.1.x never recorded, and refusing every registry-less legacy-default upgrade would refuse the documented main upgrade path with it. The install guide carries the workaround instead: pass OPENDJ explicitly when another OpenDJ tree sits in the legacy default directory - which guard (2) below has to let through, and does. (2) The upgrade would relocate. Passing OPENDJ=<somewhere else> over a detected installation is not a move: RemoveExistingProducts empties the old tree while the new one is installed elsewhere, so the instance data stays behind and any service registration keeps pointing at deleted files. Refuse when a location IS known - the value this package recorded, or a legacy default directory that really holds a server - and the requested one is not the same directory. Plain equality, not a substring test: a two-way "one contains the other" would tolerate the trailing backslash but also admit C:\opendj\v2 over a recorded C:\opendj, which is the very relocation being guarded against. Both sides carry the backslash by construction - CostFinalize resolves OPENDJ as a directory property, the registry value is the resolved [OPENDJ] this package wrote, and the legacy side is built above rather than read from a search. A hand-edited registry value without one is the accepted residual: it refuses, and the message says what to do. When OPENDJ is not given at all, the SetProperty actions above have already set it to the known location, so the comparison passes and nothing fires. The legacy branch of (2) carries one exception, and it is the workaround the residual of (1) prescribes: a named directory that HOLDS A SERVER is not a relocation. Without it the two guards contradicted each other - the only way past a decoy in the legacy default is to name the real directory, and naming it made this branch refuse, leaving that installation with no upgrade path at all. The exception is deliberately narrow: it needs both OPENDJ_GIVEN (the directory came from the command line, not from a search or a dialog) and a setup.bat found INSIDE that directory (OPENDJ_GIVEN_INSTALL holds its full path, so requiring the path to contain the resolved OPENDJ is both the "a server is there" test and the reason a hand-passed OPENDJ_GIVEN_INSTALL cannot stand in for one). Naming an empty or new directory is still refused, which is what a relocation looks like, so the case CI exercises - 5.1.x in the legacy default, OPENDJ=C:\opendj-b - refuses exactly as before. It does NOT extend to the registry branch: a location this package recorded is authoritative, and an administrator who wants to move an installation it knows about can uninstall it first. Residual: naming a directory that holds SOME OpenDJ tree while the product being upgraded lives elsewhere now proceeds and strands that installation's data. Directory evidence cannot tell the two apart at all, and between refusing the documented workaround and trusting an explicit instruction, the explicit instruction wins. Second residual, and the reason the exception asks for OPENDJ_GIVEN rather than the setup.bat evidence alone: in that decoy topology a wizard session that BROWSES to the real directory is still refused, because browsing sets no property this guard can read - InstallDirDlg runs long after the capture point above. It is the safe half of the wizard's behaviour (nothing is installed and nothing is removed) and the message names the command line, which now works in a wizard session too. Widening the exception to "the target holds a server, however it was chosen" would cover it, at the cost of letting a hand-passed OPENDJ_GIVEN_INSTALL stand alone. --> <!-- Both guards ask about properties that must be visible in the EXECUTE sequence, which is processed in the installer service: only public properties reach it, and only the secured ones reach it for a non-administrator. Every property named in the two conditions below is therefore public (WIX_UPGRADE_DETECTED, OPENDJ, OPENDJ_DEFAULT, OPENDJ_REG, OPENDJ_GIVEN, OPENDJ_LEGACY_INSTALL, OPENDJ_LEGACY_DEFAULT, OPENDJ_GIVEN_INSTALL), and build.yml asserts that against this file - a private property here reads as empty and silently inverts a guard, which is exactly what happened to the OpendjDirGiven flag these conditions used to carry. --> <CustomAction Id="RequireDirOnCustomUpgrade" Error="An existing OpenDJ installation was detected, but its location could not be determined: 32-bit packages recorded none, and the directory this installation would use holds no OpenDJ server. Select the existing installation directory in the wizard, or pass it explicitly: msiexec /i opendj.msi OPENDJ="C:\path\to\opendj" - and if a directory was given already, check it for a typo. If that installation is the default directory itself, the command line is the only way - the wizard cannot tell a confirmed default from an unchanged one."/> <CustomAction Id="RefuseRelocatingUpgrade" Error="OpenDJ is already installed in a different directory, and this installer cannot move an existing installation: its configuration, database and logs would be left behind. Install into the existing directory, or uninstall the existing OpenDJ first. If the directory that was detected holds an unrelated OpenDJ tree - a Zip installation, a copy, a decommissioned instance - and the server you are upgrading is elsewhere, pass that server's directory on the command line: msiexec /i opendj.msi OPENDJ="C:\path\to\opendj"."/> <!-- RemoveExistingProducts removes the old product's files during the sequence pass, long before the new product's InstallFiles executes in the script: a server still running as a service out of that tree would hold lib\*.jar while they are removed. Proceeding on a missed stop is NOT benign: the JVM opens jars without FILE_SHARE_DELETE (hardcoded, JDK-8224794 is Won't Fix), so locked jars cannot be renamed into Config.Msi and the nested uninstall degrades to delete-on-reboot entries naming the original paths. So: a best-effort immediate '"net.exe" stop' first, then a check that REFUSES the upgrade unless the service is provably gone or Stopped. That check polls instead of sampling the SCM once. net.exe returns the instant a control cannot be accepted (ERROR_SERVICE_CANNOT_ACCEPT_CTRL in any pending state), Return="ignore" eats the failure, and the state right then is StopPending or StartPending - neither of which is 'Running', so a single "-eq Running" test waved through precisely the cases the stop had not handled: an administrator who followed the install guide and ran 'net stop' in another window, or a service still starting, which service.c allows 300 s + a 30 s hint to finish while its JVM holds lib\*.jar. So StopPending is waited out - up to 90 s, matching the budget service.c itself allows a stop it is waiting on, and comfortably over the ~61 s in which the stop worker resolves to STOPPED or RUNNING - and every other state refuses immediately: once net.exe has returned, Running means the stop was rejected or denied, and StartPending means a JVM is on its way up. This package registers no service of its own - windows-service.bat does, when the administrator asks - so the service is not ours to own, only to insist is stopped; nothing here removes it, and an upgrade in place leaves it registered and working against the refreshed tree. Immediate actions impersonate the invoking user (a deferred elevated stop cannot be used: it is only written into the installation script, breaking the RemoveExistingProducts placement rule - ICE63 - and executing only after the old tree is gone), so the stop succeeds from an elevated console / SCCM / SYSTEM / CI but fails access-denied under the filtered token of a UAC double-click (swallowed by Return="ignore", which also covers the service simply not running). Querying the state needs no elevation, so the check works everywhere and turns the silent half-upgrade into a deterministic msiexec failure (error 1722 naming CheckServiceStopped; guidance in the install guide: stop the service or run the installer elevated). PowerShell instead of 'sc query | findstr': the exit code needs no parsing of localized output - and no square brackets in the command, ExeCommand is a Formatted field (the script blocks are safe there: a {} group holding no [property] reference is passed through unchanged, braces included). Full paths prevent resolving the exes from the working directory. Gated on the ImagePath evidence alone: any upgrade over a tree whose service is running has the locking problem, whether the installed product came from a WiX3-era package or from this one. --> <!-- Restart Manager would undo everything the actions below establish. It runs at InstallValidate - before any of them - maps the JVM holding lib\*.jar to the "OpenDJ Server" service, stops the service itself, and RESTARTS it at the end of the installation: the freshly upgraded server comes up against instance data upgrade.bat has not migrated yet, when the whole point of leaving the service Stopped is that the install guide requires upgrade.bat before the first start. It also disarms the starting-service refusal: RM stops a StartPending service before CheckServiceStopped gets to see it, waving through the very upgrade the check exists to refuse. msiexec /norestart is no defence - it suppresses reboots, not RM restarts. DisableShutdown is not a middle ground, and this is measured rather than reasoned: it keeps RM's detection ("The installer still uses the Restart Manager to detect files in use by applications") and was tried here for exactly that, but the restart is not part of what it disables. The headline upgrade scenario came out of that run with the service StartPending at the end of an otherwise successful install - the installer restarts what the RM session listed, whether RM stopped it or the guards did. So: Disable, and the detection goes with it. What that costs is stated plainly, because it is the reason CheckServerNotRunning below exists: with RM off the only in-use detection left is the legacy scan, which finds applications with a top-level window and so never a headless JVM. A server started by start-ds.bat has no service key either, so the ImagePath-gated pair below does not fire for it. The lock check covers both modes instead, and covers them in /quiet, where a FilesInUse dialog could not be shown anyway. What remains uncovered is some OTHER process holding payload files - the WiX3-era behaviour, which is where this package started. --> <Property Id="MSIRESTARTMANAGERCONTROL" Value="Disable"/> <CustomAction Id="StopServiceBeforeUpgrade" Directory="TARGETDIR" ExeCommand=""[System64Folder]net.exe" stop "OpenDJ Server"" Execute="immediate" Return="ignore"/> <CustomAction Id="CheckServiceStopped" Directory="TARGETDIR" ExeCommand=""[System64Folder]WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -Command "$deadline = (Get-Date).AddSeconds(90); do { $svc = Get-Service 'OpenDJ Server' -ErrorAction SilentlyContinue; if (-not $svc -or $svc.Status -eq 'Stopped') { exit 0 }; if ($svc.Status -ne 'StopPending') { exit 1 }; Start-Sleep -Seconds 2 } while ((Get-Date) -lt $deadline); exit 1"" Execute="immediate" Return="check"/> <!-- The same refusal for a server that is simply RUNNING, service or not. A server started by start-ds.bat holds lib\*.jar exactly as one started by the SCM does, and it is the mode this package ships by default: it registers no service, so an instance only ever becomes one when the administrator runs windows-service.bat. The evidence is the exclusive byte-range lock the server holds on locks\server.lock for as long as its JVM is up - the same probe .github/scripts/wait-server-stopped.ps1 uses in CI, and the reason it exists: #768 proved a zero exit code from stop-ds is not evidence that the JVM let go. Inline rather than dot-sourced from that script, because this runs before InstallFiles and the script is not part of the payload. The explicit Lock(0, 1) is required: a byte-range lock does not prevent opening the file, so a bare open would always succeed. No file, or no lock, and the upgrade proceeds. Sixty seconds of grace, the same budget wait-server-stopped.ps1 allows: the SCM reporting Stopped, or stop-ds returning, is not the moment the JVM releases the lock, and CheckServiceStopped hands over at exactly that moment. There is no pending state to read here, so a server that is genuinely up pays the whole minute before the refusal - which is the right way round, since the alternative is refusing an upgrade whose server did stop. A held byte-range lock and a sharing violation raise IOException, and only that is retried; anything else - an UnauthorizedAccessException on an ACL that does not let the lock file be read at all, which is a sibling of IOException rather than a subclass and so reaches the bare catch - means the question could not be asked rather than answered, and lets the upgrade proceed. That is the same fail-open the swallowed 'net stop' has, and for the same reason: an immediate action impersonates the invoking user, who under the filtered token of a UAC double-click holds whatever rights that user has on the old tree, not the ones the elevated install runs with. Which is why the open asks for READ and not read-write: LockFile needs only GENERIC_READ, while a read-write open of a file under [ProgramFilesFolder] is denied to a standard user outright - so a read-write probe took the fail-open path on every double-click upgrade of a Program Files install, running server or not, and the refusal promised at the top of this file only ever happened for the elevated console / SCCM / SYSTEM / CI modes. Read is what the default Program Files ACL grants Users, so the lock is testable in all of them now. (.github/scripts/wait-server-stopped.ps1 keeps its read-write open: it runs elevated over a workspace tree it owns, where the distinction cannot arise.) The file going away between the test and the open raises an IOException subclass, so it costs one sleep before Test-Path lets the next pass out at exit 0. The typed 'catch [System.IO.IOException]' is load-bearing and has to be a TYPE, not a name comparison: PowerShell wraps anything thrown out of a .NET method or constructor in a MethodInvocationException, so inside a plain catch-all $_.Exception is that wrapper and the real exception is its InnerException. An earlier version of this action asked $_.Exception.GetType().FullName -ne 'System.IO.IOException' and so took the fail-open branch on EVERY exception, including the held lock it exists to detect: the exit 1 was unreachable and the CI scenario that covers it passed on msiexec 3010 - the upgrade going through and deferring the locked jars to a reboot - instead of the 1603 a refusal produces. A typed catch matches on the inner exception; wait-server-stopped.ps1 has always had it, and the copy inlined here had lost it. build.yml now takes this command out of this file, resolves it the way msiexec resolves a Formatted field and runs it against a real held lock. [\[] and [\]] are the Formatted-field escapes for literal brackets: ExeCommand resolves [property] references, so an unescaped [System.IO.IOException] would be substituted away, leaving two catch-all blocks and a script that fails to parse - which this time would refuse every upgrade rather than none. --> <CustomAction Id="CheckServerNotRunning" Directory="TARGETDIR" ExeCommand=""[System64Folder]WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -Command "$lock = '[OPENDJ]locks\server.lock'; $deadline = (Get-Date).AddSeconds(60); do { if (-not (Test-Path $lock)) { exit 0 }; try { $fs = New-Object System.IO.FileStream($lock, 'Open', 'Read', 'ReadWrite'); try { $fs.Lock(0, 1); $fs.Unlock(0, 1); exit 0 } finally { $fs.Close() } } catch [\[]System.IO.IOException[\]] { Start-Sleep -Seconds 2 } catch { exit 0 } } while ((Get-Date) -lt $deadline); exit 1"" Execute="immediate" Return="check"/> <InstallExecuteSequence> <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))))"/> <Custom Action="RefuseRelocatingUpgrade" After="RequireDirOnCustomUpgrade" Condition="WIX_UPGRADE_DETECTED AND ((OPENDJ_REG AND (OPENDJ_REG ~<> OPENDJ)) OR (NOT OPENDJ_REG AND OPENDJ_LEGACY_INSTALL AND (OPENDJ_LEGACY_DEFAULT ~<> OPENDJ) AND NOT (OPENDJ_GIVEN AND (OPENDJ ~<< OPENDJ_GIVEN) AND (OPENDJ_GIVEN_INSTALL ~>< OPENDJ))))"/> <Custom Action="StopServiceBeforeUpgrade" Before="CheckServiceStopped" Condition="WIX_UPGRADE_DETECTED AND (OPENDJ_SVC_IMAGEPATH ~>< OPENDJ)"/> <Custom Action="CheckServiceStopped" Before="CheckServerNotRunning" Condition="WIX_UPGRADE_DETECTED AND (OPENDJ_SVC_IMAGEPATH ~>< OPENDJ)"/> <!-- No ImagePath gate: the lock says whether A server is running out of the tree about to be replaced, which is the question, and the service check above has already dealt with the SCM's half of it. --> <Custom Action="CheckServerNotRunning" Before="RemoveExistingProducts" Condition="WIX_UPGRADE_DETECTED"/> </InstallExecuteSequence> <!-- UI customization --> <WixVariable Id="WixUIBannerBmp" Value="opendjbanner.bmp" /> <WixVariable Id="WixUIDialogBmp" Value="opendjdialog.bmp" /> <WixVariable Id="WixUIBannerBmp" Value="opendjbanner.bmp"/> <WixVariable Id="WixUIDialogBmp" Value="opendjdialog.bmp"/> <!-- Upgrading --> <MajorUpgrade DowngradeErrorMessage="A newer version of $(var.name) is already installed."/> <Directory Id="TARGETDIR" Name="SourceDir"> <Directory Id="ProgramFilesFolder" Name="PFiles"> <Directory Id="OPENDJ" Name="$(var.name)"> <!-- x64 install location: C:\Program Files\OpenDJ --> <StandardDirectory Id="ProgramFiles64Folder"> <Directory Id="OPENDJ" Name="$(var.name)"> <Directory Id="OPENDJ_LIB" Name="lib"> <!-- Custom extension jars land here. The payload ships snmp-mib2605.jar in it today, so the harvest creates it anyway - the explicit component keeps the directory (and startup free of WARN_ADMIN_NO_EXTENSIONS_DIR) even if that jar ever leaves the package. --> <Directory Id="DIR_LIB_EXTENSIONS" Name="extensions"/> </Directory> <!-- Empty instance directories: the <Files> harvest ships files only (heat had -ke), so they must be created explicitly or the server cannot write its lock/pid files. --> <Directory Id="DIR_BAK" Name="bak"/> <Directory Id="DIR_CHANGELOGDB" Name="changelogDb"/> <Directory Id="DIR_CLASSES" Name="classes"/> <Directory Id="DIR_DB" Name="db"/> <Directory Id="DIR_IMPORTTMP" Name="import-tmp"/> <Directory Id="DIR_LDIF" Name="ldif"/> <Directory Id="DIR_LOCKS" Name="locks"/> <Directory Id="DIR_LOGS" Name="logs"/> <Directory Id="DIR_TMP" Name="tmp"/> <Directory Id="DIR_TEMPLATE" Name="template"> <Directory Id="DIR_TPL_BAK" Name="bak"/> <Directory Id="DIR_TPL_CHANGELOGDB" Name="changelogDb"/> <Directory Id="DIR_TPL_CLASSES" Name="classes"/> <Directory Id="DIR_TPL_DB" Name="db"/> <Directory Id="DIR_TPL_IMPORTTMP" Name="import-tmp"/> <Directory Id="DIR_TPL_LDIF" Name="ldif"/> <Directory Id="DIR_TPL_LOCKS" Name="locks"/> <Directory Id="DIR_TPL_LOGS" Name="logs"/> </Directory> </Directory> </Directory> </StandardDirectory> <Feature Id="All" Title="Server and tools" Level="1" ConfigurableDirectory="OPENDJ"> <ComponentGroupRef Id="all"/> <ComponentGroup Id="OpenDJEmptyDirs"> <Component Id="CDirBak" Directory="DIR_BAK" Guid="7CA85932-ADB2-4089-B3BF-0E2DD4AC4E66"><CreateFolder/></Component> <Component Id="CDirChangelogDb" Directory="DIR_CHANGELOGDB" Guid="E9C21C67-E4B6-482D-8C86-593EC14F9152"><CreateFolder/></Component> <Component Id="CDirClasses" Directory="DIR_CLASSES" Guid="7656FD50-3581-4394-9818-FF1A0E87ADB3"><CreateFolder/></Component> <Component Id="CDirDb" Directory="DIR_DB" Guid="F0E4603C-6402-484C-A274-217BA489CF54"><CreateFolder/></Component> <Component Id="CDirImportTmp" Directory="DIR_IMPORTTMP" Guid="EA738740-FA59-474D-93F0-3DD45366AB46"><CreateFolder/></Component> <Component Id="CDirLdif" Directory="DIR_LDIF" Guid="77580438-E14F-4268-B947-D5142F92AD6C"><CreateFolder/></Component> <Component Id="CDirLocks" Directory="DIR_LOCKS" Guid="078CF9AE-1510-4B29-B719-02FAAE85D3AC"><CreateFolder/></Component> <Component Id="CDirLogs" Directory="DIR_LOGS" Guid="2FF1ECAD-2E64-4629-92A5-4CA635DA3959"><CreateFolder/></Component> <Component Id="CDirTmp" Directory="DIR_TMP" Guid="DECC581E-2F76-4C89-9F18-02EAD2F26173"><CreateFolder/></Component> <Component Id="CDirLibExtensions" Directory="DIR_LIB_EXTENSIONS" Guid="A2486768-4BD0-47B9-818B-8BDCA17F08CE"><CreateFolder/></Component> <Component Id="CDirTplBak" Directory="DIR_TPL_BAK" Guid="EB4F9340-5C24-42AC-A996-C95E7DC3C7A3"><CreateFolder/></Component> <Component Id="CDirTplChangelogDb" Directory="DIR_TPL_CHANGELOGDB" Guid="B0598916-0D2C-4497-900C-5A534FADA852"><CreateFolder/></Component> <Component Id="CDirTplClasses" Directory="DIR_TPL_CLASSES" Guid="C66CC1A0-996E-4BE5-9CFB-57A32F2C4F14"><CreateFolder/></Component> <Component Id="CDirTplDb" Directory="DIR_TPL_DB" Guid="97CD56F6-402F-4721-AF1C-A23DA232D7E7"><CreateFolder/></Component> <Component Id="CDirTplImportTmp" Directory="DIR_TPL_IMPORTTMP" Guid="4888F735-CFF5-426F-8593-1592F45C076D"><CreateFolder/></Component> <Component Id="CDirTplLdif" Directory="DIR_TPL_LDIF" Guid="AA9F70A5-0F88-4A7C-8457-81201CC619BE"><CreateFolder/></Component> <Component Id="CDirTplLocks" Directory="DIR_TPL_LOCKS" Guid="93F3CFA4-207C-4F08-9413-1831E7B1E50C"><CreateFolder/></Component> <Component Id="CDirTplLogs" Directory="DIR_TPL_LOGS" Guid="7A4ABB8C-01D4-43BA-B85E-B0FAC14CBE75"><CreateFolder/></Component> </ComponentGroup> <!-- Remember the install location so upgrades land in the same directory. --> <Component Id="InstallDirRegistry" Directory="OPENDJ"> <RegistryValue Root="HKLM" Key="SOFTWARE\$(var.name)" Name="InstallDir" Type="string" Value="[OPENDJ]" KeyPath="yes"/> </Component> <!-- Harvest the payload. Exclusions are already applied during staging (Ant), so each <Files> needs only the Include attribute (WiX 5 requires it; no Exclude here): stagingRoot has everything except lib; stagingLib has lib, opendj_service.exe included - the package lays the wrapper down like any other file and registers no service of its own; windows-service.bat does that when the administrator asks. --> <ComponentGroup Id="OpenDJRoot" Directory="OPENDJ"> <Files Include="$(var.stagingRoot)\**"/> </ComponentGroup> <ComponentGroup Id="OpenDJLib" Directory="OPENDJ_LIB"> <Files Include="$(var.stagingLib)\**"/> </ComponentGroup> <Feature Id="All" Title="Server and tools" ConfigurableDirectory="OPENDJ"> <ComponentRef Id="InstallDirRegistry"/> <ComponentGroupRef Id="OpenDJRoot"/> <ComponentGroupRef Id="OpenDJLib"/> <ComponentGroupRef Id="OpenDJEmptyDirs"/> </Feature> <!-- User interface --> <Property Id="WIXUI_INSTALLDIR" Value="OPENDJ"/> <UI Id="OpenDJ_Install"> <UIRef Id="WixUI_InstallDir"/> <UIRef Id="WixUI_ErrorProgressText"/> <!-- Don't show the license agreement in the install, just in setup --> <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">NOT Installed</Publish> <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Order="2" Value="WelcomeDlg">1</Publish> <!-- WixUI_InstallDir with LicenseAgreementDlg skipped: the CDDL license is shown and accepted by setup, and the stock dialog would otherwise display WiX's placeholder License.rtf (Lorem ipsum) because WixUILicenseRtf is a build-time default. --> <ui:WixUI Id="WixUI_InstallDir" InstallDirectory="OPENDJ"/> <UIRef Id="WixUI_ErrorProgressText"/> <UI> <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="99" Condition="NOT Installed"/> <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="99" Condition="NOT Installed"/> </UI> </Product> </Package> </Wix> opendj-packages/opendj-msi/pom.xml
@@ -13,6 +13,7 @@ information: "Portions Copyright [year] [name of copyright owner]". Copyright 2015-2016 ForgeRock AS. Portions Copyright 2026 3A Systems, LLC. --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> @@ -32,63 +33,19 @@ This module contains configuration and generic plugin call to build OpenDJ MSI packages. </description> <profiles> <profile> <id>/usr/bin/wine</id> <activation> <os><family>unix</family></os> <file><exists>/usr/bin/wine</exists></file> </activation> <modules> <module>opendj-msi-standard</module> </modules> <properties> <exec.heat>/usr/bin/wine</exec.heat><param.heat>${project.build.directory}/wix/heat.exe</param.heat> <exec.candle>/usr/bin/wine</exec.candle><param.candle>${project.build.directory}/wix/candle.exe</param.candle> <exec.light>/usr/bin/wine</exec.light><param.light>${project.build.directory}/wix/light.exe</param.light> </properties> </profile> <profile> <id>/usr/local/bin/wine</id> <activation> <os><family>unix</family></os> <file><exists>/usr/local/bin/wine</exists></file> </activation> <modules> <module>opendj-msi-standard</module> </modules> <properties> <exec.heat>/usr/local/bin/wine</exec.heat><param.heat>${project.build.directory}/wix/heat.exe</param.heat> <exec.candle>/usr/local/bin/wine</exec.candle><param.candle>${project.build.directory}/wix/candle.exe</param.candle> <exec.light>/usr/local/bin/wine</exec.light><param.light>${project.build.directory}/wix/light.exe</param.light> </properties> </profile> <profile> <id>/opt/local/bin/wine</id> <activation> <os><family>unix</family></os> <file><exists>/opt/local/bin/wine</exists></file> </activation> <modules> <module>opendj-msi-standard</module> </modules> <properties> <exec.heat>/opt/local/bin/wine</exec.heat><param.heat>${project.build.directory}/wix/heat.exe</param.heat> <exec.candle>/opt/local/bin/wine</exec.candle><param.candle>${project.build.directory}/wix/candle.exe</param.candle> <exec.light>/opt/local/bin/wine</exec.light><param.light>${project.build.directory}/wix/light.exe</param.light> </properties> </profile> <profile> <id>windows</id> <activation><os><family>windows</family></os></activation> <modules> <module>opendj-msi-standard</module> </modules> <properties> <exec.heat>${project.build.directory}\wix\heat.exe</exec.heat><param.heat /> <exec.candle>${project.build.directory}\wix\candle.exe</exec.candle><param.candle /> <exec.light>${project.build.directory}\wix\light.exe</exec.light><param.light /> </properties> </profile> </profiles> <!-- Not published: the MSI is distributed through GitHub Releases and the Package/Deploy artifact, and the Maven coordinate stays at its last published version (5.1.2). Both MSI poms are nevertheless part of every reactor, because that is the only way the release plugin keeps rewriting their versions - see the note in opendj-packages - and a reactor module is published unless something takes it out. What takes these two out is excludeArtifacts on central-publishing-maven-plugin in the root pom, where the reason it cannot be a maven.deploy.skip here is written down. --> <!-- The MSI is built with the WiX v5 .NET tool on Windows only (WiX's build task P/Invokes msi.dll), which is what the distribution-windows-msi profile inside opendj-msi-standard gates. The module list here is unconditional: everywhere else the module simply builds nothing. --> <modules> <module>opendj-msi-standard</module> </modules> </project> opendj-packages/pom.xml
@@ -13,6 +13,7 @@ information: "Portions Copyright [year] [name of copyright owner]". Copyright 2015-2016 ForgeRock AS. Portions Copyright 2026 3A Systems, LLC. --> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> @@ -79,11 +80,24 @@ <os><family>windows</family></os> </activation> <modules> <module>opendj-msi</module> <module>opendj-msi</module> <module>opendj-docker</module> </modules> </profile> </profiles> <!-- opendj-msi stays in every module list, including the ones that cannot build an MSI: maven-release-plugin only rewrites the versions of ${reactorProjects}, and release.yml runs release:prepare on ubuntu-latest with no -P. Leaving the module out of the Linux reactor - as an earlier revision of this PR did, gating it on Windows plus wix.exe - freezes opendj-msi/pom.xml and opendj-msi-standard/pom.xml at the parent version they happen to carry. Maven does not fail on that skew; it resolves the parent from the repository instead, so the MSI module keeps building as the old version: it would unpack the previous line's published snapshot instead of the server zip built beside it, name the package after that version and hand parse-version the wrong numbers for the ProductVersion the upgrade guards key on. What actually cannot run outside Windows is the wix invocation, so that is what the toolchain profile in opendj-msi-standard gates - the module itself is a no-op pom everywhere else. --> <build><finalName>${project.groupId}.${project.artifactId}</finalName> <pluginManagement> opendj-server-legacy/lib/launcher_administrator.exeBinary files differ
opendj-server-legacy/lib/opendj_service.exeBinary files differ
opendj-server-legacy/lib/winlauncher.exeBinary files differ
opendj-server-legacy/pom.xml
@@ -1403,7 +1403,6 @@ <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.3.2</version> <executions> <execution> <id>mib-generation</id> opendj-server-legacy/src/build-tools/windows/Makefile
@@ -13,6 +13,7 @@ # # Copyright 2008 Sun Microsystems, Inc. # Portions Copyright 2011 ForgeRock AS. # Portions Copyright 2026 3A Systems, LLC. # # This is the Makefile than can be used to generate the executables @@ -36,10 +37,14 @@ SERVICE_PROGNAME=opendj_service.exe LAUNCHER_ADMINISTRATOR_PROGNAME=launcher_administrator.exe WINLAUNCHER_PROGNAME=winlauncher.exe LINKER=link -nologo /machine:x86 # /Brepro makes the outputs reproducible (content-hash PE timestamps instead of the # build time). The Package/Deploy workflow commits these binaries back to the branch # whenever their bytes differ from the committed ones; without /Brepro every build # would differ and it would commit on every push. LINKER=link -nologo /machine:x86 /Brepro LIBS=advapi32.lib CFLAGS= -D_WINDOWS -nologo -W3 -O2 CFLAGS= -D_WINDOWS -nologo -W3 -O2 /Brepro RC=rc MC=mc MT=mt opendj-server-legacy/src/build-tools/windows/service.c
@@ -143,7 +143,11 @@ NULL, // ServicesActive database accessRights // desired rights ); if (scm == NULL) // *scm, not scm: the latter is the address of the caller's variable and is never // NULL, so the failure went unreported and callers saw SERVICE_RETURN_OK with a NULL // handle. The outcome was still an error - EnumServicesStatus and friends reject the // NULL handle - but one attributed to the wrong call and without this message. if (*scm == NULL) { debugError("Failed to open the Service Control Manager. Last error = %d", GetLastError()); @@ -970,8 +974,15 @@ // product. All commands are supposed to be unique because they have // the instance dir as parameter. // // The functions returns SERVICE_RETURN_OK if we could get a service name // and SERVICE_RETURN_ERROR otherwise. // The functions returns SERVICE_RETURN_OK if we could get a service name, // SERVICE_LIST_UNAVAILABLE if the list of services could not be read at all - // which callers must not read as "no such service" - SERVICE_LIST_PARTIAL when // the list was read but at least one entry could not be examined and nothing // matched, which callers must not read as "no such service" either, and // SERVICE_RETURN_ERROR when the list was read in full and held no match. // One residual stays on SERVICE_RETURN_ERROR: an entry that matches but whose // name does not fit MAX_SERVICE_NAME. Windows caps a service name at 256 // characters, the same bound, so that is unreachable rather than tolerated. // The serviceName buffer must be allocated OUTSIDE the function and its // minimum size must be of 256 (the maximum string length of a Service Name). // ---------------------------------------------------- @@ -997,6 +1008,7 @@ if (returnValue == SERVICE_RETURN_OK) { int i; int unreadable = 0; returnValue = SERVICE_RETURN_ERROR; if (nbServices > 0) { @@ -1023,12 +1035,31 @@ break; } } else { // getServiceList() leaves cmdToRun NULL for an entry whose // configuration it could not read - QueryServiceConfig denied, or the // service deleted between the enumeration and the read - and the // command line is the only thing this search matches on. Such an entry // can neither be matched nor ruled out, so remember that the sweep was // incomplete rather than let it pass as a clean "no match". unreadable++; } } free (serviceList); } if ((returnValue != SERVICE_RETURN_OK) && (unreadable > 0)) { returnValue = SERVICE_LIST_PARTIAL; debug("getServiceName: no match, but %d service(s) could not be read.", unreadable); } } else { // Distinct from "no service matched": callers such as removeService must // report an error instead of concluding the service does not exist. returnValue = SERVICE_LIST_UNAVAILABLE; debug("getServiceName: could not get service list."); } @@ -2438,8 +2469,30 @@ returnCode = 0; debug("Service '%s' is enabled.", serviceName); } else if (code == SERVICE_LIST_UNAVAILABLE) { // The SCM could not be enumerated, so whether a service is registered // is simply unknown; say so instead of answering "disabled". This only // reaches --serviceState, which prints the error message and exits 2: // the java callers of ConfigureWindowsService.serviceState() all test // for SERVICE_STATE_ENABLED, so an unknown state still reads as "not // enabled" to the uninstaller, the control panel and // isRunningAsWindowsService - which is what the previous DISABLED answer // already did for them, so nothing changes here except the message. The // residual, unchanged and pre-existing: an uninstall that hits an // unreadable SCM skips --disableService and leaves a registered service // pointing at the tree it just removed. returnCode = 2; debug("Could not determine the state of the service: no service list."); } else { // SERVICE_LIST_PARTIAL lands here too, on purpose: a state question whose // answer is only ever compared against "enabled" reads the same either // way, while a --serviceState that printed an error whenever an unrelated // service denies QueryServiceConfig would be a worse answer than the // "disabled" it replaces. removeService() below does treat it as an // error, because there the unproven absence is acted on. returnCode = 1; debug("Service '%s' is disabled.", serviceName); } @@ -2469,11 +2522,21 @@ debug("Removing service with name %s.", serviceName); if (code != SERVICE_IN_USE) if (code == SERVICE_NOT_IN_USE) { returnCode = 1; debug("Service does not exist."); } else if (code != SERVICE_IN_USE) { // serviceNameInUse() could not enumerate the SCM: "the service does not // exist" cannot be proven, and exit code 1 is what the callers read as // SERVICE_ALREADY_DISABLED - the lie removeService() stopped telling when // its own enumeration fails. Report an error instead, for both the 'remove' // and the 'cleanup' subcommand. returnCode = 3; debug("Could not determine whether the service exists."); } else { code = removeServiceFromScm(serviceName); @@ -2525,6 +2588,26 @@ { returnCode = removeServiceWithServiceName(serviceName); } else if ((code == SERVICE_LIST_UNAVAILABLE) || (code == SERVICE_LIST_PARTIAL)) { // The SCM could not be enumerated, or an entry in it could not be read: // "the service does not exist" cannot be proven, so report an error // instead of the "already disabled" success the callers map exit code 1 // to. Deliberately not mirrored in serviceState() above, which keeps // answering "disabled" on a partial read: all three java callers of // ConfigureWindowsService.serviceState() only ever test for // SERVICE_STATE_ENABLED, so the answer would not change for any of them, // and --serviceState would start printing an error on every box where an // unrelated service denies QueryServiceConfig to the invoking user. // Removal is the path where an unproven absence destroys something. // What changes in practice is narrow: the uninstaller asks serviceState() // first and never calls this when nothing is registered, so the new error // reaches a hand-run --disableService or 'cleanup' on an instance whose // service really is absent - which now says "could not tell" instead of // "already disabled", on a box where a service denied its config to us. returnCode = 3; } else { returnCode = 1; @@ -2532,7 +2615,14 @@ } else { returnCode = 2; // createServiceBinPath() failed - GetModuleFileName() truncated at MAX_PATH, // or "<exe>" start "<instanceDir>" did not fit COMMAND_SIZE. Hard errors // both, and this function's contract puts those on 3: exit code 2 is // WARN_WINDOWS_SERVICE_MARKED_FOR_DELETION, which InstallerHelper // .disableWindowsService() lets through as a warning, so an install path // long enough to hit either limit reported a deletion that never happened // and the uninstall carried on. returnCode = 3; } debug("removeService returning %d.", returnCode); opendj-server-legacy/src/build-tools/windows/service.h
@@ -79,7 +79,13 @@ typedef enum { SERVICE_RETURN_OK, SERVICE_RETURN_ERROR, SERVICE_IN_USE, SERVICE_NOT_IN_USE, DUPLICATED_SERVICE_NAME, SERVICE_ALREADY_EXISTS, SERVICE_MARKED_FOR_DELETION SERVICE_MARKED_FOR_DELETION, SERVICE_LIST_UNAVAILABLE, // The service list was read, no entry matched, and at least one entry could // not be examined - so the non-match does not prove the service is absent. // Between SERVICE_LIST_UNAVAILABLE ("could not be read at all") and // SERVICE_RETURN_ERROR ("read, and held no match"), which are not the only // two answers the SCM can give. SERVICE_LIST_PARTIAL } ServiceReturnCode; pom.xml
@@ -341,6 +341,22 @@ <publishingServerId>ossrh</publishingServerId> <autoPublish>true</autoPublish> <waitMaxTime>5400</waitMaxTime> <!-- The MSI modules stay in every reactor so that maven-release-plugin keeps rewriting their versions (see the note in opendj-packages/pom.xml), but the installer they build is distributed through GitHub Releases and the Package/Deploy artifact, not Central - and only a Windows job can build it, so the Linux release that publishes to Central has nothing but their poms to offer. Excluded here rather than with maven.deploy.skip in the modules: the extension above unbinds maven-deploy-plugin and publishes through this plugin's own goal, which that property does not gate, so the poms would ship to Central with no .msi beside them - a resolvable coordinate whose artifact does not exist, which is worse for a consumer than a version that was never published. The match is against the bare artifactId; a groupId:artifactId entry would silently never match. --> <excludeArtifacts> <excludeArtifact>opendj-msi</excludeArtifact> <excludeArtifact>opendj-msi-standard</excludeArtifact> </excludeArtifacts> </configuration> </plugin> </plugins> @@ -619,6 +635,12 @@ </executions> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.6.3</version> </plugin> <!-- Retrieve the build timestamp and SCM revision number --> <plugin> <groupId>org.codehaus.mojo</groupId>