/* * 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 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 */ protected abstract LocalizableMessage getFrameTitle(); /** * Launches the command line based uninstall. * * @param cliApp the CLI application to launch * @return 0 if everything worked fine, and an error code if something wrong * occurred. */ private int launchCli(CliApplication cliApp) { System.setProperty(Constants.CLI_JAVA_PROPERTY, "true"); QuickSetupCli cli = new QuickSetupCli(cliApp, this); ReturnCode returnValue = cli.run(); if (returnValue.equals(ReturnCode.USER_DATA_ERROR)) { printUsage(true); System.exit(ReturnCode.USER_DATA_ERROR.getReturnCode()); } else if (returnValue.equals(ReturnCode.CANCELED)) { System.exit(ReturnCode.CANCELED.getReturnCode()); } else if (returnValue.equals(ReturnCode.USER_INPUT_ERROR)) { System.exit(ReturnCode.USER_INPUT_ERROR.getReturnCode()); } return returnValue.getReturnCode(); } /** Prints the version statement to standard output terminal. */ private void printVersion() { System.out.print(PRINTABLE_VERSION_STRING); } /** * Prints a usage statement to terminal and exits with an error * code. * @param toStdErr whether the message must be printed to the standard error * or the standard output. */ private void printUsage(boolean toStdErr) { try { ArgumentParser argParser = getArgumentParser(); if (argParser != null) { String msg = argParser.getUsage(); printUsage(msg, toStdErr); } } catch (Throwable t) { System.out.println("ERROR: "+t); t.printStackTrace(); } } /** * Creates a CLI application that will be run if the * launcher needs to launch a CLI application. * @return CliApplication that will be run */ protected abstract CliApplication createCliApplication(); /** * Called before the launcher launches the GUI. Here * subclasses can do any application specific things * like set system properties of print status messages * that need to be done before the GUI launches. */ protected abstract void willLaunchGui(); /** Called if launching of the GUI failed. */ protected abstract void guiLaunchFailed(); /** The main method which is called by the command lines. */ public void launch() { if (shouldPrintVersion()) { ArgumentParser parser = getArgumentParser(); if (parser == null || !parser.usageOrVersionDisplayed()) { printVersion(); } System.exit(ReturnCode.PRINT_VERSION.getReturnCode()); } else if (shouldPrintUsage()) { ArgumentParser parser = getArgumentParser(); if (parser == null || !parser.usageOrVersionDisplayed()) { printUsage(false); } 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); System.exit(exitCode); } else { 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); preExit(cliApp); System.exit(exitCode); } } } private void preExit(CliApplication cliApp) { if (cliApp != null) { UserData ud = cliApp.getUserData(); if (ud != null && !ud.isQuiet()) { // Add an extra space systematically System.out.println(); if (hasTempLogFile()) { System.out.println(INFO_GENERAL_SEE_FOR_DETAILS.get(getTempLogFile().getPath())); } } } } }