mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
7 hours ago 0138b5924ff14ef709398bf878dbb9bf2a0b7c33
[#1030] Keep the setup log out of the way of start-ds, and say when it is gone (#1032)
23 files modified
4 files added
1401 ■■■■■ changed files
.github/workflows/build.yml 29 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/bin/start-ds 12 ●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/bin/start-ds.bat 20 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/guitools/uninstaller/UninstallLauncher.java 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/guitools/uninstaller/Uninstaller.java 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/quicksetup/Application.java 49 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/quicksetup/Installation.java 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/quicksetup/Launcher.java 141 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/quicksetup/SplashScreen.java 14 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/quicksetup/TempLogFile.java 102 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java 51 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/SetupLauncher.java 42 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/quicksetup/ui/QuickSetup.java 7 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDS.java 41 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/quickSetup.properties 7 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_de.properties 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_es.properties 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_fr.properties 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_ja.properties 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_ko.properties 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_pl.properties 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_zh_CN.properties 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_zh_TW.properties 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/quicksetup/LauncherTest.java 209 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/quicksetup/TempLogFileTest.java 220 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/InstallerTest.java 298 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/tools/InstallDSTest.java 131 ●●●●● patch | view | raw | blame | history
.github/workflows/build.yml
@@ -241,8 +241,17 @@
          --baseDN dc=example,dc=com --sampleData 5000 --cli --acceptLicense --no-prompt \
          --propertiesFilePath /tmp/opendj-setup.properties.bcfks --doNotStart
        
        # start-ds sweeps the native libraries the BC FIPS loader leaves in tmp/ and nothing
        # else: the rest of tmp/ belongs to tools that may still be running, setup among them
        # (issue #1030). Without these two probes a sweep back to "tmp/*" passes every cell.
        mkdir -p opendj-server-legacy/target/package/opendj/tmp/bc-fips-jni_123
        touch opendj-server-legacy/target/package/opendj/tmp/keep.me
        opendj-server-legacy/target/package/opendj/bin/start-ds
        
        test ! -e opendj-server-legacy/target/package/opendj/tmp/bc-fips-jni_123
        test -e opendj-server-legacy/target/package/opendj/tmp/keep.me
        opendj-server-legacy/target/package/opendj/bin/status --hostname localhost --bindDN "cn=Directory Manager" --bindPassword password --trustAll
        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
        opendj-server-legacy/target/package/opendj/bin/ldapsearch --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "ou=people,dc=example,dc=com" --searchScope sub "(uid=user.*)" dn | grep ^dn: | wc -l | grep -q 5000
@@ -397,7 +406,14 @@
        opendj-server-legacy\target\package\opendj\upgrade.bat
        opendj-server-legacy\target\package\opendj\bat\import-ldif.bat --offline --ldifFile test.ldif --backendID=example2
        opendj-server-legacy\target\package\opendj\bat\rebuild-index.bat --offline --bindDN "cn=Directory Manager" --bindPassword password  --baseDN "dc=example2,dc=com" --rebuildAll
        # The same probe as the Unix FIPS step, for start-ds.bat: the sweep takes the BC FIPS
        # directories and leaves everything else in tmp\ alone (issue #1030). The runner's
        # install path has no space in it, so this does not settle the quoting of the for /D line.
        New-Item -ItemType Directory -Force opendj-server-legacy\target\package\opendj\tmp\bc-fips-jni_123 | Out-Null
        New-Item -ItemType File -Force opendj-server-legacy\target\package\opendj\tmp\keep.me | Out-Null
        opendj-server-legacy\target\package\opendj\bat\start-ds.bat
        if (Test-Path opendj-server-legacy\target\package\opendj\tmp\bc-fips-jni_123) { throw "start-ds.bat left a bc-fips-jni_* directory behind" }
        if (-not (Test-Path opendj-server-legacy\target\package\opendj\tmp\keep.me)) { throw "start-ds.bat removed a tmp file that is not its own" }
        opendj-server-legacy\target\package\opendj\bat\rebuild-index.bat --bindDN "cn=Directory Manager" --bindPassword password --baseDN "dc=example2,dc=com" --rebuildAll --trustAll
        opendj-server-legacy\target\package\opendj\bat\ldapsearch.bat --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example2,dc=com" --searchScope sub "(uid=user.*)" dn | find /c '"dn:"' | findstr "10000"
        opendj-server-legacy\target\package\opendj\bat\stop-ds.bat
@@ -412,6 +428,19 @@
        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
    # A test step that fails leaves its instances behind. The server-side story of a
    # failed start lives in logs/server.out and logs/errors, and nothing else prints it
    # (setup only has the client-side view, see issue #1030).
    - name: Dump the server logs of a failed test
      if: failure()
      shell: bash
      run: |
        for f in opendj-server-legacy/target/package/opendj*/logs/server.out opendj-server-legacy/target/package/opendj*/logs/errors; do
          [ -f "$f" ] || continue
          echo "===== $f"
          cat "$f"
        done
    - name: Upload artifacts OpenDJ Server
      uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
      with:
opendj-server-legacy/resource/bin/start-ds
@@ -14,7 +14,7 @@
#
# Copyright 2006-2009 Sun Microsystems, Inc.
# Portions Copyright 2011-2014 ForgeRock AS.
# Portions Copyright 2025 3A Systems LLC.
# Portions Copyright 2025-2026 3A Systems LLC.
# Capture the current working directory so that we can change to it later.
@@ -73,10 +73,16 @@
LOG_FILE=${INSTANCE_ROOT}/logs/server.out
STARTING_FILE=${INSTANCE_ROOT}/logs/server.starting
# Cleanup the tmp directory
# Remove the native libraries the BC FIPS loader extracts into the tmp directory
# on every start, so that they do not pile up. Nothing else in there is ours to
# remove: the tmp directory is java.io.tmpdir for every tool, and a tool may
# still be running - setup starts the server through this script and keeps its
# own log open (issue #1030). The logs a failed dsreplication or status run
# leaves there (opendj-replication-*.log, opendj-status-*.log) are the
# operator's to read and to remove; starting the server no longer takes them.
OPENDJ_TMP_DIR="${INSTANCE_ROOT}/tmp"
if [ -d "${OPENDJ_TMP_DIR}" ]; then
   rm -rf ${OPENDJ_TMP_DIR}/*
   rm -rf "${OPENDJ_TMP_DIR}"/bc-fips-jni_*
fi
# See if the provided set of arguments were sufficient for us to be able to
opendj-server-legacy/resource/bin/start-ds.bat
@@ -58,16 +58,18 @@
echo %SCRIPT%: PATH=%PATH% >> %LOG%
rem cleanup the tmp directory
set CUR_DIR=%CD%
rem Remove the native libraries the BC FIPS loader extracts into the tmp
rem directory on every start, so that they do not pile up. Nothing else in
rem there is ours to remove: the tmp directory is java.io.tmpdir for every
rem tool, and a tool may still be running - setup starts the server through
rem this script and keeps its own log open (issue #1030). The logs a failed
rem dsreplication or status run leaves there (opendj-replication-*.log,
rem opendj-status-*.log) are the operator's to read and to remove; starting
rem the server no longer takes them.
set OPENDJ_TMP_DIR=%INSTANCE_ROOT%\tmp
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%"
)
rem %%~i drops the quotes cmd would hand back if it ever kept them: an install path with a
rem space would otherwise split the rmdir argument, and the redirect would hide it.
for /D %%i in ("%OPENDJ_TMP_DIR%\bc-fips-jni_*") do rmdir "%%~i" /s/q>NUL 2>&1
"%OPENDJ_JAVA_BIN%" -client %SCRIPT_NAME_ARG% org.opends.server.core.DirectoryServer --configFile "%INSTANCE_ROOT%\config\config.ldif" --checkStartability %*
opendj-server-legacy/src/main/java/org/opends/guitools/uninstaller/UninstallLauncher.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.guitools.uninstaller;
@@ -129,8 +130,8 @@
  @Override
  protected void guiLaunchFailed() {
      System.err.println(
          tempLogFile.isEnabled() ? ERR_UNINSTALL_LAUNCHER_GUI_LAUNCHED_FAILED_DETAILS.get(tempLogFile.getPath())
                                  : ERR_UNINSTALL_LAUNCHER_GUI_LAUNCHED_FAILED.get());
          hasTempLogFile() ? ERR_UNINSTALL_LAUNCHER_GUI_LAUNCHED_FAILED_DETAILS.get(getTempLogFile().getPath())
                           : ERR_UNINSTALL_LAUNCHER_GUI_LAUNCHED_FAILED.get());
  }
  @Override
opendj-server-legacy/src/main/java/org/opends/guitools/uninstaller/Uninstaller.java
@@ -696,6 +696,11 @@
  /** Actually performs the uninstall in this thread. The thread is blocked. */
  @Override
  public void run() {
    // The uninstall begins here, as the install begins in Installer.run(): the errors below go
    // to the error logger, and only the log file puts a publisher on it. Without it a failed
    // uninstall in the wizard leaves no record anywhere (issue #1030). The command line
    // uninstaller is given no supplier - its launcher has created the log already.
    openTempLogFile();
    status = STARTED;
    logger.info(LocalizableMessage.raw("run of the Uninstaller started"));
opendj-server-legacy/src/main/java/org/opends/quicksetup/Application.java
@@ -26,6 +26,7 @@
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
@@ -76,9 +77,17 @@
  private final ErrorPrintStream err = new ErrorPrintStream();
  private final OutputPrintStream out = new OutputPrintStream();
  /** Temporary log file where messages will be logged. */
  /**
   * Temporary log file where messages will be logged, once {@link #openTempLogFile()} has
   * asked for it. It stays {@code null} until then: creating it costs a file - and the
   * directory it lives in - that nothing removes unless the operation succeeds, so the roads
   * which attempt nothing must not create one (issue #1030).
   */
  protected TempLogFile tempLogFile;
  /** Supplies the temporary log file, see {@link #openTempLogFile()}. */
  private Supplier<TempLogFile> tempLogFileSupplier;
  /**
   * Creates an application by instantiating the Application class
   * denoted by the System property
@@ -685,14 +694,44 @@
  }
  /**
   * Sets the temporary log file where messages will be logged.
   * Sets where the temporary log file of this application comes from, without creating it.
   * <p>
   * The wizard and the CLI hand the supplier over as they start: the file itself is created by
   * {@link #openTempLogFile()}, on the road that runs the operation, so that a run which does
   * nothing - a quit at any step of the wizard, a server which turns out to be installed
   * already - leaves no log behind (issue #1030).
   * <p>
   * There is deliberately no setter taking the file itself: a caller holding a supplier could
   * then resolve it on the spot, and the log would be created before anything is attempted
   * again.
   *
   * @param tempLogFile
   *            temporary log file where messages will be logged.
   *            supplies the temporary log file where messages will be logged.
   */
  public void setTempLogFile(final TempLogFile tempLogFile)
  public void setTempLogFile(final Supplier<TempLogFile> tempLogFile)
  {
    this.tempLogFile = tempLogFile;
    this.tempLogFile = null;
    this.tempLogFileSupplier = tempLogFile;
  }
  /**
   * Creates the temporary log file of this application, unless it has one already.
   * <p>
   * Called where the operation begins and not before: from that point on the log is worth
   * keeping, because there is something which can fail and be reported.
   * <p>
   * An application nobody handed a supplier to has no log of its own: the command line
   * uninstaller is built that way, and its launcher creates the log before running it.
   *
   * @return the temporary log file of this application, or {@code null} if it was given none.
   */
  protected TempLogFile openTempLogFile()
  {
    if (tempLogFile == null && tempLogFileSupplier != null)
    {
      tempLogFile = tempLogFileSupplier.get();
    }
    return tempLogFile;
  }
  /**
opendj-server-legacy/src/main/java/org/opends/quicksetup/Installation.java
@@ -64,7 +64,7 @@
  /** The relative path where the database files are. */
  private static final String DATABASES_PATH_RELATIVE = "db";
  /** The relative path where the log files are. */
  private static final String LOGS_PATH_RELATIVE = "logs";
  public static final String LOGS_PATH_RELATIVE = "logs";
  /** The relative path where the LDIF files are. */
  private static final String LDIFS_PATH_RELATIVE = "ldif";
  /** The relative path where the backup files are. */
opendj-server-legacy/src/main/java/org/opends/quicksetup/Launcher.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2009 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.quicksetup;
@@ -21,7 +22,9 @@
import static org.opends.messages.QuickSetupMessages.*;
import static org.opends.server.util.DynamicConstants.*;
import java.io.File;
import java.io.PrintStream;
import java.util.function.Supplier;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
@@ -40,8 +43,16 @@
  /** Arguments with which this launcher was invoked. */
  protected final String[] args;
  /** The temporary log file which will be kept if an error occurs. */
  protected final TempLogFile tempLogFile;
  private final String tempLogFilePrefix;
  private final File tempLogFileDirectory;
  /**
   * The temporary log file which will be kept if an error occurs; see {@link #getTempLogFile()}.
   * Volatile because the GUI road asks for it from the thread that runs the splash screen and
   * the roads after it read it from the main thread.
   */
  private volatile TempLogFile tempLogFile;
  /** Why the GUI did not come up, kept until there is a log to write it to. */
  private volatile Throwable guiLaunchFailure;
  /**
   * Creates a Launcher.
@@ -52,11 +63,79 @@
   *          temporary log file path where messages will be logged
   */
  public Launcher(final String[] args, final String tempLogFilePrefix) {
    this(args, tempLogFilePrefix, null);
  }
  /**
   * Creates a Launcher whose temporary log file lives in the given directory.
   *
   * @param args
   *          String[] of argument passes from the command line
   * @param tempLogFilePrefix
   *          temporary log file path where messages will be logged
   * @param tempLogFileDirectory
   *          the directory to create the temporary log file in, or {@code null} for the OS
   *          temporary directory
   */
  public Launcher(final String[] args, final String tempLogFilePrefix, final File tempLogFileDirectory) {
    if (args == null) {
      throw new IllegalArgumentException("args cannot be null");
    }
    this.args = args;
    this.tempLogFile = TempLogFile.newTempLogFile(tempLogFilePrefix);
    this.tempLogFilePrefix = tempLogFilePrefix;
    this.tempLogFileDirectory = tempLogFileDirectory;
  }
  /**
   * The temporary log file of this launcher, created the first time it is asked for.
   * <p>
   * Creating it costs a file - and, with a directory of the caller's choosing, the directory
   * as well - that nothing removes afterwards unless the operation succeeds. So it is created
   * on the first road that can fail an operation and not before: {@code --help},
   * {@code --version}, a usage error and the other roads that attempt nothing leave no log
   * behind (issue #1030).
   *
   * @return the temporary log file, creating it if this is the first call.
   */
  protected synchronized TempLogFile getTempLogFile() {
    if (tempLogFile == null) {
      tempLogFile = TempLogFile.newTempLogFile(tempLogFilePrefix, tempLogFileDirectory);
      logGuiLaunchFailure();
    }
    return tempLogFile;
  }
  /**
   * Whether there is a log to name, without creating one to answer.
   *
   * @return {@code true} if a temporary log file has been created and can be used to log
   *         messages.
   */
  protected boolean hasTempLogFile() {
    return tempLogFile != null && tempLogFile.isEnabled();
  }
  /** Writes the reason the GUI did not come up, now that there is a log to hold it. */
  private void logGuiLaunchFailure() {
    Throwable failure = guiLaunchFailure;
    if (failure == null) {
      return;
    }
    logger.warn(LocalizableMessage.raw("Error launching GUI: " + failure));
    StringBuilder buf = new StringBuilder();
    while (failure != null)
    {
      for (StackTraceElement aStack : failure.getStackTrace()) {
        buf.append(aStack).append("\n");
      }
      failure = failure.getCause();
      if (failure != null)
      {
        buf.append("Root cause:\n");
      }
    }
    logger.warn(LocalizableMessage.raw(buf));
  }
  /**
@@ -194,29 +273,16 @@
      {
        try
        {
          SplashScreen.main(tempLogFile, args);
          startSplashScreen(Launcher.this::getTempLogFile, args);
          returnValue[0] = 0;
        }
        catch (Throwable t)
        {
          if (tempLogFile.isEnabled())
          {
            logger.warn(LocalizableMessage.raw("Error launching GUI: "+t));
            StringBuilder buf = new StringBuilder();
            while (t != null)
            {
              for (StackTraceElement aStack : t.getStackTrace()) {
                buf.append(aStack).append("\n");
              }
              t = t.getCause();
              if (t != null)
              {
                buf.append("Root cause:\n");
              }
            }
            logger.warn(LocalizableMessage.raw(buf));
          }
          // Kept rather than logged: a GUI which does not come up is not by itself an
          // operation that failed, and creating a log here would leave one behind on every
          // headless road that installs nothing (issue #1030). It goes into the log as soon
          // as something asks for one.
          guiLaunchFailure = t;
        }
      }
    });
@@ -240,6 +306,26 @@
  }
  /**
   * Shows the splash screen and, behind it, builds the wizard.
   * <p>
   * The log file is handed over as a supplier and not as a file: the splash screen comes up
   * before the user has said anything, and a log created there outlives every road that
   * installs nothing - a quit at any wizard step, a server which is configured already
   * (issue #1030). The application asks for it when it starts the operation.
   * <p>
   * Package-private so that a test can drive {@link #launchGui(String[])} without a display.
   *
   * @param tempLogFile
   *          supplies the temporary log file of the application
   * @param args
   *          the arguments to pass to the splash screen
   */
  void startSplashScreen(final Supplier<TempLogFile> tempLogFile, final String[] args)
  {
    SplashScreen.main(tempLogFile, args);
  }
  /**
   * Gets the frame title of the GUI application that will be used
   * in some operating systems.
   * @return internationalized String representing the frame title
@@ -337,6 +423,9 @@
      }
      System.exit(ReturnCode.SUCCESSFUL.getReturnCode());
    } else if (isCli()) {
      // An operation is about to run: from here on there is something worth logging, and
      // preExit() names the file. The roads above attempt nothing and leave no log behind.
      getTempLogFile();
      CliApplication cliApp = createCliApplication();
      int exitCode = launchCli(cliApp);
      preExit(cliApp);
@@ -345,6 +434,10 @@
      willLaunchGui();
      int exitCode = launchGui(args);
      if (exitCode != 0) {
        // The GUI did not come up and the operation runs on the command line after all: from
        // here on there is something worth logging, the reason the GUI failed included. The
        // log comes first, so that guiLaunchFailed() can name the file holding that reason.
        getTempLogFile();
        guiLaunchFailed();
        CliApplication cliApp = createCliApplication();
        exitCode = launchCli(cliApp);
@@ -361,8 +454,8 @@
        // Add an extra space systematically
        System.out.println();
        if (tempLogFile.isEnabled()) {
          System.out.println(INFO_GENERAL_SEE_FOR_DETAILS.get(tempLogFile.getPath()));
        if (hasTempLogFile()) {
          System.out.println(INFO_GENERAL_SEE_FOR_DETAILS.get(getTempLogFile().getPath()));
        }
      }
    }
opendj-server-legacy/src/main/java/org/opends/quicksetup/SplashScreen.java
@@ -26,6 +26,7 @@
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.awt.Window;
import java.util.function.Supplier;
import javax.swing.SwingUtilities;
@@ -51,7 +52,12 @@
  private Object quickSetup;
  private Class<?> quickSetupClass;
  private TempLogFile tempLogFile;
  /**
   * Supplies the temporary log file of the application behind this splash screen. Nothing
   * here asks for it: the splash screen shows before anything is attempted, and a log created
   * at that point outlives every road that attempts nothing (issue #1030).
   */
  private Supplier<TempLogFile> tempLogFile;
  /** Constant for the display of the splash screen. */
  private static final int MIN_SPLASH_DISPLAY = 3000;
@@ -61,11 +67,11 @@
   * It can be called from the event thread and outside the event thread.
   *
   * @param tempLogFile
   *        temporary log file where messages will be logged
   *        supplies the temporary log file where messages will be logged
   * @param args
   *        arguments to be passed to the method QuickSetup.initialize
   */
  public static void main(final TempLogFile tempLogFile, String[] args)
  public static void main(final Supplier<TempLogFile> tempLogFile, String[] args)
  {
    SplashScreen screen = new SplashScreen();
    screen.tempLogFile = tempLogFile;
@@ -196,7 +202,7 @@
    {
      quickSetupClass = Class.forName("org.opends.quicksetup.ui.QuickSetup");
      quickSetup = quickSetupClass.getDeclaredConstructor().newInstance();
      quickSetupClass.getMethod("initialize", new Class[] { TempLogFile.class, String[].class })
      quickSetupClass.getMethod("initialize", new Class[] { Supplier.class, String[].class })
                     .invoke(quickSetup, tempLogFile, args);
    } catch (Exception e)
    {
opendj-server-legacy/src/main/java/org/opends/quicksetup/TempLogFile.java
@@ -20,6 +20,7 @@
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.Date;
import java.text.DateFormat;
@@ -51,9 +52,51 @@
   */
  public static TempLogFile newTempLogFile(final String prefix)
  {
    return newTempLogFile(prefix, null);
  }
  /**
   * Creates a new temporary log file in the given directory.
   * <p>
   * The directory is created if it does not exist yet. When it is {@code null} or cannot be
   * used, the log file goes to the OS temporary directory instead, as with
   * {@link #newTempLogFile(String)}. The name of the file follows the pattern
   * prefix-[RANDOM_NUMBER_STRING].log either way.
   *
   * @param prefix
   *          log file prefix to which log messages will be written.
   * @param directory
   *          the directory to create the log file in, or {@code null} for the OS temporary
   *          directory.
   * @return a new temporary log file.
   */
  public static TempLogFile newTempLogFile(final String prefix, final File directory)
  {
    IOException fallbackReason = null;
    if (directory != null)
    {
      try
      {
        Files.createDirectories(directory.toPath());
        return new TempLogFile(Files.createTempFile(directory.toPath(), prefix, ".log").toFile());
      }
      catch (final IOException e)
      {
        // Nothing can be logged yet: the first publisher is the one the constructor installs
        // below, so the warning has to wait until there is a log to write it to.
        fallbackReason = e;
      }
    }
    try
    {
      return new TempLogFile(Files.createTempFile(prefix, ".log").toFile());
      final TempLogFile tempLogFile = new TempLogFile(Files.createTempFile(prefix, ".log").toFile());
      if (fallbackReason != null)
      {
        localizedLogger.warn(LocalizableMessage.raw("Unable to create temp log file in " + directory
            + " because: " + fallbackReason.getMessage() + ", falling back to the temporary directory"),
            fallbackReason);
      }
      return tempLogFile;
    }
    catch (final IOException e)
    {
@@ -68,9 +111,14 @@
  {
    this.logFile = null;
    this.writer=null;
    this.startupErrorLogPublisher = null;
    this.startupDebugLogPublisher = null;
  }
  final TextWriter writer;
  /** Kept so that they can be taken off the logger singletons again, see {@link #deleteLogFileAfterSuccess()}. */
  private final ErrorLogPublisher startupErrorLogPublisher;
  private final DebugLogPublisher startupDebugLogPublisher;
  
  private TempLogFile(final File file) throws IOException
  {
@@ -83,9 +131,9 @@
    }else {
        writer=new TextWriter.STREAM(new FileOutputStream(file));
    }
    ErrorLogPublisher startupErrorLogPublisher = TextErrorLogPublisher.getServerStartupTextErrorPublisher(writer);
    startupErrorLogPublisher = TextErrorLogPublisher.getServerStartupTextErrorPublisher(writer);
    ErrorLogger.getInstance().addLogPublisher(startupErrorLogPublisher);
    DebugLogger.getInstance().addPublisherIfRequired(writer);
    startupDebugLogPublisher = DebugLogger.getInstance().addPublisherIfRequired(writer);
    localizedLogger.info(LocalizableMessage.raw("QuickSetup application launched " + DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date()), null));
  }
@@ -100,11 +148,23 @@
    return logFile;
  }
  /** Closes the log file handler and delete the temp log file . */
  /**
   * Closes the log file handler and delete the temp log file .
   * <p>
   * The publishers installed by the constructor go with it: they are held by the logger
   * singletons, which outlive this object, and once the writer is shut everything they are
   * handed is written to a closed stream and swallowed.
   */
  public void deleteLogFileAfterSuccess()
  {
    if (isEnabled())
    {
      if (startupErrorLogPublisher != null) {
        ErrorLogger.getInstance().removeLogPublisher(startupErrorLogPublisher);
      }
      if (startupDebugLogPublisher != null) {
        DebugLogger.getInstance().removeLogPublisher(startupDebugLogPublisher);
      }
        if (writer!=null) {
            writer.shutdown();
        }
@@ -122,6 +182,40 @@
  }
  /**
   * Return {@code true} if the temp log file is still on disk and can be read.
   * <p>
   * Unlike {@link #isEnabled()} this is about the file, not the logger: something else may have
   * removed the file while the logger still writes to it (see issue #1030), and then there is
   * nothing to hand over to whoever needs the log.
   *
   * @return {@code true} if the temp log file is there and readable.
   */
  public boolean isReadable()
  {
    return logFile != null && Files.isReadable(logFile.toPath()) && Files.isRegularFile(logFile.toPath());
  }
  /**
   * Reads the whole temp log file.
   * <p>
   * The file is decoded with the default charset of the JVM, which is the one
   * {@link TextWriter.STREAM} wrote it with: reader and writer are the same JVM, so a
   * non-ASCII path or base DN in a report comes back as it was logged.
   *
   * @return the contents of the temp log file.
   * @throws IOException
   *           if the file cannot be read, for instance because it is no longer there.
   */
  public String readContents() throws IOException
  {
    if (logFile == null)
    {
      throw new IOException("No temp log file");
    }
    return new String(Files.readAllBytes(logFile.toPath()), Charset.defaultCharset());
  }
  /**
   * Return the absolute path of the temp log file.
   * @return the absolute path of the temp log file.
   */
opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java
@@ -42,9 +42,6 @@
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -244,6 +241,11 @@
  @Override
  public void run()
  {
    // The install begins here: this is the first point where something can fail and be worth
    // a report, so this is where the log file is created. Every road which stops before this
    // one - a quit at any step of the wizard, "already configured", a refused licence, a
    // cancel at the prompt - leaves neither the log nor its directory behind (issue #1030).
    openTempLogFile();
    applicationException = null;
    PrintStream origErr = System.err;
    PrintStream origOut = System.out;
@@ -317,6 +319,10 @@
        uninstall();
        setCurrentProgressStep(InstallProgressStep.FINISHED_CANCELED);
        notifyListeners(null);
        // Nothing names this log on this road - notifyListenersOfExistingLogFile() belongs to
        // handleInstallationError() below - and uninstall() has just taken the installation
        // back, so keeping the file would leave a report nobody is told about (issue #1030).
        tempLogFile.deleteLogFileAfterSuccess();
      } else {
        handleInstallationError(ex);
      }
@@ -617,23 +623,34 @@
    return Utils.getInstancePathFromInstallPath(installPath);
  }
  private void notifyListenersOfExistingLogFile()
  /** Package-private so that {@code InstallerTest} can drive every road of this report. */
  void notifyListenersOfExistingLogFile()
  {
    if (tempLogFile.isEnabled())
    if (!tempLogFile.isEnabled())
    {
      final String tempLogFilePath = tempLogFile.getPath();
      notifyListeners(getFormattedProgress(INFO_GENERAL_PROVIDE_LOG_IN_ERROR.get(tempLogFilePath)));
    //write log
      try {
        notifyListeners(getLineBreak());
        notifyListeners(LocalizableMessage.valueOf(new String(Files.readAllBytes(Paths.get(tempLogFilePath)),"UTF-8")));
      } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
      } catch (IOException e) {
        e.printStackTrace();
      }
      notifyListeners(getLineBreak());
      return;
    }
    final String tempLogFilePath = tempLogFile.getPath();
    if (!tempLogFile.isReadable())
    {
      // Something removed the log while it was being written (issue #1030): say so
      // rather than ask for a file that is not there.
      notifyListeners(getFormattedWarning(INFO_GENERAL_LOG_IN_ERROR_MISSING.get(tempLogFilePath)));
      notifyListeners(getLineBreak());
      return;
    }
    notifyListeners(getFormattedProgress(INFO_GENERAL_PROVIDE_LOG_IN_ERROR.get(tempLogFilePath)));
    notifyListeners(getLineBreak());
    // Write the log out as well, so that a report has it even when the file is not attached.
    try
    {
      notifyListeners(LocalizableMessage.raw(tempLogFile.readContents()));
    }
    catch (final IOException e)
    {
      notifyListeners(getFormattedWarning(INFO_GENERAL_LOG_IN_ERROR_UNREADABLE.get(tempLogFilePath, e)));
    }
    notifyListeners(getLineBreak());
  }
  /** Creates a default instance. */
opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/SetupLauncher.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.quicksetup.installer;
@@ -22,6 +23,8 @@
import static com.forgerock.opendj.util.StaticUtils.registerBcProvider;
import java.io.File;
import org.forgerock.i18n.LocalizableMessage;
import org.opends.quicksetup.CliApplication;
import org.opends.quicksetup.Installation;
@@ -65,7 +68,7 @@
   * @param args the arguments passed by the command lines.
   */
  public SetupLauncher(String[] args) {
    super(args, LOG_FILE_PREFIX);
    super(args, LOG_FILE_PREFIX, instanceLogsDirectory());
    if (System.getProperty(PROPERTY_SCRIPT_NAME) == null)
    {
      System.setProperty(PROPERTY_SCRIPT_NAME, Installation.getSetupFileName());
@@ -73,6 +76,32 @@
    initializeParser();
  }
  /**
   * The {@code logs/} directory of the instance being set up, where the setup log is kept.
   * <p>
   * The launcher scripts point {@code java.io.tmpdir} at {@code <instance>/tmp}, the scratch
   * space of every tool, which {@code start-ds} - run by setup itself to start the server -
   * used to sweep clean (issue #1030). The log of a failed setup belongs next to the server's
   * own logs instead, where {@code server.out} tells the other half of the story.
   * <p>
   * Neither the directory nor the log is created here: {@link Launcher#getTempLogFile()}
   * creates both when an install is about to run, so that a road which installs nothing -
   * {@code setup --help} on a package whose instance directory is not laid down yet, for one -
   * leaves nothing behind.
   *
   * @return the logs directory of the instance, or {@code null} when the launcher is not
   *         running from an installation and the OS temporary directory has to do.
   */
  private static File instanceLogsDirectory()
  {
    final String installPath = Utils.getInstallPathFromClasspath();
    if (installPath == null)
    {
      return null;
    }
    return new File(Utils.getInstancePathFromInstallPath(installPath), Installation.LOGS_PATH_RELATIVE);
  }
  /** Initialize the contents of the argument parser. */
  protected void initializeParser()
  {
@@ -109,7 +138,7 @@
      else if (isCli())
      {
        Utils.checkJavaVersion();
        System.exit(InstallDS.mainCLI(args, tempLogFile));
        System.exit(InstallDS.mainCLI(args, this::getTempLogFile));
      }
      else
      {
@@ -121,7 +150,7 @@
        if (exitCode != 0) {
          guiLaunchFailed();
          Utils.checkJavaVersion();
          System.exit(InstallDS.mainCLI(args, tempLogFile));
          System.exit(InstallDS.mainCLI(args, this::getTempLogFile));
        }
      }
    }
@@ -144,9 +173,10 @@
  @Override
  protected void guiLaunchFailed() {
      System.err.println(
          tempLogFile.isEnabled() ? INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED_DETAILS.get(tempLogFile.getPath())
                                  : INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED.get());
    // No log is named here: none exists yet, and creating one to name would leave it behind on
    // every command line road that installs nothing (issue #1030). The reason the GUI failed
    // goes into the log of the install that follows, and a failed install names that log.
    System.err.println(INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED.get());
  }
  @Override
opendj-server-legacy/src/main/java/org/opends/quicksetup/ui/QuickSetup.java
@@ -27,6 +27,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.logging.Handler;
import javax.swing.SwingUtilities;
@@ -91,12 +92,14 @@
   * blocked.
   *
   * @param tempLogFile
   *          temporary log file where messages will be logged.
   *          supplies the temporary log file where messages will be logged. The wizard does
   *          not create it: the application does, when it starts the operation, so that a
   *          wizard the user quits leaves no log behind (issue #1030).
   * @param args
   *          for the moment this parameter is not used but we keep it in order
   *          to (in case of need) pass parameters through the command line.
   */
  public void initialize(final TempLogFile tempLogFile, String[] args)
  public void initialize(final Supplier<TempLogFile> tempLogFile, String[] args)
  {
    ProgressMessageFormatter formatter = new HtmlProgressMessageFormatter();
opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDS.java
@@ -40,6 +40,7 @@
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Supplier;
import com.forgerock.opendj.util.FipsStaticUtils;
import org.forgerock.i18n.LocalizableMessage;
@@ -202,7 +203,11 @@
  private Integer lastResetAdminConnectorPort;
  private Integer lastResetJmxPort;
  private final TempLogFile tempLogFile;
  /**
   * The temporary log file where messages will be logged, asked for only once an install is
   * about to run: the roads which return before that one leave no log behind (issue #1030).
   */
  private final Supplier<TempLogFile> tempLogFile;
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
@@ -216,7 +221,7 @@
   * @param tempLogFile
   *          the temporary log file where messages will be logged.
   */
  private InstallDS(PrintStream out, PrintStream err, TempLogFile tempLogFile)
  private InstallDS(PrintStream out, PrintStream err, Supplier<TempLogFile> tempLogFile)
  {
    super(out, err);
    this.tempLogFile = tempLogFile;
@@ -229,10 +234,12 @@
   * @param args
   *          the command-line arguments provided to this program.
   * @param tempLogFile
   *          the temporary log file where messages will be logged.
   *          supplies the temporary log file where messages will be logged. It is called on
   *          the road that runs the install and not on the roads which return before it, so
   *          that a run which installs nothing creates no log.
   * @return The error code.
   */
  public static int mainCLI(String[] args, final TempLogFile tempLogFile)
  public static int mainCLI(String[] args, final Supplier<TempLogFile> tempLogFile)
  {
    return mainCLI(args, System.out, System.err, tempLogFile);
  }
@@ -259,7 +266,29 @@
    //
    // *NOTE* this method has been kept public because it is used by OpenAM.
    //
    return mainCLI(args, outStream, errStream, () -> tempLogFile);
  }
  /**
   * Parses the provided command-line arguments and uses that information to run
   * the setup tool.
   *
   * @param args
   *          The command-line arguments provided to this program.
   * @param outStream
   *          The output stream to use for standard output, or <CODE>null</CODE>
   *          if standard output is not needed.
   * @param errStream
   *          The output stream to use for standard error, or <CODE>null</CODE>
   *          if standard error is not needed.
   * @param tempLogFile
   *          supplies the temporary log file where messages will be logged, see
   *          {@link #mainCLI(String[], Supplier)}.
   * @return The error code.
   */
  public static int mainCLI(
      String[] args, OutputStream outStream, OutputStream errStream, Supplier<TempLogFile> tempLogFile)
  {
    final PrintStream out = NullOutputStream.wrapOrNullStream(outStream);
    System.setProperty(Constants.CLI_JAVA_PROPERTY, "true");
@@ -349,6 +378,10 @@
    System.setProperty(Constants.CLI_JAVA_PROPERTY, "true");
    final Installer installer = new Installer();
    // The supplier is handed over, not the file: the installer creates it when it starts to
    // run, which is the first point where something can fail. The roads above - a usage
    // error, "already installed", a refused licence, a cancel at the prompt - return without
    // asking for it and leave no log behind (issue #1030).
    installer.setTempLogFile(tempLogFile);
    installer.setUserData(uData);
    installer.setProgressMessageFormatter(formatter);
opendj-server-legacy/src/messages/org/opends/messages/quickSetup.properties
@@ -369,6 +369,10 @@
INFO_GENERAL_SEE_FOR_DETAILS=See %s for a detailed log of this operation.
INFO_GENERAL_PROVIDE_LOG_IN_ERROR=See %s for a detailed log of the failed operation.\
 \nPlease report this error and provide the log file mentioned above.
INFO_GENERAL_LOG_IN_ERROR_MISSING=The log file %s of the failed operation is no \
 longer there, so it cannot be provided with the error report.
INFO_GENERAL_LOG_IN_ERROR_UNREADABLE=The log file %s of the failed operation \
 could not be read: %s
INFO_GENERAL_SERVER_STARTED=started
INFO_GENERAL_SERVER_STOPPED=stopped
INFO_GENERAL_WARNING=Warning
@@ -731,9 +735,6 @@
INFO_SERVER_SETTINGS_STEP=Server Settings
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED=%n%nThe graphical Setup launch \
 failed.%n%nLaunching command line setup...
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED_DETAILS=%n%nThe graphical Setup \
 launch failed. Check file %s for more details.%n%nLaunching command line \
 setup...
INFO_SETUP_LAUNCHER_LAUNCHING_GUI=Launching graphical setup...
INFO_SHOW_EXCEPTION_DETAILS=Show Details
# Only translate if the image is specific to the local
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_de.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2010 Sun Microsystems, Inc.
# Portions Copyright 2010-2016 ForgeRock AS.
# Portions Copyright 2026 3A Systems, LLC.
@@ -491,7 +492,6 @@
INFO_SERVER_SETTINGS_PANEL_TITLE=Servereinstellungen
INFO_SERVER_SETTINGS_STEP=Servereinstellungen
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED=%n%nSetup-Start \u00fcber grafische Benutzeroberfl\u00e4che ist fehlgeschlagen.%n%nSetup \u00fcber Befehlszeile wird gestartet...
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED_DETAILS=%n%nSetup-Start \u00fcber grafische Benutzeroberfl\u00e4che ist fehlgeschlagen.  Weitere Details finden Sie in der Datei %s.%n%nSetup \u00fcber Befehlszeile wird gestartet...
INFO_SETUP_LAUNCHER_LAUNCHING_GUI=Setup \u00fcber grafische Benutzeroberfl\u00e4che wird gestartet...
INFO_SHOW_EXCEPTION_DETAILS=Details anzeigen
# Only translate if the image is specific to the local
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_es.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2010 Sun Microsystems, Inc.
# Portions Copyright 2010-2016 ForgeRock AS.
# Portions Copyright 2026 3A Systems, LLC.
@@ -491,7 +492,6 @@
INFO_SERVER_SETTINGS_PANEL_TITLE=Configuraci\u00f3n del servidor
INFO_SERVER_SETTINGS_STEP=Configuraci\u00f3n del servidor
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED=%n%nError al iniciar la instalaci\u00f3n gr\u00e1fica.%n%nIniciando la instalaci\u00f3n mediante l\u00ednea de comandos...
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED_DETAILS=%n%nError al iniciar la instalaci\u00f3n gr\u00e1fica.  Consulte el archivo %s para obtener m\u00e1s informaci\u00f3n.%n%nIniciando la instalaci\u00f3n mediante l\u00ednea de comandos...
INFO_SETUP_LAUNCHER_LAUNCHING_GUI=Iniciando configuraci\u00f3n gr\u00e1fica...
INFO_SHOW_EXCEPTION_DETAILS=Mostrar detalles
# Only translate if the image is specific to the local
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_fr.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2010 Sun Microsystems, Inc.
# Portions Copyright 2010-2016 ForgeRock AS.
# Portions Copyright 2026 3A Systems, LLC.
@@ -492,7 +493,6 @@
INFO_SERVER_SETTINGS_PANEL_TITLE=Param\u00e8tres du serveur
INFO_SERVER_SETTINGS_STEP=Param\u00e8tres du serveur
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED=%n%n\u00c9chec du lancement de la configuration graphique.%n%nD\u00e9marrage de la configuration via la ligne de commande...
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED_DETAILS=%n%n\u00c9chec du lancement de la configuration graphique.  Reportez-vous au fichier %s pour plus d'informations.%n%nD\u00e9marrage de la configuration via la ligne de commande...
INFO_SETUP_LAUNCHER_LAUNCHING_GUI=D\u00e9marrage de la configuration graphique...
INFO_SHOW_EXCEPTION_DETAILS=Afficher les d\u00e9tails
# Only translate if the image is specific to the local
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_ja.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2010 Sun Microsystems, Inc.
# Portions Copyright 2010-2016 ForgeRock AS.
# Portions Copyright 2026 3A Systems, LLC.
@@ -491,7 +492,6 @@
INFO_SERVER_SETTINGS_PANEL_TITLE=\u30b5\u30fc\u30d0\u30fc\u8a2d\u5b9a
INFO_SERVER_SETTINGS_STEP=\u30b5\u30fc\u30d0\u30fc\u8a2d\u5b9a
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED=%n%n\u30b0\u30e9\u30d5\u30a3\u30ab\u30eb\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002%n%n\u30b3\u30de\u30f3\u30c9\u884c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u8d77\u52d5\u3057\u3066\u3044\u307e\u3059...
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED_DETAILS=%n%n\u30b0\u30e9\u30d5\u30a3\u30ab\u30eb\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002  \u8a73\u7d30\u306f\u30d5\u30a1\u30a4\u30eb %s \u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002%n%n\u30b3\u30de\u30f3\u30c9\u884c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u8d77\u52d5\u3057\u3066\u3044\u307e\u3059...
INFO_SETUP_LAUNCHER_LAUNCHING_GUI=\u30b0\u30e9\u30d5\u30a3\u30ab\u30eb\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u8d77\u52d5\u3057\u3066\u3044\u307e\u3059...
INFO_SHOW_EXCEPTION_DETAILS=\u8a73\u7d30\u3092\u8868\u793a
# Only translate if the image is specific to the local
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_ko.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2009 Sun Microsystems, Inc.
# Portions Copyright 2010-2016 ForgeRock AS.
# Portions Copyright 2026 3A Systems, LLC.
@@ -470,7 +471,6 @@
INFO_SERVER_SETTINGS_PANEL_TITLE=\uc11c\ubc84 \uc124\uc815
INFO_SERVER_SETTINGS_STEP=\uc11c\ubc84 \uc124\uc815
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED=%n%n\uadf8\ub798\ud53d \uc124\uce58\ub97c \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.%n%n\uba85\ub839\uc904 \uc124\uce58\ub97c \uc2dc\uc791\ud558\ub294 \uc911...
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED_DETAILS=%n%n\uadf8\ub798\ud53d \uc124\uce58\ub97c \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.  \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 %s \ud30c\uc77c\uc744 \ucc38\uc870\ud558\uc2ed\uc2dc\uc624.%n%n\uba85\ub839\uc904 \uc124\uce58\ub97c \uc2dc\uc791\ud558\ub294 \uc911...
INFO_SETUP_LAUNCHER_LAUNCHING_GUI=\uadf8\ub798\ud53d \uc124\uce58\ub97c \uc2dc\uc791\ud558\ub294 \uc911...
INFO_SHOW_EXCEPTION_DETAILS=\uc138\ubd80 \uc815\ubcf4 \ud45c\uc2dc
# Only translate if the image is specific to the local
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_pl.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2009 Sun Microsystems, Inc.
# Portions Copyright 2010-2016 ForgeRock AS.
# Portions Copyright 2026 3A Systems, LLC.
@@ -63,7 +64,6 @@
INFO_CERTIFICATE_LABEL=Certyfikat:
ERR_CANNOT_CONNECT_TO_LOCAL_GENERIC=Could not connect to the server.  Check that the server is running and that the provided credentials are valid.%nSzczeg\u00f3\u0142y b\u0142\u0119du:%n%s
INFO_CERTIFICATE_DIALOG_ACCEPT_FOR_SESSION_BUTTON_TOOLTIP=Zamknij to okienko i zaakceptuj certyfikat tylko dla tej sesji.
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED_DETAILS=%n%nThe graphical Setup launch failed.  Sprawd\u017a plik %s aby uzyska\u0107 szczeg\u00f3\u0142owe informacje.%n%nLaunching command line setup...
INFO_CANCEL_BUTTON_LABEL=Anuluj
INFO_CERTIFICATE_DIALOG_ACCEPT_PERMANENTLY_BUTTON_TOOLTIP=Zamknij to okienko i zaakceptuj certyfikat na sta\u0142e.
INFO_CANNOT_USE_DEFAULT_ADMIN_CONNECTOR_PORT=Nie mo\u017cna u\u017cy\u0107 4444. Port w u\u017cyciu lub nieautoryzowany u\u017cytkownik.
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_zh_CN.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2010 Sun Microsystems, Inc.
# Portions Copyright 2010-2016 ForgeRock AS.
# Portions Copyright 2026 3A Systems, LLC.
@@ -491,7 +492,6 @@
INFO_SERVER_SETTINGS_PANEL_TITLE=\u670d\u52a1\u5668\u8bbe\u7f6e
INFO_SERVER_SETTINGS_STEP=\u670d\u52a1\u5668\u8bbe\u7f6e
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED=%n%n\u542f\u52a8\u56fe\u5f62\u5316\u5b89\u88c5\u5931\u8d25\u3002%n%n\u6b63\u5728\u542f\u52a8\u547d\u4ee4\u884c\u5b89\u88c5...
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED_DETAILS=%n%n\u542f\u52a8\u56fe\u5f62\u5316\u5b89\u88c5\u5931\u8d25\u3002\u8bf7\u68c0\u67e5\u6587\u4ef6 %s\uff0c\u4ee5\u83b7\u53d6\u66f4\u591a\u7684\u8be6\u7ec6\u4fe1\u606f\u3002%n%n\u6b63\u5728\u542f\u52a8\u547d\u4ee4\u884c\u5b89\u88c5...
INFO_SETUP_LAUNCHER_LAUNCHING_GUI=\u6b63\u5728\u542f\u52a8\u56fe\u5f62\u5316\u5b89\u88c5...
INFO_SHOW_EXCEPTION_DETAILS=\u663e\u793a\u8be6\u7ec6\u8d44\u6599
# Only translate if the image is specific to the local
opendj-server-legacy/src/messages/org/opends/messages/quickSetup_zh_TW.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2009 Sun Microsystems, Inc.
# Portions Copyright 2010-2016 ForgeRock AS.
# Portions Copyright 2026 3A Systems, LLC.
@@ -471,7 +472,6 @@
INFO_SERVER_SETTINGS_PANEL_TITLE=\u4f3a\u670d\u5668\u8a2d\u5b9a
INFO_SERVER_SETTINGS_STEP=\u4f3a\u670d\u5668\u8a2d\u5b9a
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED=%n%n\u5716\u5f62\u5316\u8a2d\u5b9a\u555f\u52d5\u5931\u6557\u3002%n%n\u6b63\u5728\u555f\u52d5\u6307\u4ee4\u884c\u8a2d\u5b9a...
INFO_SETUP_LAUNCHER_GUI_LAUNCHED_FAILED_DETAILS=%n%n\u5716\u5f62\u5316\u8a2d\u5b9a\u555f\u52d5\u5931\u6557\u3002\u6aa2\u67e5\u6a94\u6848 %s \u4ee5\u53d6\u5f97\u66f4\u591a\u8a73\u7d30\u8cc7\u6599\u3002%n%n\u6b63\u5728\u555f\u52d5\u6307\u4ee4\u884c\u8a2d\u5b9a...
INFO_SETUP_LAUNCHER_LAUNCHING_GUI=\u6b63\u5728\u555f\u52d5\u5716\u5f62\u5316\u8a2d\u5b9a...
INFO_SHOW_EXCEPTION_DETAILS=\u986f\u793a\u8a73\u7d30\u8cc7\u8a0a
# Only translate if the image is specific to the local
opendj-server-legacy/src/test/java/org/opends/quicksetup/LauncherTest.java
New file
@@ -0,0 +1,209 @@
/*
 * 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.
 */
package org.opends.quicksetup;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import org.forgerock.i18n.LocalizableMessage;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.testng.SkipException;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import com.forgerock.opendj.cli.ArgumentParser;
/**
 * Tests when a launcher creates its temporary log file.
 * <p>
 * Nothing removes that file unless the operation it belongs to succeeds, so a road which
 * attempts nothing - {@code setup --help}, {@code --version}, a usage error - must not create
 * one, nor the directory it would live in (issue #1030).
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "quicksetup" }, sequential = true)
public class LauncherTest extends DirectoryServerTestCase
{
  private static final String PREFIX = "opendj-setup-";
  private File tempDir;
  private final List<TempLogFile> created = new ArrayList<>();
  @BeforeClass
  public void setUp() throws IOException
  {
    tempDir = TestCaseUtils.createTemporaryDirectory("launcherTest");
  }
  @AfterClass
  public void tearDown() throws IOException
  {
    for (TempLogFile logFile : created)
    {
      logFile.deleteLogFileAfterSuccess();
    }
    TestCaseUtils.deleteDirectory(tempDir);
  }
  @Test
  public void testBuildingALauncherLeavesNothingOnDisk() throws Exception
  {
    final File instance = new File(tempDir, "not-yet-laid-down");
    final File logs = new File(instance, "logs");
    final TestLauncher launcher = new TestLauncher(logs);
    assertFalse(launcher.hasTempLogFile(), "the log must wait for a road that can fail");
    assertFalse(logs.exists(), logs.getPath());
    assertFalse(instance.exists(), instance.getPath());
  }
  @Test
  public void testAskingForTheLogCreatesItOnce() throws Exception
  {
    final File logs = new File(tempDir, "asked-for/logs");
    final TestLauncher launcher = new TestLauncher(logs);
    final TempLogFile logFile = launcher.getTempLogFile();
    created.add(logFile);
    assertTrue(logFile.isReadable(), logFile.getPath());
    assertEquals(logFile.getLogFile().getCanonicalFile().getParentFile(), logs.getCanonicalFile());
    assertTrue(launcher.hasTempLogFile());
    assertSame(launcher.getTempLogFile(), logFile, "a second ask must not create a second log");
  }
  /**
   * The wizard road: the splash screen comes up before the user has said anything, so the log
   * cannot be created on the way to it - it is the application that asks for one, when it
   * starts the install. A wizard quit at any step leaves nothing behind.
   */
  @Test
  public void testLaunchingTheGuiCreatesNoLog() throws Exception
  {
    final File instance = new File(tempDir, "quit-at-the-first-step");
    final File logs = new File(instance, "logs");
    final TestLauncher launcher = new TestLauncher(logs);
    // The wizard behind the splash screen quits without installing anything.
    launcher.launchGui(new String[0]);
    assertFalse(launcher.hasTempLogFile(), "the splash screen must not cost a log");
    assertFalse(logs.exists(), logs.getPath());
    assertFalse(instance.exists(), instance.getPath());
    // What the wizard was handed is the launcher's own log, made on the first ask.
    final TempLogFile logFile = launcher.splashLogFile.get();
    created.add(logFile);
    assertTrue(logFile.isReadable(), logFile.getPath());
    assertSame(logFile, launcher.getTempLogFile());
  }
  /**
   * A GUI which does not come up costs no log either, and its reason is not lost for that: it
   * goes into the log as soon as something asks for one - on the headless road, the operation
   * which runs on the command line instead.
   */
  @Test
  public void testAGuiWhichDoesNotComeUpIsLoggedOnceThereIsALog() throws Exception
  {
    if ("true".equalsIgnoreCase(System.getenv("OPENDJ_LOG_TO_STDOUT")))
    {
      // The reason is written to the log, which then goes to stdout and leaves the file empty.
      throw new SkipException("OPENDJ_LOG_TO_STDOUT writes the log to stdout, not to the file");
    }
    final File logs = new File(tempDir, "headless/logs");
    final TestLauncher launcher = new TestLauncher(logs);
    launcher.splashFailure = new IllegalStateException("no display at all");
    assertTrue(launcher.launchGui(new String[0]) != 0, "a GUI which does not come up is reported");
    assertFalse(launcher.hasTempLogFile(), "a GUI which does not come up must not cost a log");
    assertFalse(logs.exists(), logs.getPath());
    final TempLogFile logFile = launcher.getTempLogFile();
    created.add(logFile);
    logFile.writer.shutdown();
    final String contents = logFile.readContents();
    assertTrue(contents.contains("no display at all"), contents);
  }
  /** A launcher with nothing in it but the log file behaviour under test. */
  private static final class TestLauncher extends Launcher
  {
    /** What {@link Launcher#launchGui(String[])} handed the splash screen. */
    private Supplier<TempLogFile> splashLogFile;
    /** What the splash screen throws, if it is not to come up. */
    private RuntimeException splashFailure;
    TestLauncher(final File tempLogFileDirectory)
    {
      super(new String[0], PREFIX, tempLogFileDirectory);
    }
    @Override
    void startSplashScreen(final Supplier<TempLogFile> tempLogFile, final String[] args)
    {
      // No display here, and no install either: the wizard is quit at its first step - or it
      // does not come up at all.
      splashLogFile = tempLogFile;
      if (splashFailure != null)
      {
        throw splashFailure;
      }
    }
    @Override
    public ArgumentParser getArgumentParser()
    {
      return null;
    }
    @Override
    protected LocalizableMessage getFrameTitle()
    {
      return LocalizableMessage.raw("test");
    }
    @Override
    protected CliApplication createCliApplication()
    {
      return null;
    }
    @Override
    protected void willLaunchGui()
    {
      // nothing is launched here
    }
    @Override
    protected void guiLaunchFailed()
    {
      // nothing is launched here
    }
  }
}
opendj-server-legacy/src/test/java/org/opends/quicksetup/TempLogFileTest.java
New file
@@ -0,0 +1,220 @@
/*
 * 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.
 */
package org.opends.quicksetup;
import static java.nio.charset.Charset.defaultCharset;
import static java.nio.file.StandardOpenOption.APPEND;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.loggers.ErrorLogger;
import org.testng.SkipException;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
/**
 * Tests that a {@link TempLogFile} can be placed in a directory of the caller's choosing (the
 * instance {@code logs/} directory for setup, so that {@code start-ds} does not sweep it away
 * with the rest of {@code tmp/}, see issue #1030), and that it tells whether the file is still
 * there to be read.
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "quicksetup" }, sequential = true)
public class TempLogFileTest extends DirectoryServerTestCase
{
  private static final String PREFIX = "opendj-setup-";
  private File tempDir;
  private final List<TempLogFile> created = new ArrayList<>();
  @BeforeClass
  public void setUp() throws IOException
  {
    tempDir = TestCaseUtils.createTemporaryDirectory("tempLogFileTest");
  }
  @AfterClass
  public void tearDown() throws IOException
  {
    for (TempLogFile logFile : created)
    {
      logFile.deleteLogFileAfterSuccess();
    }
    TestCaseUtils.deleteDirectory(tempDir);
  }
  private TempLogFile track(TempLogFile logFile)
  {
    created.add(logFile);
    return logFile;
  }
  private static File parentOf(TempLogFile logFile) throws IOException
  {
    return logFile.getLogFile().getCanonicalFile().getParentFile();
  }
  /** The directory does not exist before setup lays the instance down: it has to be created. */
  @Test
  public void testLogFileIsCreatedInTheRequestedDirectory() throws Exception
  {
    final File logs = new File(tempDir, "not-yet-laid-down/logs");
    assertFalse(logs.exists());
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, logs));
    assertTrue(logFile.isEnabled());
    assertTrue(logFile.isReadable());
    assertEquals(parentOf(logFile), logs.getCanonicalFile());
    assertTrue(logFile.getLogFile().getName().startsWith(PREFIX), logFile.getPath());
    assertTrue(logFile.getLogFile().getName().endsWith(".log"), logFile.getPath());
  }
  @Test
  public void testReadContentsReturnsWhatIsInTheFile() throws Exception
  {
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, new File(tempDir, "logs")));
    // The log's own stream is not in append mode and sits at the end of its own bytes, so a
    // record written after the marker would be written over it: shut the writer first, and
    // the marker is the last thing in the file whatever else the JVM logs.
    logFile.writer.shutdown();
    final String marker = "the last line written before the failure";
    Files.write(logFile.getLogFile().toPath(), (marker + "\n").getBytes(defaultCharset()), APPEND);
    assertTrue(logFile.readContents().endsWith(marker + "\n"));
  }
  /** Being enabled means messages are logged; being readable means the file is there to hand over. */
  @Test
  public void testIsReadableFollowsTheFileNotTheLogger() throws Exception
  {
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, new File(tempDir, "logs")));
    assertTrue(logFile.isReadable());
    // Not File.delete(): the writer still holds the file, and Windows does not delete a file
    // that is open. deleteLogFileAfterSuccess() shuts the writer first, as setup does.
    logFile.deleteLogFileAfterSuccess();
    assertFalse(logFile.getLogFile().exists());
    assertTrue(logFile.isEnabled());
    assertFalse(logFile.isReadable());
    try
    {
      logFile.readContents();
      fail("reading a deleted log must fail");
    }
    catch (IOException expected)
    {
      // the caller reports it instead of promising the file
    }
  }
  /** A directory where the log was is not a log: there is nothing to hand over either. */
  @Test
  public void testADirectoryAtTheLogPathIsNotReadable() throws Exception
  {
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, new File(tempDir, "logs")));
    logFile.deleteLogFileAfterSuccess();
    assertTrue(logFile.getLogFile().mkdir());
    assertTrue(Files.isReadable(logFile.getLogFile().toPath()));
    assertFalse(logFile.isReadable());
  }
  @Test
  public void testNoDirectoryMeansTheTemporaryDirectory() throws Exception
  {
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, null));
    assertTrue(logFile.isEnabled());
    assertEquals(parentOf(logFile), new File(System.getProperty("java.io.tmpdir")).getCanonicalFile());
  }
  /** A directory that cannot be used must not cost the log: fall back to the temporary directory. */
  @Test
  public void testUnusableDirectoryFallsBackToTheTemporaryDirectory() throws Exception
  {
    final File notADirectory = new File(tempDir, "not-a-directory");
    assertTrue(notADirectory.createNewFile());
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, notADirectory));
    assertTrue(logFile.isEnabled());
    assertTrue(logFile.isReadable());
    assertNotEquals(parentOf(logFile), notADirectory.getCanonicalFile());
    assertEquals(parentOf(logFile), new File(System.getProperty("java.io.tmpdir")).getCanonicalFile());
    // Why the log is not where it was asked for is warned about once there is a log to carry
    // the warning: at the point the directory failed, no publisher was installed yet.
    assumeTheLogGoesToTheFile();
    logFile.writer.shutdown();
    final String contents = logFile.readContents();
    assertTrue(contents.contains("falling back to the temporary directory"), contents);
    assertTrue(contents.contains(notADirectory.toString()), contents);
  }
  /**
   * The publishers the constructor puts on the logger singletons come off again with the log:
   * the singletons outlive the file, and what a leaked publisher is handed goes to a closed
   * stream and is swallowed.
   */
  @Test
  public void testDeletingTheLogTakesItsPublisherOffTheLogger() throws Exception
  {
    final int before = errorLogPublishers();
    final TempLogFile logFile = track(TempLogFile.newTempLogFile(PREFIX, new File(tempDir, "logs")));
    assertEquals(errorLogPublishers(), before + 1, "the log logs through a publisher of its own");
    logFile.deleteLogFileAfterSuccess();
    assertEquals(errorLogPublishers(), before, "the publisher must not outlive the log");
  }
  /** How many publishers the error logger holds; {@code getLogPublishers()} is protected. */
  private static int errorLogPublishers() throws Exception
  {
    final Method getLogPublishers = ErrorLogger.class.getDeclaredMethod("getLogPublishers");
    getLogPublishers.setAccessible(true);
    return ((Collection<?>) getLogPublishers.invoke(ErrorLogger.getInstance())).size();
  }
  /**
   * {@code OPENDJ_LOG_TO_STDOUT} sends the records to stdout and leaves the file empty, so a
   * case which reads the log back has nothing to look at.
   */
  private static void assumeTheLogGoesToTheFile()
  {
    if ("true".equalsIgnoreCase(System.getenv("OPENDJ_LOG_TO_STDOUT")))
    {
      throw new SkipException("OPENDJ_LOG_TO_STDOUT writes the log to stdout, not to the file");
    }
  }
}
opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/InstallerTest.java
New file
@@ -0,0 +1,298 @@
/*
 * 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.
 */
package org.opends.quicksetup.installer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.opends.messages.QuickSetupMessages.*;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.forgerock.i18n.LocalizableMessage;
import org.opends.quicksetup.TempLogFile;
import org.opends.quicksetup.UserData;
import org.opends.quicksetup.event.ProgressUpdateEvent;
import org.opends.quicksetup.event.ProgressUpdateListener;
import org.opends.quicksetup.util.PlainTextProgressMessageFormatter;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
/**
 * Tests what a failed installation tells its listeners about the log file.
 * <p>
 * This report is the one place the diagnosis of a failed setup lives, and it used to promise
 * the file without looking at it - printing a {@code NoSuchFileException} stack when the file
 * was gone (issue #1030). The roads below are the whole of that decision.
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "quicksetup" }, sequential = true)
public class InstallerTest extends DirectoryServerTestCase
{
  private static final String PREFIX = "opendj-setup-";
  /** Marks a line the report sends out as a warning, see {@link MarkedArmsFormatter}. */
  private static final String WARNING = "[W]";
  /** Marks a line the report sends out as progress. */
  private static final String PROGRESS = "[P]";
  private File tempDir;
  private final List<TempLogFile> created = new ArrayList<>();
  @BeforeClass
  public void setUp() throws IOException
  {
    tempDir = TestCaseUtils.createTemporaryDirectory("installerTest");
  }
  @AfterClass
  public void tearDown() throws IOException
  {
    for (TempLogFile logFile : created)
    {
      logFile.deleteLogFileAfterSuccess();
    }
    TestCaseUtils.deleteDirectory(tempDir);
  }
  /** A log that is there is named, as before, and named as progress rather than as a warning. */
  @Test
  public void testAReadableLogIsHandedOver() throws Exception
  {
    final TempLogFile logFile = newLogFile();
    final String report = reportOf(logFile);
    assertContains(report, PROGRESS, INFO_GENERAL_PROVIDE_LOG_IN_ERROR.get(logFile.getPath()));
    assertDoesNotContain(report, INFO_GENERAL_LOG_IN_ERROR_MISSING.get(logFile.getPath()));
  }
  /**
   * The contents of the log reach the report: attaching the file is up to whoever reads it,
   * so the report carries the log itself.
   * <p>
   * What the file holds is not the point here and is mocked away - with
   * {@code OPENDJ_LOG_TO_STDOUT} set, a real log is empty and would pin nothing.
   */
  @Test
  public void testTheContentsOfTheLogReachTheReport() throws Exception
  {
    final String contents = "QuickSetup application launched\nthe last line before the failure\n";
    final TempLogFile logFile = mock(TempLogFile.class);
    when(logFile.isEnabled()).thenReturn(true);
    when(logFile.isReadable()).thenReturn(true);
    when(logFile.getPath()).thenReturn(new File(tempDir, "readable.log").getAbsolutePath());
    when(logFile.readContents()).thenReturn(contents);
    final String report = reportOf(logFile);
    assertTrue(report.contains(contents), report);
  }
  /** A log something else removed is reported as gone, instead of being asked for. */
  @Test
  public void testAMissingLogIsReportedAsMissing() throws Exception
  {
    final TempLogFile logFile = newLogFile();
    final String path = logFile.getPath();
    logFile.deleteLogFileAfterSuccess();
    assertFalse(logFile.isReadable());
    final String report = reportOf(logFile);
    assertContains(report, WARNING, INFO_GENERAL_LOG_IN_ERROR_MISSING.get(path));
    // The line that asks for the file must not go out when there is no file to provide.
    assertDoesNotContain(report, INFO_GENERAL_PROVIDE_LOG_IN_ERROR.get(path));
  }
  /** A log which is there but cannot be read costs the report its contents, not its diagnosis. */
  @Test
  public void testAnUnreadableLogIsReportedAsUnreadable() throws Exception
  {
    final String path = new File(tempDir, "unreadable.log").getAbsolutePath();
    final IOException failure = new IOException("Input/output error");
    final TempLogFile logFile = mock(TempLogFile.class);
    when(logFile.isEnabled()).thenReturn(true);
    when(logFile.isReadable()).thenReturn(true);
    when(logFile.getPath()).thenReturn(path);
    when(logFile.readContents()).thenThrow(failure);
    final String report = reportOf(logFile);
    assertContains(report, WARNING, INFO_GENERAL_LOG_IN_ERROR_UNREADABLE.get(path, failure));
    assertDoesNotContain(report, INFO_GENERAL_LOG_IN_ERROR_MISSING.get(path));
  }
  /** Nothing is said about a log that was never created. */
  @Test
  public void testNoLogMeansNoReport() throws Exception
  {
    final TempLogFile logFile = mock(TempLogFile.class);
    when(logFile.isEnabled()).thenReturn(false);
    assertTrue(reportOf(logFile).isEmpty(), "a launcher without a log has nothing to report");
  }
  /**
   * A cancelled install takes its log with it - and asks for it only when it begins.
   * <p>
   * Nothing names the log on that road - the report belongs to the failure road - and the
   * cancel has just taken the installation back, so a log kept there is a report nobody is
   * ever pointed at (issue #1030). The installer is handed the log the way the wizard and the
   * command line hand it over, as a supplier: nothing asks for it before {@code run()}.
   */
  @Test
  public void testACancelledInstallTakesItsLogWithIt() throws Exception
  {
    final TempLogFile logFile = newLogFile();
    final File instance = new File(tempDir, "cancelled");
    // A locks directory the lock file can be taken in: without it the cancel road reads the
    // server as running and goes off to stop it.
    assertTrue(new File(instance, "locks").mkdirs());
    final TestInstaller installer = new TestInstaller(instance);
    installer.setProgressMessageFormatter(new MarkedArmsFormatter());
    final int[] asked = { 0 };
    installer.setTempLogFile(() -> {
      asked[0]++;
      return logFile;
    });
    installer.setUserData(new UserData());
    installer.cancel();
    assertEquals(asked[0], 0, "nothing is logged before the install begins");
    installer.run();
    assertEquals(asked[0], 1, "the install asks for its log once, as it begins");
    assertEquals(installer.getCurrentProgressStep(), InstallProgressStep.FINISHED_CANCELED);
    assertFalse(logFile.getLogFile().exists(), logFile.getPath());
  }
  /**
   * An application nobody handed a log to has none, and asking for it is not a failure: the
   * command line uninstaller is built that way, its launcher having created the log already.
   */
  @Test
  public void testAnApplicationGivenNoLogHasNone() throws Exception
  {
    assertNull(new TestInstaller(null).openTempLogFile());
  }
  private TempLogFile newLogFile()
  {
    final TempLogFile logFile = TempLogFile.newTempLogFile(PREFIX, new File(tempDir, "logs"));
    created.add(logFile);
    return logFile;
  }
  /** What the listeners of a failed installation are told about the log file. */
  private static String reportOf(final TempLogFile logFile)
  {
    final TestInstaller installer = new TestInstaller(null);
    installer.setProgressMessageFormatter(new MarkedArmsFormatter());
    final StringBuilder report = new StringBuilder();
    installer.addProgressUpdateListener(new ProgressUpdateListener()
    {
      @Override
      public void progressUpdate(final ProgressUpdateEvent ev)
      {
        if (ev.getNewLogs() != null)
        {
          report.append(ev.getNewLogs());
        }
      }
    });
    installer.setTempLogFile(() -> logFile);
    // The report is made by an install which has begun, and so has asked for its log.
    installer.openTempLogFile();
    installer.notifyListenersOfExistingLogFile();
    return report.toString();
  }
  private static void assertContains(final String report, final String arm, final LocalizableMessage expected)
  {
    assertTrue(report.contains(arm + expected), "expected <" + arm + expected + "> in <" + report + ">");
  }
  private static void assertDoesNotContain(final String report, final LocalizableMessage unexpected)
  {
    assertFalse(report.contains(unexpected.toString()), "unexpected <" + unexpected + "> in <" + report + ">");
  }
  /** An installer which can be told where it lives, and whose log can be asked for. */
  private static final class TestInstaller extends Installer
  {
    /** The installation, or {@code null} for the one the class path names. */
    private final File instance;
    TestInstaller(final File instance)
    {
      this.instance = instance;
    }
    /**
     * An installation under the temporary directory rather than the one the class path names:
     * the installer takes both paths from there, and in a test run there is none.
     */
    @Override
    public String getInstallationPath()
    {
      return instance != null ? instance.getAbsolutePath() : super.getInstallationPath();
    }
    @Override
    public String getInstancePath()
    {
      return instance != null ? instance.getAbsolutePath() : super.getInstancePath();
    }
    @Override
    protected TempLogFile openTempLogFile()
    {
      return super.openTempLogFile();
    }
  }
  /**
   * A formatter that marks the arm each line goes out on.
   * <p>
   * The plain text formatter returns warnings and progress messages unchanged - only the
   * wizard's HTML formatter tells them apart - so without a mark a report which says "the log
   * is gone" as an ordinary progress line reads exactly like one which warns about it.
   */
  private static final class MarkedArmsFormatter extends PlainTextProgressMessageFormatter
  {
    @Override
    public LocalizableMessage getFormattedWarning(final LocalizableMessage text, final boolean applyMargin)
    {
      return LocalizableMessage.raw(WARNING + super.getFormattedWarning(text, applyMargin));
    }
    @Override
    public LocalizableMessage getFormattedProgress(final LocalizableMessage text)
    {
      return LocalizableMessage.raw(PROGRESS + super.getFormattedProgress(text));
    }
  }
}
opendj-server-legacy/src/test/java/org/opends/server/tools/InstallDSTest.java
New file
@@ -0,0 +1,131 @@
/*
 * 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.
 */
package org.opends.server.tools;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import java.io.File;
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.opends.quicksetup.Constants;
import org.opends.quicksetup.Installation;
import org.opends.quicksetup.TempLogFile;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.testng.annotations.Test;
/**
 * Tests when the CLI setup asks for its log file.
 * <p>
 * Nothing removes that file unless the install succeeds, so the roads which install nothing -
 * a usage request, a usage error, a server which is configured already, a refused licence, a
 * cancel at the prompt - must not ask for one: asking creates it, and the instance
 * {@code logs/} directory with it (issue #1030).
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "tools" }, sequential = true)
public class InstallDSTest extends DirectoryServerTestCase
{
  /** {@code InstallReturnCode.SUCCESSFUL_NOP}: the usage was displayed and nothing was done. */
  private static final int SUCCESSFUL_NOP = 0;
  /** {@code InstallReturnCode.ERROR_USER_DATA}: the arguments could not be parsed. */
  private static final int ERROR_USER_DATA = 2;
  /** {@code InstallReturnCode.ERROR_SERVER_ALREADY_INSTALLED}: the server is configured already. */
  private static final int ERROR_SERVER_ALREADY_INSTALLED = 3;
  /** {@code setup --help} displays the usage and returns before anything can fail. */
  @Test
  public void testAUsageRequestAsksForNoLog() throws Exception
  {
    assertAsksForNoLog(SUCCESSFUL_NOP, "--help");
  }
  /** An argument the parser does not know is reported, and no install is attempted. */
  @Test
  public void testAUsageErrorAsksForNoLog() throws Exception
  {
    assertAsksForNoLog(ERROR_USER_DATA, "--no-such-option");
  }
  /**
   * A server which is configured already is reported as such, and no install is attempted.
   * <p>
   * This is the first road past the argument parser - the licence and the prompts come after
   * it - so it is the one which tells a log asked for as soon as the arguments are known from
   * one asked for where the install begins.
   */
  @Test
  public void testAServerWhichIsConfiguredAlreadyAsksForNoLog() throws Exception
  {
    // The installation the setup looks at: one whose configuration directory holds something.
    // Neither the class path of a test run nor the server the tests run against will do - the
    // former is no installation, and the latter is not to be taken as one - so the setup is
    // pointed at a directory of its own. A locks directory keeps it from reading as running.
    final File instance = TestCaseUtils.createTemporaryDirectory("installDSTest");
    assertTrue(new File(instance, "locks").mkdirs());
    assertTrue(new File(instance, "config").mkdirs());
    assertTrue(new File(instance, "config/config.ldif").createNewFile());
    // Installation.getLocal() keeps what it resolved first in a static field.
    final Field local = Installation.class.getDeclaredField("local");
    local.setAccessible(true);
    final Object previous = local.get(null);
    local.set(null, new Installation(instance, instance));
    try
    {
      assertAsksForNoLog(ERROR_SERVER_ALREADY_INSTALLED);
    }
    finally
    {
      local.set(null, previous);
      TestCaseUtils.deleteDirectory(instance);
    }
  }
  /**
   * Runs the CLI setup with a supplier which counts the asks and hands out nothing, so that a
   * road which asks for the log is red here rather than a file left behind on a real run.
   */
  private static void assertAsksForNoLog(final int expectedReturnCode, final String... args)
  {
    final AtomicInteger asked = new AtomicInteger();
    final Supplier<TempLogFile> countingSupplier = () -> {
      asked.incrementAndGet();
      return null;
    };
    // mainCLI() sets this property for the run; put back what the rest of the JVM had.
    final String cliProperty = System.getProperty(Constants.CLI_JAVA_PROPERTY);
    try
    {
      assertEquals(InstallDS.mainCLI(args, null, null, countingSupplier), expectedReturnCode);
      assertEquals(asked.get(), 0, "a run which installs nothing must not create a log");
    }
    finally
    {
      if (cliProperty != null)
      {
        System.setProperty(Constants.CLI_JAVA_PROPERTY, cliProperty);
      }
      else
      {
        System.clearProperty(Constants.CLI_JAVA_PROPERTY);
      }
    }
  }
}