From 94823dbc53a839ef282609746efaa51938af9afd Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 08 Jul 2026 16:04:58 +0000
Subject: [PATCH] Add CI install-test for the Windows MSI + document MSI install/upgrade/uninstall (#664)

---
 opendj-server-legacy/resource/bin/start-ds.bat                               |   10 +-
 opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc   |   44 +++++++++++
 opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc |   29 +++++++
 opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs     |    2 
 opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc   |   47 +++++++++++
 .github/workflows/build.yml                                                  |   52 +++++++++++++
 opendj-server-legacy/resource/bin/_script-util.bat                           |   19 ++--
 opendj-server-legacy/resource/bin/setcp.bat                                  |   11 +-
 8 files changed, 195 insertions(+), 19 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ec8d21f..8d77743 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -562,3 +562,55 @@
             *.jmeter.out
           if-no-files-found: warn
           retention-days: 90
+
+  test-msi:
+    needs: build-maven
+    runs-on: 'windows-latest'
+    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 MSI (silent)
+        shell: pwsh
+        run: |
+          $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+          if (-not $msi) { throw "MSI not found in the windows-latest-11 artifact" }
+          Write-Host "MSI: $msi"
+          # No OPENDJ property: use the default install directory (a path with spaces),
+          # which the server scripts must handle.
+          $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v install.log"
+          if ($p.ExitCode -ne 0) { Get-Content install.log -Tail 80; throw "msiexec /i failed: $($p.ExitCode)" }
+          $root = @("C:\Program Files (x86)\OpenDJ","C:\Program Files\OpenDJ") | Where-Object { Test-Path "$_\setup.bat" } | Select-Object -First 1
+          if (-not $root) { Get-Content install.log -Tail 80; throw "OpenDJ install root with setup.bat not found" }
+          Write-Host "Installed to $root"
+          "OPENDJ_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append
+      - name: Setup and start/stop the Windows service
+        shell: pwsh
+        run: |
+          $root = $env:OPENDJ_ROOT
+          $env:OPENDJ_JAVA_ARGS = "-server -Xmx512m"
+          & "$root\setup.bat" -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --addBaseEntry --cli --acceptLicense --no-prompt --doNotStart
+          if ($LASTEXITCODE -ne 0) { throw "setup.bat failed: $LASTEXITCODE" }
+          & "$root\bat\windows-service.bat" --enableService
+          if ($LASTEXITCODE -ne 0) { throw "windows-service --enableService failed: $LASTEXITCODE" }
+          net start "OpenDJ Server"
+          if ($LASTEXITCODE -ne 0) { throw "net start failed: $LASTEXITCODE" }
+          for ($i=0; $i -lt 12; $i++) { try { $c = New-Object System.Net.Sockets.TcpClient('localhost', 1636); $c.Close(); break } catch { Start-Sleep -Seconds 5 } }
+          & "$root\bat\ldapsearch.bat" --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
+          if ($LASTEXITCODE -ne 0) { throw "ldapsearch failed: $LASTEXITCODE" }
+          net stop "OpenDJ Server"
+          if ($LASTEXITCODE -ne 0) { throw "net stop failed: $LASTEXITCODE" }
+          & "$root\bat\windows-service.bat" --disableService
+      - name: Uninstall MSI
+        shell: pwsh
+        run: |
+          $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+          $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall.log"
+          if ($p.ExitCode -ne 0) { Get-Content uninstall.log -Tail 80; throw "msiexec /x failed: $($p.ExitCode)" }
+          Write-Host "Uninstalled OK"
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 92be08f..f976103 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
@@ -40,6 +40,8 @@
 
 * xref:#install-rpm["To Install From the RPM Package"]
 
+* xref:#install-msi["To Install With the Windows Installer (MSI)"]
+
 * xref:#install-properties-file["To Install OpenDJ Directory Server With a Properties File"]
 
 * xref:#pdb-to-je["To Move Data from a PDB Backend to a JE Backend"]
@@ -635,6 +637,48 @@
 
 ====
 
+[#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.
+
+. 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.
+
+. Install the package, either with the GUI or silently:
++
+* 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):
++
+
+[source, console, subs="attributes"]
+----
+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).
+
+. 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:
++
+
+[source, console]
+----
+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:
++
+
+[source, console]
+----
+C:\path\to\opendj\bat> windows-service.bat --enableService
+C:\> net start "OpenDJ Server"
+----
+
+====
+
 [#install-properties-file]
 .To Install OpenDJ Directory Server With a Properties File
 ====
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 19cc3a3..17e0413 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
@@ -12,7 +12,7 @@
   information: "Portions copyright [year] [name of copyright owner]".
  
   Copyright 2017 ForgeRock AS.
-  Portions Copyright 2024 3A Systems LLC.
+  Portions Copyright 2024-2026 3A Systems LLC.
 ////
 
 :figure-caption!:
@@ -33,6 +33,8 @@
 
 * xref:#uninstall-rpm["To Uninstall the RPM Package"]
 
+* xref:#uninstall-msi["To Uninstall the Windows MSI Package"]
+
 
 [#uninstall-gui]
 .To Remove OpenDJ With the GUI Uninstaller
@@ -157,3 +159,28 @@
 
 ====
 
+[#uninstall-msi]
+.To Uninstall the Windows MSI Package
+====
+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:
++
+
+[source, console]
+----
+C:\path\to\opendj\bat> windows-service.bat --disableService
+----
+
+. Uninstall the package, either through __Settings > Apps__ (or __Control Panel > Programs and Features__) by selecting OpenDJ and choosing Uninstall, or from the command-line:
++
+
+[source, console, subs="attributes"]
+----
+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.
+
+====
+
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 cf0268d..c514e71 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
@@ -45,6 +45,8 @@
 
 * xref:#upgrade-zip-example["Upgrading to OpenDJ {opendj-version-short}"]
 
+* xref:#upgrade-msi["To Upgrade the Windows MSI Installation"]
+
 * xref:#upgrade-repl["To Upgrade Replicated Servers"]
 
 * xref:#new-repl-mixed-topology["To Add a New Replica to an Existing Topology"]
@@ -243,6 +245,51 @@
 ----
 ====
 
+[#upgrade-msi]
+.To Upgrade the Windows MSI Installation
+====
+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:
++
+
+[source, console]
+----
+C:\path\to\opendj\bat> windows-service.bat --disableService
+----
+
+. 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:
++
+
+[source, console, subs="attributes"]
+----
+C:\> msiexec /i opendj-{opendj-version}.msi /quiet OPENDJ="C:\path\to\opendj"
+----
+
+. 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:
++
+
+[source, console]
+----
+C:\path\to\opendj> upgrade.bat --no-prompt --acceptLicense
+----
+
+. Start the upgraded OpenDJ server.
+
+. If you disabled the Windows service, enable it again:
++
+
+[source, console]
+----
+C:\path\to\opendj\bat> windows-service.bat --enableService
+----
+
+====
+
 [#upgrade-repl]
 .To Upgrade Replicated Servers
 ====
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 23f4dd4..f77dda7 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
@@ -13,7 +13,7 @@
  ! information: "Portions Copyright [year] [name of copyright owner]".
  !
  ! Copyright 2013-2016 ForgeRock AS.
- ! Portion Copyright 2018 Open Identity Platform Community
+ ! Portions Copyright 2018-2026 3A Systems, LLC
  ! -->
 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
   <Product Id="*" Codepage="1252" Language="1033" Manufacturer="Open Identity Platform Community"
diff --git a/opendj-server-legacy/resource/bin/_script-util.bat b/opendj-server-legacy/resource/bin/_script-util.bat
index f6bacbb..bea162b7 100644
--- a/opendj-server-legacy/resource/bin/_script-util.bat
+++ b/opendj-server-legacy/resource/bin/_script-util.bat
@@ -13,7 +13,7 @@
 rem
 rem Copyright 2008-2010 Sun Microsystems, Inc.
 rem Portions Copyright 2011-2016 ForgeRock AS.
-rem Portions Copyright 2020-2025 3A Systems, LLC.
+rem Portions Copyright 2020-2026 3A Systems, LLC.
 
 set SET_JAVA_HOME_AND_ARGS_DONE=false
 set SET_ENVIRONMENT_VARS_DONE=false
@@ -56,10 +56,10 @@
 rem it also helps comparing the two paths
 FOR /F "delims=" %%i IN ("%INSTALL_ROOT%")  DO set INSTALL_ROOT=%%~dpnxi
 FOR /F "delims=" %%i IN ("%INSTANCE_ROOT%") DO set INSTANCE_ROOT=%%~dpnxi
-call "%INSTALL_ROOT%\lib\setcp.bat" %INSTALL_ROOT%\lib\bootstrap-client.jar
+call "%INSTALL_ROOT%\lib\setcp.bat" "%INSTALL_ROOT%\lib\bootstrap-client.jar"
 set CLASSPATH=%INSTANCE_ROOT%\classes;%CLASSPATH%
 if "%INSTALL_ROOT%" == "%INSTANCE_ROOT%" goto setClassPathDone
-FOR %%x in ("%INSTANCE_ROOT%\lib\*.jar") DO call "%INSTANCE_ROOT%\lib\setcp.bat" %%x
+FOR %%x in ("%INSTANCE_ROOT%\lib\*.jar") DO call "%INSTANCE_ROOT%\lib\setcp.bat" "%%x"
 :setClassPathDone
 set SET_CLASSPATH_DONE=true
 goto scriptBegin
@@ -71,7 +71,7 @@
 rem it also helps comparing the two paths
 FOR /F "delims=" %%i IN ("%INSTALL_ROOT%")  DO set INSTALL_ROOT=%%~dpnxi
 FOR /F "delims=" %%i IN ("%INSTANCE_ROOT%") DO set INSTANCE_ROOT=%%~dpnxi
-call "%INSTALL_ROOT%\lib\setcp.bat" %INSTALL_ROOT%\lib\*
+call "%INSTALL_ROOT%\lib\setcp.bat" "%INSTALL_ROOT%\lib\*"
 set CLASSPATH=%INSTANCE_ROOT%\classes;%CLASSPATH%
 set SET_CLASSPATH_DONE=true
 goto scriptBegin
@@ -100,7 +100,8 @@
 set SCRIPT_JAVA_ARGS_PROPERTY=%SCRIPT_NAME%.java-args
 call:readProperty %SCRIPT_JAVA_ARGS_PROPERTY%
 set OPENDJ_JAVA_ARGS=%OPENDJ_JAVA_ARGS% %PROPERTY_VALUE%
-if not "%OPENDJ_JAVA_ARGS%" == "" goto checkEnvJavaHome
+rem "if defined" comparisons: the value may contain quotes (java.io.tmpdir), which break "%VAR%" == "" checks.
+if defined OPENDJ_JAVA_ARGS goto checkEnvJavaHome
 call:readProperty default.java-args
 set OPENDJ_JAVA_ARGS=%OPENDJ_JAVA_ARGS% %PROPERTY_VALUE
 if "%OPENDJ_JAVA_BIN%" == "" goto checkEnvJavaHome
@@ -186,12 +187,12 @@
 if %SET_TEMP_DIR_DONE% == "true" goto end
 set OPENDJ_TMP_DIR=%INSTANCE_ROOT%\tmp
 if not exist "%OPENDJ_TMP_DIR%" mkdir "%OPENDJ_TMP_DIR%"
-set OPENDJ_JAVA_ARGS=%OPENDJ_JAVA_ARGS% -Djava.io.tmpdir=%OPENDJ_TMP_DIR%
+set OPENDJ_JAVA_ARGS=%OPENDJ_JAVA_ARGS% -Djava.io.tmpdir="%OPENDJ_TMP_DIR%"
 set SET_TEMP_DIR_DONE=true
 goto scriptBegin
 
 :testJava
-if "%OPENDJ_JAVA_ARGS%" == "" goto checkLegacyArgs
+if not defined OPENDJ_JAVA_ARGS goto checkLegacyArgs
 :continueTestJava
 "%OPENDJ_JAVA_BIN%" %OPENDJ_JAVA_ARGS% org.opends.server.tools.CheckJVMVersion > NUL 2>&1
 set RESULT_CODE=%errorlevel%
@@ -200,12 +201,12 @@
 goto end
 
 :checkLegacyArgs
-if "%OPENDS_JAVA_ARGS%" == "" goto continueTestJava
+if not defined OPENDS_JAVA_ARGS goto continueTestJava
 set OPENDJ_JAVA_ARGS=%OPENDS_JAVA_ARGS%
 goto continueTestJava
 
 :noValidJavaHome
-if NOT "%OPENDJ_JAVA_ARGS%" == "" goto noValidHomeWithArgs
+if defined OPENDJ_JAVA_ARGS goto noValidHomeWithArgs
 echo ERROR:  The detected Java version could not be used.  The detected
 echo Java binary is:
 echo %OPENDJ_JAVA_BIN%
diff --git a/opendj-server-legacy/resource/bin/setcp.bat b/opendj-server-legacy/resource/bin/setcp.bat
index a47d4e6..eca179c 100644
--- a/opendj-server-legacy/resource/bin/setcp.bat
+++ b/opendj-server-legacy/resource/bin/setcp.bat
@@ -12,14 +12,17 @@
 rem information: "Portions Copyright [year] [name of copyright owner]".
 rem
 rem Copyright 2006-2008 Sun Microsystems, Inc.
+rem Portions Copyright 2026 3A Systems, LLC
 
-set CLASSPATHCOMPONENT=%1
-if ""%1""=="""" goto gotAllArgs
+rem Use %~1 and quoted comparisons so paths containing spaces and parentheses
+rem (for example C:\Program Files (x86)\OpenDJ) do not break the parser.
+set CLASSPATHCOMPONENT=%~1
+if "%~1"=="" goto gotAllArgs
 shift
 
 :argCheck
-if ""%1""=="""" goto gotAllArgs
-set CLASSPATHCOMPONENT=%CLASSPATHCOMPONENT% %1
+if "%~1"=="" goto gotAllArgs
+set CLASSPATHCOMPONENT=%CLASSPATHCOMPONENT% %~1
 shift
 goto argCheck
 
diff --git a/opendj-server-legacy/resource/bin/start-ds.bat b/opendj-server-legacy/resource/bin/start-ds.bat
index 7f7be60..f591502 100644
--- a/opendj-server-legacy/resource/bin/start-ds.bat
+++ b/opendj-server-legacy/resource/bin/start-ds.bat
@@ -14,7 +14,7 @@
 rem
 rem Copyright 2006-2010 Sun Microsystems, Inc.
 rem Portions Copyright 2011-2014 ForgeRock AS.
-rem Portions Copyright 2025 3A Systems LLC.
+rem Portions Copyright 2025-2026 3A Systems LLC.
 
 setlocal
 set DIR_HOME=%~dp0..
@@ -61,10 +61,12 @@
 rem cleanup the tmp directory
 set CUR_DIR=%CD%
 set OPENDJ_TMP_DIR=%INSTANCE_ROOT%\tmp
-dir /b /s /a %OPENDJ_TMP_DIR% | findstr .>nul && (
-    cd /d %OPENDJ_TMP_DIR%
+rem The paths must be quoted: an unquoted parenthesis (e.g. from
+rem "C:\Program Files (x86)") terminates the ( ) block at parse time.
+dir /b /s /a "%OPENDJ_TMP_DIR%" | findstr .>nul && (
+    cd /d "%OPENDJ_TMP_DIR%"
     for /F "delims=" %%i in ('dir /b') do (rmdir "%%i" /s/q>NUL 2>&1 || del "%%i" /s/q>NUL 2>&1)
-    cd /d %CUR_DIR%
+    cd /d "%CUR_DIR%"
 )
 
 "%OPENDJ_JAVA_BIN%" -client %SCRIPT_NAME_ARG% org.opends.server.core.DirectoryServer --configFile "%INSTANCE_ROOT%\config\config.ldif" --checkStartability %*

--
Gitblit v1.10.0