/* * 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 2008-2009 Sun Microsystems, Inc. * Portions Copyright 2013-2016 ForgeRock AS. * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.quicksetup; import static com.forgerock.opendj.cli.ArgumentConstants.*; 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; import org.opends.quicksetup.util.Utils; import com.forgerock.opendj.cli.ArgumentParser; /** * Responsible for providing initial evaluation of command line arguments * and determining whether to launch a CLI, GUI, or print a usage statement. */ public abstract class Launcher { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** Arguments with which this launcher was invoked. */ protected final String[] args; 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. * * @param args * String[] of argument passes from the command line * @param tempLogFilePrefix * 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.tempLogFilePrefix = tempLogFilePrefix; this.tempLogFileDirectory = tempLogFileDirectory; } /** * The temporary log file of this launcher, created the first time it is asked for. *
* 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)); } /** * Gets the arguments with which this launcher was invoked. * @return String[] args from the CLI invocation */ public String[] getArguments() { return this.args; } /** * Gets an argument parser appropriate for this CLI launcher. * * @return ArgumentParser for parsing args */ public abstract ArgumentParser getArgumentParser(); /** * Indicates whether the launcher should print a usage statement * based on the content of the arguments passed into the constructor. * @return boolean where true indicates usage should be printed */ protected boolean shouldPrintUsage() { if (args != null && args.length > 0) { for (String arg : args) { if (arg.equals("-?") || arg.equalsIgnoreCase("-H") || arg.equalsIgnoreCase("--help")) { return true; } } } return false; } /** * Indicates whether the launcher should print a usage statement * based on the content of the arguments passed into the constructor. * @return boolean where true indicates usage should be printed */ protected boolean isQuiet() { if (args != null && args.length > 0) { for (String arg : args) { if (arg.equals("-?") || arg.equalsIgnoreCase("-Q") || arg.equalsIgnoreCase("--quiet")) { return true; } } } return false; } /** * Indicates whether the launcher should print a version statement * based on the content of the arguments passed into the constructor. * @return boolean where true indicates version should be printed */ protected boolean shouldPrintVersion() { if (args != null && args.length > 0) { for (String arg : args) { if (arg.equalsIgnoreCase("--version")) { return true; } } } return false; } /** * Indicates whether the launcher will launch a command line versus * a graphical application based on the contents of the arguments * passed into the constructor. * * @return boolean where true indicates that a CLI application * should be launched */ protected boolean isCli() { for (String arg : args) { if (arg.equalsIgnoreCase("--"+OPTION_LONG_CLI) || arg.equalsIgnoreCase("-"+OPTION_SHORT_CLI)) { return true; } } return false; } /** * Prints a usage message to the terminal. * @param i18nMsg localized user message that will be printed to the terminal. * @param toStdErr whether the message must be printed to the standard error * or the standard output. */ private void printUsage(String i18nMsg, boolean toStdErr) { if (toStdErr) { System.err.println(i18nMsg); } else { System.out.println(i18nMsg); } } /** * Launches the graphical uninstall. The graphical uninstall is launched in a * different thread that the main thread because if we have a problem with the * graphical system (for instance the DISPLAY environment variable is not * correctly set) the native libraries will call exit. However if we launch * this from another thread, the thread will just be killed. * * This code also assumes that if the call to SplashWindow.main worked (and * the splash screen was displayed) we will never get out of it (we will call * a System.exit() when we close the graphical uninstall dialog). * * @param args String[] the arguments used to call the SplashWindow main * method * @return 0 if everything worked fine, or 1 if we could not display properly * the SplashWindow. */ protected int launchGui(final String[] args) { // Setup MacOSX native menu bar before AWT is loaded. Utils.setMacOSXMenuBar(getFrameTitle()); final int[] returnValue = { -1 }; Thread t = new Thread(new Runnable() { @Override public void run() { try { startSplashScreen(Launcher.this::getTempLogFile, args); returnValue[0] = 0; } catch (Throwable t) { // 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; } } }); /* * This is done to avoid displaying the stack that might occur if there are * problems with the display environment. */ PrintStream printStream = System.err; System.setErr(Utils.getEmptyPrintStream()); t.start(); try { t.join(); } catch (InterruptedException ie) { /* An error occurred, so the return value will be -1. We got nothing to do with this exception. */ } System.setErr(printStream); return returnValue[0]; } /** * Shows the splash screen and, behind it, builds the wizard. *
* 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. *
* 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