diff --git a/.gitattributes b/.gitattributes index e195b512dd..849c1c0aed 100644 --- a/.gitattributes +++ b/.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 diff --git a/.github/scripts/wait-server-stopped.ps1 b/.github/scripts/wait-server-stopped.ps1 new file mode 100644 index 0000000000..9e7814a89d --- /dev/null +++ b/.github/scripts/wait-server-stopped.ps1 @@ -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" +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f92f3b7d43..fb7aebb3da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,8 @@ concurrency: cancel-in-progress: true # Nothing in this workflow writes back to the repository: the docker jobs push to -# the local registry service, not to a remote one. The docker jobs additionally get +# the local registry service, not to a remote one, and the other jobs only +# publish artifacts through the actions API. The docker jobs additionally get # security-events: write to upload Trivy scan results to code scanning. permissions: contents: read @@ -45,19 +46,12 @@ jobs: - { 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 @@ -87,20 +81,64 @@ jobs: 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 - name: Build with Maven timeout-minutes: 180 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 --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: | @@ -293,21 +331,7 @@ jobs: - 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 @@ -367,14 +391,6 @@ jobs: 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: @@ -663,10 +679,71 @@ jobs: if-no-files-found: warn retention-days: 90 + # 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: @@ -676,41 +753,704 @@ jobs: 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)(.*?)') + if (-not $sequence.Success) { throw "package.wxs has no InstallExecuteSequence" } + foreach ($action in @('RequireDirOnCustomUpgrade', 'RefuseRelocatingUpgrade')) { + $scheduled = [regex]::Match($sequence.Groups[1].Value, '(?s)') + 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 (']*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 ']*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) 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 + with: + name: windows-latest-11 + - name: Set up Java + uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'zulu' + - 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" } + # 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: 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\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 (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 after upgrade failed: $LASTEXITCODE" } + net stop "OpenDJ Server" + 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 + 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 + # 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 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 18d78a0753..2195c7e14f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -26,36 +26,136 @@ concurrency: # 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. + 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 eight 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 +214,25 @@ jobs: 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 +307,4 @@ jobs: 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 + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab73d843f8..ced2358746 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,18 +45,11 @@ jobs: 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 @@ jobs: 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 @@ jobs: 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,79 @@ jobs: 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 - + # a path drift would otherwise yield "Could not find the selected project in + # the reactor", swallowed by the job's continue-on-error. + 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' diff --git a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc index f976103528..cfa49751c3 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc @@ -640,14 +640,16 @@ opendj 0:off 1:off 2:on 3:on 4:on 5:on 6:off [#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): @@ -658,9 +660,11 @@ The installer does not check for Java. If your default Java environment is not a 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] @@ -668,7 +672,7 @@ By default the package installs under `C:\Program Files\OpenDJ` (the 32-bit inst 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] @@ -676,6 +680,8 @@ C:\path\to\opendj> setup.bat --cli 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. ==== diff --git a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc index 17e0413635..7f5e201190 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc @@ -164,7 +164,7 @@ Removing the package does not remove your data or configuration. You must remove ==== 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:\path\to\opendj\bat> windows-service.bat --disableService 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. ==== diff --git a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc index 5077ecf8ff..5c5ac0e9ca 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc @@ -257,25 +257,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: + @@ -285,14 +286,12 @@ C:\> msiexec /i opendj-{opendj-version}.msi /quiet OPENDJ="C:\path\to\opendj" 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" ---- ==== diff --git a/opendj-packages/opendj-msi/opendj-msi-standard/pom.xml b/opendj-packages/opendj-msi/opendj-msi-standard/pom.xml index 807215df25..650cefdf69 100644 --- a/opendj-packages/opendj-msi/opendj-msi-standard/pom.xml +++ b/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 --> 4.0.0 @@ -29,265 +29,155 @@ OpenDJ MSI Standard Package - 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. ${basedir}/resources/msi ${project.build.directory}/${product.name.lowercase} + + ${project.build.directory}/msi-staging + ${project.build.directory}/msi-staging-lib + ${project.build.directory}/${product.name.lowercase}-${project.version}.msi - - - + + + + + + + distribution-windows-msi + + windows + ${env.USERPROFILE}/.dotnet/tools/wix.exe + ${project.groupId}.${project.artifactId} + - org.codehaus.mojo - build-helper-maven-plugin + org.apache.maven.plugins + maven-dependency-plugin - - org.openidentityplatform.commons - maven-external-dependency-plugin - false - - - ${project.build.directory}/dependencies/ - - false - true - false - - - openidentityplatform.org - wixtoolset - 3.11.1 - zip - - https://github.com/wixtoolset/wix3/releases/download/wix3111rtm/wix311-binaries.zip - - false - - - openidentityplatform.org - winetricks - LAST - sh - - https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks - - false - - - - - - clean-external-dependencies - clean - - clean-external - - - - resolve-install-external-dependencies - process-resources - - resolve-external - install-external - - - - deploy-external-dependencies - deploy - - deploy-external - - - - - + + org.apache.maven.plugins - maven-dependency-plugin + maven-antrun-plugin - unpack-wix + stage-msi-payload package - unpack + run - - - openidentityplatform.org - wixtoolset - 3.11.1 - zip - - - - ${project.build.directory}/wix - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + - unpack-winetricks + wix-build-msi package - copy + exec - - - openidentityplatform.org - winetricks - LAST - sh - - - - ${project.build.directory}/winetricks - + wix + ${project.build.directory} + + build + ${msi.resources}/package.wxs + -archx64 + -extWixToolset.UI.wixext + -bindpath${msi.resources} + -dname=${product.name} + -dmajor=${parsedVersion.majorVersion} + -dminor=${parsedVersion.minorVersion} + -dpoint=${parsedVersion.incrementalVersion} + -dstagingRoot=${staging.root} + -dstagingLib=${staging.lib} + -o${msi.file} + + + - org.apache.maven.plugins - maven-antrun-plugin + org.codehaus.mojo + build-helper-maven-plugin - build-msi-package-prepare + attach-msi package - run + attach-artifact - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ------------------- ${exec.heat} ${param.heat} ------------------- - - - - - - - - - - - - - - - - ------------------- ${exec.candle} ${param.candle} ------------------- - - - - - - - - - - - - - ------------------- ${exec.light} ${param.light} ------------------- - - - - - - - - - - - - - - - - - - + + ${msi.file}msi + - \ No newline at end of file + + + diff --git a/opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs b/opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs index f77dda7a14..1e72559544 100644 --- a/opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs +++ b/opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs @@ -15,45 +15,544 @@ ! Copyright 2013-2016 ForgeRock AS. ! Portions Copyright 2018-2026 3A Systems, LLC ! --> - - - - - - - + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + 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= 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. --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + - - - - - - - NOT Installed - 1 + + + + + + - + diff --git a/opendj-packages/opendj-msi/pom.xml b/opendj-packages/opendj-msi/pom.xml index 748456f462..33d0c3c13d 100644 --- a/opendj-packages/opendj-msi/pom.xml +++ b/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. --> 4.0.0 @@ -32,63 +33,21 @@ This module contains configuration and generic plugin call to build OpenDJ MSI packages. - - - /usr/bin/wine - - unix - /usr/bin/wine - - - opendj-msi-standard - - - /usr/bin/wine${project.build.directory}/wix/heat.exe - /usr/bin/wine${project.build.directory}/wix/candle.exe - /usr/bin/wine${project.build.directory}/wix/light.exe - - - - /usr/local/bin/wine - - unix - /usr/local/bin/wine - - - opendj-msi-standard - - - /usr/local/bin/wine${project.build.directory}/wix/heat.exe - /usr/local/bin/wine${project.build.directory}/wix/candle.exe - /usr/local/bin/wine${project.build.directory}/wix/light.exe - - - - /opt/local/bin/wine - - unix - /opt/local/bin/wine - - - opendj-msi-standard - - - /opt/local/bin/wine${project.build.directory}/wix/heat.exe - /opt/local/bin/wine${project.build.directory}/wix/candle.exe - /opt/local/bin/wine${project.build.directory}/wix/light.exe - - - - windows - windows - - opendj-msi-standard - - - ${project.build.directory}\wix\heat.exe - ${project.build.directory}\wix\candle.exe - ${project.build.directory}\wix\light.exe - - - + + + true + + + + + opendj-msi-standard + diff --git a/opendj-packages/pom.xml b/opendj-packages/pom.xml index d2148a7768..8a5343d87f 100644 --- a/opendj-packages/pom.xml +++ b/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. --> 4.0.0 @@ -79,11 +80,24 @@ windows - opendj-msi + opendj-msi opendj-docker + + ${project.groupId}.${project.artifactId} diff --git a/opendj-server-legacy/lib/launcher_administrator.exe b/opendj-server-legacy/lib/launcher_administrator.exe index 50b885811d..12649a9dd1 100644 Binary files a/opendj-server-legacy/lib/launcher_administrator.exe and b/opendj-server-legacy/lib/launcher_administrator.exe differ diff --git a/opendj-server-legacy/lib/opendj_service.exe b/opendj-server-legacy/lib/opendj_service.exe index 0b458d8f72..c4997a47e6 100644 Binary files a/opendj-server-legacy/lib/opendj_service.exe and b/opendj-server-legacy/lib/opendj_service.exe differ diff --git a/opendj-server-legacy/lib/winlauncher.exe b/opendj-server-legacy/lib/winlauncher.exe index 04f210cb64..a80a4ee9bc 100644 Binary files a/opendj-server-legacy/lib/winlauncher.exe and b/opendj-server-legacy/lib/winlauncher.exe differ diff --git a/opendj-server-legacy/pom.xml b/opendj-server-legacy/pom.xml index 5f5d8d2eeb..7a0daadcb3 100644 --- a/opendj-server-legacy/pom.xml +++ b/opendj-server-legacy/pom.xml @@ -1403,7 +1403,6 @@ org.codehaus.mojo exec-maven-plugin - 1.3.2 mib-generation diff --git a/opendj-server-legacy/src/build-tools/windows/Makefile b/opendj-server-legacy/src/build-tools/windows/Makefile index c1da51e953..2f73f590a1 100644 --- a/opendj-server-legacy/src/build-tools/windows/Makefile +++ b/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 @@ CC=cl 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 diff --git a/opendj-server-legacy/src/build-tools/windows/service.c b/opendj-server-legacy/src/build-tools/windows/service.c index 3f6e7891b3..21c75c06e8 100644 --- a/opendj-server-legacy/src/build-tools/windows/service.c +++ b/opendj-server-legacy/src/build-tools/windows/service.c @@ -143,7 +143,11 @@ ServiceReturnCode openScm(DWORD accessRights, SC_HANDLE *scm) 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,10 @@ ServiceReturnCode createServiceBinPath(char* serviceBinPath) // 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" - and SERVICE_RETURN_ERROR +// when the list was read and held no match. // 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). // ---------------------------------------------------- @@ -1029,6 +1035,9 @@ ServiceReturnCode getServiceName(char* cmdToRun, char* serviceName) } 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,6 +2447,22 @@ int serviceState() 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 { returnCode = 1; @@ -2525,6 +2550,13 @@ int removeService() { returnCode = removeServiceWithServiceName(serviceName); } + else if (code == SERVICE_LIST_UNAVAILABLE) + { + // The SCM could not be enumerated: "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. + returnCode = 3; + } else { returnCode = 1; diff --git a/opendj-server-legacy/src/build-tools/windows/service.h b/opendj-server-legacy/src/build-tools/windows/service.h index 5b3330ecb7..754c87c148 100644 --- a/opendj-server-legacy/src/build-tools/windows/service.h +++ b/opendj-server-legacy/src/build-tools/windows/service.h @@ -79,7 +79,7 @@ typedef struct { 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 } ServiceReturnCode; diff --git a/pom.xml b/pom.xml index 23cdba7234..b32d68425e 100644 --- a/pom.xml +++ b/pom.xml @@ -619,6 +619,12 @@ + + org.codehaus.mojo + exec-maven-plugin + 3.6.3 + + org.codehaus.mojo