From 0138b5924ff14ef709398bf878dbb9bf2a0b7c33 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 23 Sep 2026 13:05:17 +0000
Subject: [PATCH] [#1030] Keep the setup log out of the way of start-ds, and say when it is gone (#1032)

---
 opendj-server-legacy/src/main/java/org/opends/guitools/uninstaller/UninstallLauncher.java |    5 
 opendj-server-legacy/src/main/java/org/opends/quicksetup/Application.java                 |   49 +
 opendj-server-legacy/src/messages/org/opends/messages/quickSetup_es.properties            |    2 
 opendj-server-legacy/src/test/java/org/opends/quicksetup/TempLogFileTest.java             |  220 +++++++++
 opendj-server-legacy/src/main/java/org/opends/quicksetup/Installation.java                |    2 
 opendj-server-legacy/resource/bin/start-ds.bat                                            |   20 
 opendj-server-legacy/src/messages/org/opends/messages/quickSetup_ja.properties            |    2 
 opendj-server-legacy/src/main/java/org/opends/quicksetup/SplashScreen.java                |   14 
 opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java         |   51 +
 opendj-server-legacy/src/messages/org/opends/messages/quickSetup_zh_CN.properties         |    2 
 opendj-server-legacy/src/messages/org/opends/messages/quickSetup_pl.properties            |    2 
 opendj-server-legacy/src/messages/org/opends/messages/quickSetup_fr.properties            |    2 
 opendj-server-legacy/src/messages/org/opends/messages/quickSetup.properties               |    7 
 opendj-server-legacy/src/test/java/org/opends/server/tools/InstallDSTest.java             |  131 +++++
 opendj-server-legacy/src/main/java/org/opends/quicksetup/ui/QuickSetup.java               |    7 
 opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/SetupLauncher.java     |   42 +
 opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/InstallerTest.java     |  298 ++++++++++++
 opendj-server-legacy/src/main/java/org/opends/quicksetup/TempLogFile.java                 |  102 ++++
 opendj-server-legacy/src/main/java/org/opends/quicksetup/Launcher.java                    |  141 ++++-
 opendj-server-legacy/src/test/java/org/opends/quicksetup/LauncherTest.java                |  209 ++++++++
 opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDS.java                 |   41 +
 .github/workflows/build.yml                                                               |   29 +
 opendj-server-legacy/src/messages/org/opends/messages/quickSetup_ko.properties            |    2 
 opendj-server-legacy/src/main/java/org/opends/guitools/uninstaller/Uninstaller.java       |    5 
 opendj-server-legacy/resource/bin/start-ds                                                |   12 
 opendj-server-legacy/src/messages/org/opends/messages/quickSetup_zh_TW.properties         |    2 
 opendj-server-legacy/src/messages/org/opends/messages/quickSetup_de.properties            |    2 
 27 files changed, 1,309 insertions(+), 92 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 604e3ff..1e49ed6 100644
--- a/.github/workflows/build.yml
+++ b/.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:
diff --git a/opendj-server-legacy/resource/bin/start-ds b/opendj-server-legacy/resource/bin/start-ds
index 0c965b6..8e3d032 100644
--- a/opendj-server-legacy/resource/bin/start-ds
+++ b/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
diff --git a/opendj-server-legacy/resource/bin/start-ds.bat b/opendj-server-legacy/resource/bin/start-ds.bat
index f591502..905e7e8 100644
--- a/opendj-server-legacy/resource/bin/start-ds.bat
+++ b/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 %*
 
diff --git a/opendj-server-legacy/src/main/java/org/opends/guitools/uninstaller/UninstallLauncher.java b/opendj-server-legacy/src/main/java/org/opends/guitools/uninstaller/UninstallLauncher.java
index 0f714af..301b48b 100644
--- a/opendj-server-legacy/src/main/java/org/opends/guitools/uninstaller/UninstallLauncher.java
+++ b/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
diff --git a/opendj-server-legacy/src/main/java/org/opends/guitools/uninstaller/Uninstaller.java b/opendj-server-legacy/src/main/java/org/opends/guitools/uninstaller/Uninstaller.java
index 808f87b..f5265b8 100644
--- a/opendj-server-legacy/src/main/java/org/opends/guitools/uninstaller/Uninstaller.java
+++ b/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"));
 
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/Application.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/Application.java
index 0a6faf1..4d7745a 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/Application.java
+++ b/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;
   }
 
   /**
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/Installation.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/Installation.java
index dbf983f..0c54f02 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/Installation.java
+++ b/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. */
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/Launcher.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/Launcher.java
index 1fc34e7..c0622ee 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/Launcher.java
+++ b/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()));
         }
       }
     }
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/SplashScreen.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/SplashScreen.java
index e16a3bb..b2c0d01 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/SplashScreen.java
+++ b/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)
     {
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/TempLogFile.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/TempLogFile.java
index ccb1c9e..6f193b8 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/TempLogFile.java
+++ b/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.
    */
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java
index 0d3096c..a512941 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/Installer.java
+++ b/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. */
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/SetupLauncher.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/SetupLauncher.java
index ff6820b..32f91fe 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/SetupLauncher.java
+++ b/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
diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/ui/QuickSetup.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/ui/QuickSetup.java
index 5f122f5..0dc21c9 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/ui/QuickSetup.java
+++ b/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();
 
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDS.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDS.java
index b652428..49931c0 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tools/InstallDS.java
+++ b/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);
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup.properties b/opendj-server-legacy/src/messages/org/opends/messages/quickSetup.properties
index 9a304f6..ccc934d 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup.properties
+++ b/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
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_de.properties b/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_de.properties
index 1e095a2..7817c92 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_de.properties
+++ b/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
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_es.properties b/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_es.properties
index c97b569..221a263 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_es.properties
+++ b/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
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_fr.properties b/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_fr.properties
index b7d4817..2c361dc 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_fr.properties
+++ b/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
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_ja.properties b/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_ja.properties
index 7c3edf3..221a882 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_ja.properties
+++ b/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
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_ko.properties b/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_ko.properties
index 809a5ae..b58c01e 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_ko.properties
+++ b/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
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_pl.properties b/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_pl.properties
index 5b2d828..d2690bc 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_pl.properties
+++ b/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.
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_zh_CN.properties b/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_zh_CN.properties
index c1cd700..c4cbc14 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_zh_CN.properties
+++ b/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
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_zh_TW.properties b/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_zh_TW.properties
index 2308668..6de08a8 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/quickSetup_zh_TW.properties
+++ b/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
diff --git a/opendj-server-legacy/src/test/java/org/opends/quicksetup/LauncherTest.java b/opendj-server-legacy/src/test/java/org/opends/quicksetup/LauncherTest.java
new file mode 100644
index 0000000..1e0de53
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/quicksetup/LauncherTest.java
@@ -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
+    }
+  }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/quicksetup/TempLogFileTest.java b/opendj-server-legacy/src/test/java/org/opends/quicksetup/TempLogFileTest.java
new file mode 100644
index 0000000..8ff7ea5
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/quicksetup/TempLogFileTest.java
@@ -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");
+    }
+  }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/InstallerTest.java b/opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/InstallerTest.java
new file mode 100644
index 0000000..5f6b624
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/quicksetup/installer/InstallerTest.java
@@ -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));
+    }
+  }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/tools/InstallDSTest.java b/opendj-server-legacy/src/test/java/org/opends/server/tools/InstallDSTest.java
new file mode 100644
index 0000000..ce4127e
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/tools/InstallDSTest.java
@@ -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);
+      }
+    }
+  }
+}

--
Gitblit v1.10.0