From 6e6cbc7128ebc845f968ae95be36be29a0a05ef8 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 05 Oct 2026 12:41:43 +0000
Subject: [PATCH] [#1160] Report what start-ds printed when the server fails to start, and dump the quicksetup test instance's logs on a failed build (#1169)

---
 opendj-server-legacy/src/main/java/org/opends/quicksetup/util/ServerController.java |   71 +++++++++++++++++++++++++++++++----
 1 files changed, 63 insertions(+), 8 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/quicksetup/util/ServerController.java b/opendj-server-legacy/src/main/java/org/opends/quicksetup/util/ServerController.java
index aa808db..acb6ff6 100644
--- a/opendj-server-legacy/src/main/java/org/opends/quicksetup/util/ServerController.java
+++ b/opendj-server-legacy/src/main/java/org/opends/quicksetup/util/ServerController.java
@@ -21,7 +21,9 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Deque;
 import java.util.List;
 import java.util.Map;
 
@@ -58,6 +60,12 @@
 
   private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
 
+  /** How many of the last lines printed by the start command a failed start reports. */
+  private static final int MAX_REPORTED_START_LINES = 100;
+
+  /** How long a failed start waits for the readers to drain what the start command printed. */
+  private static final long START_OUTPUT_DRAIN_TIMEOUT_MS = 5000;
+
   private Application application;
 
   private Installation installation;
@@ -331,7 +339,8 @@
 
       try
       {
-        startServerViaAnotherProcess();
+        // Without suppression the readers have already shown every line to the listeners.
+        startServerViaAnotherProcess(suppressOutput || application == null);
 
         if (verifyCanConnect)
         {
@@ -358,7 +367,8 @@
     }
   }
 
-  private void startServerViaAnotherProcess() throws IOException, InterruptedException, ApplicationException
+  private void startServerViaAnotherProcess(boolean reportOutput)
+      throws IOException, InterruptedException, ApplicationException
   {
     logger.info(LocalizableMessage.raw("starting server"));
 
@@ -381,8 +391,12 @@
     String startedId = getStartedId();
     Process process = pb.start();
 
-    StartReader errReader = new StartReader(process.getErrorStream(), startedId, true);
-    StartReader outputReader = new StartReader(process.getInputStream(), startedId, false);
+    // When the start fails, what start-ds printed (the server.out of a server that stopped
+    // during its initialization, or the reason it was not launched) is the only account of
+    // the cause that reaches the caller, so both readers keep its last lines (issue #1160).
+    Deque<String> outputTail = new ArrayDeque<>();
+    StartReader errReader = new StartReader(process.getErrorStream(), startedId, true, outputTail);
+    StartReader outputReader = new StartReader(process.getInputStream(), startedId, false, outputTail);
 
     int returnValue = process.waitFor();
 
@@ -390,7 +404,12 @@
 
     if (returnValue != 0)
     {
-      throw new ApplicationException(ReturnCode.START_ERROR, INFO_ERROR_STARTING_SERVER_CODE.get(returnValue), null);
+      // The readers may still be draining the lines start-ds printed just before it exited.
+      errReader.join(START_OUTPUT_DRAIN_TIMEOUT_MS);
+      outputReader.join(START_OUTPUT_DRAIN_TIMEOUT_MS);
+      throw new ApplicationException(ReturnCode.START_ERROR, reportOutput
+          ? getStartFailedMessage(returnValue, outputTail)
+          : INFO_ERROR_STARTING_SERVER_CODE.get(returnValue), null);
     }
     if (outputReader.isFinished())
     {
@@ -596,6 +615,20 @@
     return helper.getStartedId();
   }
 
+  private static LocalizableMessage getStartFailedMessage(int returnValue, Deque<String> outputTail)
+  {
+    // The header is the message a failed start has always reported, which is translated.
+    LocalizableMessageBuilder mb = new LocalizableMessageBuilder(INFO_ERROR_STARTING_SERVER_CODE.get(returnValue));
+    synchronized (outputTail)
+    {
+      if (!outputTail.isEmpty())
+      {
+        mb.append(INFO_ERROR_STARTING_SERVER_OUTPUT.get(String.join(System.lineSeparator(), outputTail)));
+      }
+    }
+    return mb.toMessage();
+  }
+
   /**
    * This class is used to read the standard error and standard output of the
    * Start process.
@@ -613,6 +646,8 @@
 
     private boolean isFirstLine;
 
+    private final Thread thread;
+
     /**
      * The protected constructor.
      * @param stream the stream of the start process to read.
@@ -620,9 +655,11 @@
      * the start is over or not.
      * @param isError a boolean indicating whether the stream
      * corresponds to the standard error or to the standard output.
+     * @param outputTail the last lines read from both streams of the start process,
+     * which this reader appends to; it is guarded by its own monitor.
      */
     public StartReader(final InputStream stream, final String startedId,
-        final boolean isError)
+        final boolean isError, final Deque<String> outputTail)
     {
       final LocalizableMessage errorTag =
               isError ?
@@ -631,7 +668,7 @@
 
       isFirstLine = true;
 
-      Thread t = new Thread(new Runnable()
+      thread = new Thread(new Runnable()
       {
         @Override
         public void run()
@@ -662,6 +699,14 @@
                 isFirstLine = false;
               }
               logger.info(LocalizableMessage.raw("server: " + line));
+              synchronized (outputTail)
+              {
+                if (outputTail.size() == MAX_REPORTED_START_LINES)
+                {
+                  outputTail.removeFirst();
+                }
+                outputTail.addLast(line);
+              }
               if (line.toLowerCase().contains("=" + startedId))
               {
                 isFinished = true;
@@ -680,7 +725,17 @@
           isFinished = true;
         }
       });
-      t.start();
+      thread.start();
+    }
+
+    /**
+     * Waits for this reader to reach the end of its stream.
+     * @param timeoutMillis the longest time to wait, in milliseconds.
+     * @throws InterruptedException if the current thread is interrupted while waiting.
+     */
+    public void join(long timeoutMillis) throws InterruptedException
+    {
+      thread.join(timeoutMillis);
     }
 
     /**

--
Gitblit v1.10.0