| New file |
| | |
| | | /* |
| | | * 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.util; |
| | | |
| | | import static com.forgerock.opendj.util.OperatingSystem.*; |
| | | import static org.opends.messages.QuickSetupMessages.INFO_ERROR_STARTING_SERVER_CODE; |
| | | import static org.testng.Assert.*; |
| | | |
| | | import java.io.File; |
| | | import java.io.IOException; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.nio.file.Files; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.opends.quicksetup.Application; |
| | | import org.opends.quicksetup.ApplicationException; |
| | | import org.opends.quicksetup.Installation; |
| | | import org.opends.quicksetup.ProgressStep; |
| | | import org.opends.quicksetup.ReturnCode; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.testng.SkipException; |
| | | import org.testng.annotations.AfterMethod; |
| | | import org.testng.annotations.BeforeMethod; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | | * A failed start reports what the start command printed (issue #1160), unless the listeners of |
| | | * the application have already shown it. The installation is a bare directory whose start command |
| | | * is a stand-in script that prints and exits with 1. |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | @Test(sequential = true) |
| | | public class ServerControllerStartFailureTest extends DirectoryServerTestCase |
| | | { |
| | | private File root; |
| | | |
| | | @BeforeMethod |
| | | public void createInstallation() throws IOException |
| | | { |
| | | root = Files.createTempDirectory("server-controller-start-failure").toFile(); |
| | | } |
| | | |
| | | @AfterMethod(alwaysRun = true) |
| | | public void deleteInstallation() throws ApplicationException |
| | | { |
| | | new FileManager().deleteRecursively(root); |
| | | } |
| | | |
| | | @Test |
| | | public void failedStartReportsBothOutputStreams() throws Exception |
| | | { |
| | | writeStartCommandPrintingToBothStreams(); |
| | | |
| | | String message = startAndExpectFailure(null, false); |
| | | |
| | | assertTrue(message.startsWith(exitCodeMessage()), message); |
| | | assertTrue(message.contains("cause on stdout"), message); |
| | | assertTrue(message.contains("cause on stderr"), message); |
| | | } |
| | | |
| | | @Test |
| | | public void failedStartReportsOnlyTheLastLines() throws Exception |
| | | { |
| | | if (isWindows()) |
| | | { |
| | | writeStartCommand("@for /L %%i in (1,1,150) do @echo L%%iL", "@exit /b 1"); |
| | | } |
| | | else |
| | | { |
| | | writeStartCommand("#!/bin/sh", "i=1", "while [ $i -le 150 ]; do echo \"L${i}L\"; i=$((i+1)); done", "exit 1"); |
| | | } |
| | | |
| | | String message = startAndExpectFailure(null, false); |
| | | |
| | | assertFalse(message.contains("L50L"), message); |
| | | assertTrue(message.contains("L51L"), message); |
| | | assertTrue(message.contains("L150L"), message); |
| | | } |
| | | |
| | | @Test |
| | | public void failedStartReportsWhatIsPrintedAfterTheExit() throws Exception |
| | | { |
| | | if (isWindows()) |
| | | { |
| | | throw new SkipException("needs a child process that outlives the start command"); |
| | | } |
| | | // The child holds the inherited pipes after sh exits, so its line arrives after waitFor(). |
| | | // When the process exits, the JDK drains what is already in a pipe and closes it, unless a |
| | | // reader is blocked in read() on that stream, which the drain then waits for. The first |
| | | // line and the pause before the exit leave the stdout reader blocked there, so that the |
| | | // line of the child is read rather than cut off by the drain. |
| | | writeStartCommand("#!/bin/sh", "echo 'printed before the exit'", "sleep 1", |
| | | "(sleep 1; echo 'printed after the exit') &", "exit 1"); |
| | | |
| | | String message = startAndExpectFailure(null, false); |
| | | |
| | | assertTrue(message.contains("printed before the exit"), message); |
| | | assertTrue(message.contains("printed after the exit"), message); |
| | | } |
| | | |
| | | @Test |
| | | public void silentFailedStartKeepsTheExitCodeMessage() throws Exception |
| | | { |
| | | if (isWindows()) |
| | | { |
| | | writeStartCommand("@exit /b 1"); |
| | | } |
| | | else |
| | | { |
| | | writeStartCommand("#!/bin/sh", "exit 1"); |
| | | } |
| | | |
| | | assertEquals(startAndExpectFailure(null, false), exitCodeMessage()); |
| | | } |
| | | |
| | | @Test |
| | | public void failedStartShownToTheListenersKeepsTheExitCodeMessage() throws Exception |
| | | { |
| | | writeStartCommandPrintingToBothStreams(); |
| | | ListeningApplication application = new ListeningApplication(); |
| | | |
| | | assertEquals(startAndExpectFailure(application, false), exitCodeMessage()); |
| | | assertTrue(application.getShown().contains("cause on stdout"), application.getShown()); |
| | | assertTrue(application.getShown().contains("cause on stderr"), application.getShown()); |
| | | } |
| | | |
| | | @Test |
| | | public void failedStartHiddenFromTheListenersReportsBothOutputStreams() throws Exception |
| | | { |
| | | writeStartCommandPrintingToBothStreams(); |
| | | ListeningApplication application = new ListeningApplication(); |
| | | |
| | | String message = startAndExpectFailure(application, true); |
| | | |
| | | assertTrue(message.startsWith(exitCodeMessage()), message); |
| | | assertTrue(message.contains("cause on stdout"), message); |
| | | assertTrue(message.contains("cause on stderr"), message); |
| | | assertEquals(application.getShown(), ""); |
| | | } |
| | | |
| | | private static String exitCodeMessage() |
| | | { |
| | | return INFO_ERROR_STARTING_SERVER_CODE.get(1).toString(); |
| | | } |
| | | |
| | | private void writeStartCommandPrintingToBothStreams() throws IOException |
| | | { |
| | | if (isWindows()) |
| | | { |
| | | writeStartCommand("@echo cause on stdout", "@echo cause on stderr 1>&2", "@exit /b 1"); |
| | | } |
| | | else |
| | | { |
| | | writeStartCommand("#!/bin/sh", "echo 'cause on stdout'", "echo 'cause on stderr' >&2", "exit 1"); |
| | | } |
| | | } |
| | | |
| | | private void writeStartCommand(String... lines) throws IOException |
| | | { |
| | | Installation installation = new Installation(root, root); |
| | | File command = installation.getServerStartCommandFile(); |
| | | assertTrue(command.getParentFile().mkdirs()); |
| | | Files.write(command.toPath(), String.join(isWindows() ? "\r\n" : "\n", lines).concat("\n") |
| | | .getBytes(StandardCharsets.US_ASCII)); |
| | | assertTrue(command.setExecutable(true)); |
| | | } |
| | | |
| | | private String startAndExpectFailure(Application application, boolean suppressOutput) |
| | | { |
| | | try |
| | | { |
| | | new ServerController(application, new Installation(root, root)).startServer(suppressOutput); |
| | | fail("The start command exited with 1, but the start did not fail"); |
| | | return null; |
| | | } |
| | | catch (ApplicationException e) |
| | | { |
| | | assertEquals(e.getType(), ReturnCode.START_ERROR); |
| | | return e.getMessage(); |
| | | } |
| | | } |
| | | |
| | | /** An application whose listener keeps the log details it is shown. */ |
| | | private static final class ListeningApplication extends Application |
| | | { |
| | | private final StringBuilder shown = new StringBuilder(); |
| | | |
| | | ListeningApplication() |
| | | { |
| | | setProgressMessageFormatter(new PlainTextProgressMessageFormatter()); |
| | | addProgressUpdateListener(ev -> |
| | | { |
| | | synchronized (shown) |
| | | { |
| | | shown.append(ev.getNewLogs()); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | String getShown() |
| | | { |
| | | synchronized (shown) |
| | | { |
| | | return shown.toString(); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public String getInstallationPath() |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public String getInstancePath() |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public ProgressStep getCurrentProgressStep() |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Integer getRatio(ProgressStep step) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public LocalizableMessage getSummary(ProgressStep step) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public boolean isFinished() |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public boolean isCancellable() |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | public void cancel() |
| | | { |
| | | // Nothing to cancel. |
| | | } |
| | | |
| | | @Override |
| | | public void run() |
| | | { |
| | | // Never run: the test drives the ServerController directly. |
| | | } |
| | | } |
| | | } |