001/*
002 * (C) Copyright 2010-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Julien Carsique
018 *     Florent Guillaume
019 *     Ronan DANIELLOU
020 */
021package org.nuxeo.launcher;
022
023import static java.nio.charset.StandardCharsets.UTF_8;
024import static org.apache.logging.log4j.LogManager.ROOT_LOGGER_NAME;
025
026import java.io.Console;
027import java.io.File;
028import java.io.FileWriter;
029import java.io.IOException;
030import java.io.OutputStream;
031import java.io.OutputStreamWriter;
032import java.net.SocketTimeoutException;
033import java.nio.file.Files;
034import java.nio.file.Path;
035import java.nio.file.Paths;
036import java.nio.file.StandardOpenOption;
037import java.security.GeneralSecurityException;
038import java.util.ArrayList;
039import java.util.Arrays;
040import java.util.Collection;
041import java.util.Collections;
042import java.util.Date;
043import java.util.HashMap;
044import java.util.Iterator;
045import java.util.List;
046import java.util.Map;
047import java.util.concurrent.ExecutionException;
048import java.util.concurrent.ExecutorService;
049import java.util.concurrent.Executors;
050import java.util.function.Function;
051import java.util.function.Predicate;
052import java.util.regex.Pattern;
053import java.util.stream.Collectors;
054import java.util.stream.Stream;
055import java.util.zip.GZIPOutputStream;
056
057import javax.json.Json;
058import javax.json.stream.JsonGenerator;
059import javax.validation.constraints.NotNull;
060import javax.xml.bind.JAXBContext;
061import javax.xml.bind.JAXBException;
062import javax.xml.bind.Marshaller;
063import javax.xml.stream.FactoryConfigurationError;
064import javax.xml.stream.XMLOutputFactory;
065import javax.xml.stream.XMLStreamException;
066import javax.xml.stream.XMLStreamWriter;
067
068import org.apache.commons.cli.CommandLine;
069import org.apache.commons.cli.CommandLineParser;
070import org.apache.commons.cli.DefaultParser;
071import org.apache.commons.cli.HelpFormatter;
072import org.apache.commons.cli.Option;
073import org.apache.commons.cli.OptionGroup;
074import org.apache.commons.cli.Options;
075import org.apache.commons.cli.ParseException;
076import org.apache.commons.codec.binary.Base64;
077import org.apache.commons.io.IOUtils;
078import org.apache.commons.lang3.ArrayUtils;
079import org.apache.commons.lang3.StringUtils;
080import org.apache.commons.lang3.SystemUtils;
081import org.apache.commons.logging.Log;
082import org.apache.commons.logging.LogFactory;
083import org.apache.commons.logging.impl.SimpleLog;
084import org.apache.commons.validator.routines.EmailValidator;
085import org.apache.logging.log4j.Level;
086import org.nuxeo.common.Environment;
087import org.nuxeo.common.codec.Crypto;
088import org.nuxeo.common.codec.CryptoProperties;
089import org.nuxeo.connect.connector.NuxeoClientInstanceType;
090import org.nuxeo.connect.connector.http.ConnectUrlConfig;
091import org.nuxeo.connect.data.ConnectProject;
092import org.nuxeo.connect.identity.TechnicalInstanceIdentifier;
093import org.nuxeo.connect.identity.LogicalInstanceIdentifier.NoCLID;
094import org.nuxeo.connect.registration.RegistrationException;
095import org.nuxeo.connect.tools.report.client.ReportConnector;
096import org.nuxeo.connect.update.PackageException;
097import org.nuxeo.connect.update.Version;
098import org.nuxeo.launcher.config.ConfigurationException;
099import org.nuxeo.launcher.config.ConfigurationGenerator;
100import org.nuxeo.launcher.connect.ConnectBroker;
101import org.nuxeo.launcher.connect.ConnectRegistrationBroker;
102import org.nuxeo.launcher.connect.LauncherRestartException;
103import org.nuxeo.launcher.daemon.DaemonThreadFactory;
104import org.nuxeo.launcher.gui.NuxeoLauncherGUI;
105import org.nuxeo.launcher.info.CommandInfo;
106import org.nuxeo.launcher.info.CommandSetInfo;
107import org.nuxeo.launcher.info.ConfigurationInfo;
108import org.nuxeo.launcher.info.DistributionInfo;
109import org.nuxeo.launcher.info.InstanceInfo;
110import org.nuxeo.launcher.info.KeyValueInfo;
111import org.nuxeo.launcher.info.MessageInfo;
112import org.nuxeo.launcher.info.PackageInfo;
113import org.nuxeo.launcher.monitoring.StatusServletClient;
114import org.nuxeo.launcher.process.MacProcessManager;
115import org.nuxeo.launcher.process.ProcessManager;
116import org.nuxeo.launcher.process.PureJavaProcessManager;
117import org.nuxeo.launcher.process.SolarisProcessManager;
118import org.nuxeo.launcher.process.UnixProcessManager;
119import org.nuxeo.launcher.process.WindowsProcessManager;
120import org.nuxeo.log4j.Log4JHelper;
121import org.nuxeo.log4j.ThreadedStreamGobbler;
122
123import com.sun.jersey.api.json.JSONConfiguration;
124import com.sun.jersey.json.impl.writer.JsonXmlStreamWriter;
125
126/**
127 * @author jcarsique
128 * @since 5.4.2
129 */
130public abstract class NuxeoLauncher {
131
132    /**
133     * @since 7.4
134     */
135    protected static final String OUTPUT_UNSET_VALUE = "<unset>";
136
137    /**
138     * @since 5.6
139     */
140    protected static final String OPTION_NODEPS = "nodeps";
141
142    private static final String OPTION_NODEPS_DESC = "Ignore package dependencies and constraints.";
143
144    /**
145     * @since 5.6
146     */
147    protected static final String OPTION_GUI = "gui";
148
149    private static final String OPTION_GUI_DESC = "Start graphical user interface (default is true on Windows and false on other platforms).";
150
151    /**
152     * @since 5.6
153     */
154    protected static final String OPTION_JSON = "json";
155
156    private static final String OPTION_JSON_DESC = "Output JSON for mp-* commands.";
157
158    /**
159     * @since 5.6
160     */
161    protected static final String OPTION_XML = "xml";
162
163    private static final String OPTION_XML_DESC = "Output XML for mp-* commands.";
164
165    /**
166     * @since 5.6
167     */
168    protected static final String OPTION_DEBUG = "debug";
169
170    private static final String OPTION_DEBUG_DESC = "Activate debug messages.\n"
171            + "<categories>: comma-separated Java categories to debug (default: \"org.nuxeo.launcher\").";
172
173    /**
174     * @since 7.4
175     */
176    private static final String OPTION_DEBUG_CATEGORY_ARG_NAME = "categories";
177
178    /**
179     * @since 5.6
180     */
181    protected static final String OPTION_DEBUG_CATEGORY = "dc";
182
183    private static final String OPTION_DEBUG_CATEGORY_DESC = "Deprecated: see categories on '--debug' option.";
184
185    /**
186     * @since 5.6
187     */
188    protected static final String OPTION_QUIET = "quiet";
189
190    private static final String OPTION_QUIET_DESC = "Suppress information messages.";
191
192    /**
193     * @since 5.6
194     */
195    protected static final String OPTION_HELP = "help";
196
197    private static final String OPTION_HELP_DESC = "Show detailed help.";
198
199    /**
200     * @since 5.6
201     */
202    protected static final String OPTION_RELAX = "relax";
203
204    private static final String OPTION_RELAX_DESC = "Allow relax constraint on current platform (default: "
205            + ConnectBroker.OPTION_RELAX_DEFAULT + ").";
206
207    /**
208     * @since 5.6
209     */
210    protected static final String OPTION_ACCEPT = "accept";
211
212    private static final String OPTION_ACCEPT_DESC = "Accept, refuse or ask confirmation for all changes (default: "
213            + ConnectBroker.OPTION_ACCEPT_DEFAULT + ").\n"
214            + "In non interactive mode, '--accept=true' also sets '--relax=true' if needed.";
215
216    /**
217     * @since 5.9.1
218     */
219    protected static final String OPTION_SNAPSHOT = "snapshot";
220
221    private static final String OPTION_SNAPSHOT_DESC = "Allow use of SNAPSHOT Nuxeo Packages.\n"
222            + "This option is implicit:\n" //
223            + "\t- on SNAPSHOT distributions (daily builds),\n"
224            + "\t- if the command explicitly requests a SNAPSHOT package.";
225
226    /**
227     * @since 5.9.1
228     */
229    protected static final String OPTION_FORCE = "force";
230
231    private static final String OPTION_FORCE_DESC = "Deprecated: use '--strict' option instead.";
232
233    /**
234     * @since 7.4
235     */
236    protected static final String OPTION_STRICT = "strict";
237
238    private static final String OPTION_STRICT_DESC = "Abort in error the start command when a component cannot "
239            + "be activated or if a server is already running.";
240
241    /**
242     * @since 5.6
243     */
244    protected static final String OPTION_HIDE_DEPRECATION = "hide-deprecation-warnings";
245
246    protected static final String OPTION_HIDE_DEPRECATION_DESC = "Hide deprecation warnings.";
247
248    /**
249     * @since 6.0
250     */
251    protected static final String OPTION_IGNORE_MISSING = "ignore-missing";
252
253    protected static final String OPTION_IGNORE_MISSING_DESC = "Ignore unknown packages on mp-add, mp-install and mp-set commands.";
254
255    /**
256     * @since 6.0
257     */
258    protected static final String OPTION_CLID = "clid";
259
260    private static final String OPTION_CLID_DESC = "Use the provided instance CLID file";
261
262    /**
263     * @since 10.3
264     */
265    protected static final String OPTION_OFFLINE = "offline";
266
267    private static final String OPTION_OFFLINE_DESC = "Allow offline registration";
268
269    /**
270     * @since 8.10-HF15
271     */
272    protected static final String OPTION_RENEW = "renew";
273
274    private static final String OPTION_RENEW_DESC = "Renew the current CLID";
275
276    /**
277     * @since 7.4
278     */
279    protected static final String OPTION_ENCRYPT = "encrypt";
280
281    private static final String OPTION_ENCRYPT_ARG_NAME = "algorithm";
282
283    private static final String OPTION_ENCRYPT_DESC = String.format("Activate key value symmetric encryption.\n"
284            + "The algorithm can be configured: <%s> is a cipher transformation of the form: \"algorithm/mode/padding\" or \"algorithm\".\n"
285            + "Default value is \"%s\" (Advanced Encryption Standard, Electronic Cookbook Mode, PKCS5-style padding).",
286            OPTION_ENCRYPT, Crypto.DEFAULT_ALGO);
287
288    /**
289     * @since 7.4
290     */
291    protected static final String OPTION_SET = "set";
292
293    private static final String OPTION_SET_ARG_NAME = "template";
294
295    private static final String OPTION_SET_DESC = String.format("Set the value for a given key.\n"
296            + "The value is stored in {{%s}} by default unless a template name is provided; if so, it is then stored in the template's {{%s}} file.\n"
297            + "If the value is empty (''), then the property is unset.\n"
298            + "This option is implicit if no '--get' or '--get-regexp' option is used and there are exactly two parameters (key value).",
299            ConfigurationGenerator.NUXEO_CONF, ConfigurationGenerator.NUXEO_DEFAULT_CONF);
300
301    /**
302     * @since 7.4
303     */
304    protected static final String OPTION_GET = "get";
305
306    private static final String OPTION_GET_DESC = "Get the value for a given key. Returns error code 6 if the key was not found.\n"
307            + "This option is implicit if '--set' option is not used and there are more or less than two parameters.";
308
309    /**
310     * @since 7.4
311     */
312    protected static final String OPTION_GET_REGEXP = "get-regexp";
313
314    private static final String OPTION_GET_REGEXP_DESC = "Get the value for all keys matching the given regular expression(s).";
315
316    /**
317     * @since 8.3
318     */
319    protected static final String OPTION_GZIP_OUTPUT = "gzip";
320
321    private static final String OPTION_GZIP_DESC = "Compress the output.";
322
323    /**
324     * @since 8.3
325     */
326    protected static final String OPTION_OUTPUT = "output";
327
328    private static final String OPTION_OUTPUT_DESC = "Write output in specified file.";
329
330    /**
331     * @since 8.3
332     */
333    protected static final String OPTION_PRETTY_PRINT = "pretty-print";
334
335    private static final String OPTION_PRETTY_PRINT_DESC = "Pretty print the output.";
336
337    // Fallback to avoid an error when the log dir is not initialized
338    static {
339        if (System.getProperty(Environment.NUXEO_LOG_DIR) == null) {
340            System.setProperty(Environment.NUXEO_LOG_DIR, ".");
341        }
342    }
343
344    /**
345     * @since 5.6
346     */
347    private static final String DEFAULT_NUXEO_CONTEXT_PATH = "/nuxeo";
348
349    static final Log log = LogFactory.getLog(NuxeoLauncher.class);
350
351    private static Options options = initParserOptions();
352
353    private static final String JAVA_OPTS_PROPERTY = "launcher.java.opts";
354
355    private static final String JAVA_OPTS_DEFAULT = "-Xms512m -Xmx1024m";
356
357    private static final String OVERRIDE_JAVA_TMPDIR_PARAM = "launcher.override.java.tmpdir";
358
359    protected boolean overrideJavaTmpDir;
360
361    private static final String START_MAX_WAIT_PARAM = "launcher.start.max.wait";
362
363    private static final String STOP_MAX_WAIT_PARAM = "launcher.stop.max.wait";
364
365    /**
366     * Default maximum time to wait for server startup summary in logs (in seconds).
367     */
368    private static final String START_MAX_WAIT_DEFAULT = "300";
369
370    /**
371     * Default maximum time to wait for effective stop (in seconds)
372     */
373    private static final String STOP_MAX_WAIT_DEFAULT = "60";
374
375    /**
376     * Number of try to cleanly stop server before killing process
377     */
378    private static final int STOP_NB_TRY = 5;
379
380    private static final int STOP_SECONDS_BEFORE_NEXT_TRY = 2;
381
382    private static final long STREAM_MAX_WAIT = 3000;
383
384    private static final String PACK_TOMCAT_CLASS = "org.nuxeo.runtime.deployment.preprocessor.PackWar";
385
386    private static final String PARAM_UPDATECENTER_DISABLED = "nuxeo.updatecenter.disabled";
387
388    private static final String[] COMMANDS_NO_GUI = { "configure", "mp-init", "mp-purge", "mp-add", "mp-install",
389            "mp-uninstall", "mp-request", "mp-remove", "mp-hotfix", "mp-upgrade", "mp-reset", "mp-list", "mp-listall",
390            "mp-update", "status", "showconf", "mp-show", "mp-set", "config", "encrypt", "decrypt", OPTION_HELP,
391            "register", "register-trial", "connect-report" };
392
393    private static final String[] COMMANDS_NO_RUNNING_SERVER = { "pack", "mp-init", "mp-purge", "mp-add", "mp-install",
394            "mp-uninstall", "mp-request", "mp-remove", "mp-hotfix", "mp-upgrade", "mp-reset", "mp-update", "mp-set" };
395
396    /**
397     * @since 7.4
398     */
399    protected boolean commandRequiresNoRunningServer() {
400        return Arrays.asList(COMMANDS_NO_RUNNING_SERVER).contains(command);
401    }
402
403    /**
404     * @since 7.4
405     */
406    protected boolean commandRequiresNoGUI() {
407        return Arrays.asList(COMMANDS_NO_GUI).contains(command);
408    }
409
410    /**
411     * Program is running or service is OK.
412     *
413     * @since 5.7
414     */
415    public static final int STATUS_CODE_ON = 0;
416
417    /**
418     * Program is not running.
419     *
420     * @since 5.7
421     */
422    public static final int STATUS_CODE_OFF = 3;
423
424    /**
425     * Program or service status is unknown.
426     *
427     * @since 5.7
428     */
429    public static final int STATUS_CODE_UNKNOWN = 4;
430
431    /**
432     * @since 5.7
433     */
434    public static final int EXIT_CODE_OK = 0;
435
436    /**
437     * Generic or unspecified error.
438     *
439     * @since 5.7
440     */
441    public static final int EXIT_CODE_ERROR = 1;
442
443    /**
444     * Invalid or excess argument(s).
445     *
446     * @since 5.7
447     */
448    public static final int EXIT_CODE_INVALID = 2;
449
450    /**
451     * Unimplemented feature.
452     *
453     * @since 5.7
454     */
455    public static final int EXIT_CODE_UNIMPLEMENTED = 3;
456
457    /**
458     * User had insufficient privilege.
459     *
460     * @since 5.7
461     */
462    public static final int EXIT_CODE_UNAUTHORIZED = 4;
463
464    /**
465     * Program is not installed.
466     *
467     * @since 5.7
468     */
469    public static final int EXIT_CODE_NOT_INSTALLED = 5;
470
471    /**
472     * Program is not configured.
473     *
474     * @since 5.7
475     */
476    public static final int EXIT_CODE_NOT_CONFIGURED = 6;
477
478    /**
479     * Program is not running.
480     *
481     * @since 5.7
482     */
483    public static final int EXIT_CODE_NOT_RUNNING = 7;
484
485    /**
486     * Launcher is changed.
487     *
488     * @since 10.2
489     */
490    public static final int EXIT_CODE_LAUNCHER_CHANGED = 128;
491
492    private static final String OPTION_HELP_DESC_ENV = "\nENVIRONMENT VARIABLES\n"
493            + "        NUXEO_HOME\t\tPath to server root directory.\n" //
494            + "        NUXEO_CONF\t\tPath to {{nuxeo.conf}} file.\n" //
495            + "        PATH\n" //
496            + "\tJAVA\t\t\tPath to the {{java}} executable.\n"
497            + "        JAVA_HOME\t\tPath to the Java home directory. Can also be defined in {{nuxeo.conf}}.\n"
498            + "        JAVA_OPTS\t\tOptional values passed to the JVM. Can also be defined in {{nuxeo.conf}}.\n"
499            + "        REQUIRED_JAVA_VERSION\tNuxeo requirement on Java version.\n" //
500            + "\nJAVA USAGE\n"
501            + String.format(
502                    "        java [-D%s=\"JVM options\"] [-D%s=\"/path/to/nuxeo\"] [-D%s=\"/path/to/nuxeo.conf\"]"
503                            + " [-Djvmcheck=nofail] -jar \"path/to/nuxeo-launcher.jar\" \\\n"
504                            + "        \t[options] <command> [command parameters]\n\n",
505                    JAVA_OPTS_PROPERTY, Environment.NUXEO_HOME, ConfigurationGenerator.NUXEO_CONF)
506            + String.format("        %s\tParameters for the server JVM (default are %s).\n", JAVA_OPTS_PROPERTY,
507                    JAVA_OPTS_DEFAULT)
508            + String.format("        %s\t\tNuxeo server root path (default is parent of called script).\n",
509                    Environment.NUXEO_HOME)
510            + String.format("        %s\t\tPath to {{%1$s}} file (default is \"$NUXEO_HOME/bin/%1$s\").\n",
511                    ConfigurationGenerator.NUXEO_CONF)
512            + "        jvmcheck\t\tIf set to \"nofail\", ignore JVM version validation errors.\n";
513
514    private static final String OPTION_HELP_DESC_COMMANDS = "\nCOMMANDS\n" + "        help\t\t\tPrint this message.\n"
515            + "        gui\t\t\tDeprecated: use '--gui' option instead.\n"
516            + "        start\t\t\tStart Nuxeo server in background, waiting for effective start. Useful for batch executions requiring the server being immediately available after the script returned.\n"
517            + "        stop\t\t\tStop any Nuxeo server started with the same {{nuxeo.conf}} file.\n"
518            + "        restart\t\t\tRestart Nuxeo server.\n"
519            + "        config\t\t\tGet and set template or global parameters.\n"
520            + "        encrypt\t\t\tOutput encrypted value for a given parameter.\n"
521            + "        decrypt\t\t\tOutput decrypted value for a given parameter.\n"
522            + "        configure\t\tConfigure Nuxeo server with parameters from {{nuxeo.conf}}.\n"
523            + "        wizard\t\t\tStart the wizard.\n"
524            + "        console\t\t\tStart Nuxeo server in a console mode. Ctrl-C will stop it.\n"
525            + "        status\t\t\tPrint server running status.\n"
526            + "        startbg\t\t\tStart Nuxeo server in background, without waiting for effective start. Useful for starting Nuxeo as a service.\n"
527            + "        restartbg\t\tRestart Nuxeo server with a call to \"startbg\" after \"stop\".\n"
528            + "        pack\t\t\tBuild a static archive.\n"
529            + "        showconf\t\tDisplay the instance configuration.\n"
530            + "        connect-report\t\tDump a JSON report about the running server (which being used by Nuxeo support).\n"
531            + "        mp-list\t\t\tList local Nuxeo Packages.\n"
532            + "        mp-listall\t\tList all Nuxeo Packages.\n"
533            + "        mp-init\t\t\tPre-cache Nuxeo Packages locally available in the distribution.\n"
534            + "        mp-update\t\tUpdate cache of Nuxeo Packages list.\n"
535            + "        mp-add\t\t\tAdd Nuxeo Package(s) to local cache. You must provide the package file(s), name(s) or ID(s) as parameter.\n"
536            + "        mp-install\t\tRun Nuxeo Package installation. It is automatically called at startup if {{installAfterRestart.log}} file exists in data directory. Else you must provide the package file(s), name(s) or ID(s) as parameter.\n"
537            + "        mp-uninstall\t\tUninstall Nuxeo Package(s). You must provide the package name(s) or ID(s) as parameter (see \"mp-list\" command).\n"
538            + "        mp-remove\t\tRemove Nuxeo Package(s) from the local cache. You must provide the package name(s) or ID(s) as parameter (see \"mp-list\" command).\n"
539            + "        mp-reset\t\tReset all packages to DOWNLOADED state. May be useful after a manual server upgrade.\n"
540            + "        mp-set\t\t\tInstall a list of Nuxeo Packages and remove those not in the list.\n"
541            + "        mp-request\t\tInstall and uninstall Nuxeo Package(s) in one command. You must provide a *quoted* list of package names or IDs prefixed with + (install) or - (uninstall).\n"
542            + "        mp-purge\t\tUninstall and remove all packages from the local cache.\n"
543            + "        mp-hotfix\t\tInstall all the available hotfixes for the current platform but do not upgrade already installed ones (requires a registered instance).\n"
544            + "        mp-upgrade\t\tGet all the available upgrades for the Nuxeo Packages currently installed (requires a registered instance).\n"
545            + "        mp-show\t\t\tShow Nuxeo Package(s) information. You must provide the package file(s), name(s) or ID(s) as parameter.\n"
546            + "        register\t\tRegister your instance with an existing Connect account. You must provide the credentials, the project name or ID, its type and a description.\n"
547            + "        register-trial\t\tThis command is deprecated. To register for a free 30 day trial on Nuxeo Online Services, please visit https://connect.nuxeo.com/register\n"
548            + "\nThe following commands are always executed in console/headless mode (no GUI): "
549            + "\"configure\", \"mp-init\", \"mp-purge\", \"mp-add\", \"mp-install\", \"mp-uninstall\", \"mp-request\", "
550            + "\"mp-remove\", \"mp-hotfix\", \"mp-upgrade\", \"mp-reset\", \"mp-list\", \"mp-listall\", \"mp-update\", "
551            + "\"status\", \"showconf\", \"mp-show\", \"mp-set\", \"config\", \"encrypt\", \"decrypt\", \"help\".\n"
552            + "\nThe following commands cannot be executed on a running server: \"pack\", \"mp-init\", \"mp-purge\", "
553            + "\"mp-add\", \"mp-install\", \"mp-uninstall\", \"mp-request\", \"mp-remove\", \"mp-hotfix\", \"mp-upgrade\", "
554            + "\"mp-reset\".\n"
555            + "\nThe following commands can only be executed on a running server: \"connect-report\"\n"
556            + "\nCommand parameters may need to be prefixed with '--' to separate them from option arguments when confusion arises.";
557
558    private static final String OPTION_HELP_USAGE = "        nuxeoctl <command> [options] [--] [command parameters]\n\n";
559
560    private static final String OPTION_HELP_HEADER = "SYNOPSIS\n"
561            + "        nuxeoctl encrypt [--encrypt <algorithm>] [<clearValue>..] [-d [<categories>]|-q]\n"
562            + "                Output encrypted value for <clearValue>.\n"
563            + "                If <clearValue> is not provided, it is read from stdin.\n\n"
564            + "        nuxeoctl decrypt '<cryptedValue>'.. [-d [<categories>]|-q]\n" //
565            + "                Output decrypted value for <cryptedValue>. The secret key is read from stdin.\n\n"
566            + "        nuxeoctl config [<key> <value>].. <key> [<value>] [--encrypt [<algorithm>]] [--set [<template>]] [-d [<categories>]|-q]\n"
567            + "                Set template or global parameters.\n"
568            + "                If <value> is not provided and the --set 'option' is used, then the value is read from stdin.\n\n"
569            + "        nuxeoctl config [--get] <key>.. [-d [<categories>]|-q]\n"
570            + "                Get value for the given key(s).\n\n"
571            + "        nuxeoctl config [--get-regexp] <regexp>.. [-d [<categories>]|-q]\n"
572            + "                Get value for the keys matching the given regular expression(s).\n\n"
573            + "        nuxeoctl help|status|showconf [-d [<categories>]|-q]\n\n"
574            + "        nuxeoctl configure [-d [<categories>]|-q|-hdw]\n\n"
575            + "        nuxeoctl wizard [-d [<categories>]|-q|--clid <arg>|--gui <true|false|yes|no>]\n\n"
576            + "        nuxeoctl stop [-d [<categories>]|-q|--gui <true|false|yes|no>]\n\n"
577            + "        nuxeoctl start|restart|console|startbg|restartbg [-d [<categories>]|-q|--clid <arg>|--gui <true|false|yes|no>|--strict|-hdw]\n\n"
578            + "        nuxeoctl mp-show [command parameters] [-d [<categories>]|-q|--clid <arg>|--xml|--json]\n\n"
579            + "        nuxeoctl mp-list|mp-listall|mp-init|mp-update [command parameters] [-d [<categories>]|-q|--clid <arg>|--relax <true|false|yes|no>|--xml|--json]\n\n"
580            + "        nuxeoctl mp-reset|mp-purge|mp-hotfix|mp-upgrade [command parameters] [-d [<categories>]|-q|--clid <arg>|--xml|--json|--accept <true|false|yes|no|ask>]\n\n"
581            + "        nuxeoctl mp-add|mp-install|mp-uninstall|mp-remove|mp-set|mp-request [command parameters] [-d [<categories>]|-q|--clid <arg>|--xml|--json|--nodeps|--relax <true|false|yes|no|ask>|--accept <true|false|yes|no|ask>|-s|-im]\n\n"
582            + "        nuxeoctl register [<username> [<project> [<type> <description>] [<pwd>]]]\n"
583            + "                Register an instance with Nuxeo Online Services.\n\n"
584            + "        nuxeoctl register --clid <arg>\n"
585            + "                Register an instance according to the given CLID file.\n\n"
586            + "        nuxeoctl register --renew [--clid <arg>]\n"
587            + "                Renew an instance registration with Nuxeo Online Services.\n\n"
588            + "        nuxeoctl pack <target> [-d [<categories>]|-q]\n\n" //
589            + "        nuxeoctl connect-report [--output <file>|--gzip <*true|false|yes|no>|--pretty-print <true|*false|yes|no>]\n\n"
590            + "OPTIONS";
591
592    private static final String OPTION_HELP_FOOTER = "\nSee online documentation \"ADMINDOC/nuxeoctl and Control Panel Usage\": https://doc.nuxeo.com/x/FwNc";
593
594    private static final int PAGE_SIZE = 20;
595
596    public static final String CONNECT_TC_URL = "https://www.nuxeo.com/legal/nuxeo-trial-terms-conditions";
597
598    protected ConfigurationGenerator configurationGenerator;
599
600    public final ConfigurationGenerator getConfigurationGenerator() {
601        return configurationGenerator;
602    }
603
604    protected ProcessManager processManager;
605
606    protected Process nuxeoProcess;
607
608    private String processRegex;
609
610    protected String pid;
611
612    private ExecutorService executor = Executors.newSingleThreadExecutor(
613            new DaemonThreadFactory("NuxeoProcessThread", false));
614
615    private ShutdownThread shutdownHook;
616
617    protected String[] params;
618
619    protected String command;
620
621    public String getCommand() {
622        return command;
623    }
624
625    /**
626     * @since 7.4
627     */
628    public boolean commandIs(String aCommand) {
629        return StringUtils.equalsIgnoreCase(command, aCommand);
630    }
631
632    public CommandSetInfo cset = new CommandSetInfo();
633
634    private boolean useGui = false;
635
636    /**
637     * @since 5.5
638     */
639    public boolean isUsingGui() {
640        return useGui;
641    }
642
643    private boolean reloadConfiguration = false;
644
645    private int status = STATUS_CODE_UNKNOWN;
646
647    private int errorValue = EXIT_CODE_OK;
648
649    private StatusServletClient statusServletClient;
650
651    private static boolean quiet = false;
652
653    private static boolean debug = false;
654
655    private static boolean strict = false;
656
657    private boolean xmlOutput = false;
658
659    private boolean jsonOutput = false;
660
661    private ConnectBroker connectBroker = null;
662
663    private String clid = null;
664
665    private ConnectRegistrationBroker connectRegistrationBroker = null;
666
667    private InstanceInfo info;
668
669    CommandLine cmdLine;
670
671    private boolean ignoreMissing = false;
672
673    /**
674     * @since 5.5
675     * @return true if quiet mode is active
676     */
677    public boolean isQuiet() {
678        return quiet;
679    }
680
681    private static Map<String, NuxeoLauncherGUI> guis;
682
683    /**
684     * @since 5.5
685     */
686    public NuxeoLauncherGUI getGUI() {
687        if (guis == null) {
688            return null;
689        }
690        return guis.get(configurationGenerator.getNuxeoConf().toString());
691    }
692
693    /**
694     * @since 5.5
695     */
696    public void setGUI(NuxeoLauncherGUI gui) {
697        if (guis == null) {
698            guis = new HashMap<>();
699        }
700        guis.put(configurationGenerator.getNuxeoConf().toString(), gui);
701    }
702
703    public NuxeoLauncher(ConfigurationGenerator configurationGenerator) {
704        this.configurationGenerator = configurationGenerator;
705        init();
706    }
707
708    /**
709     * @since 5.6
710     */
711    public void init() {
712        if (!configurationGenerator.init(true)) {
713            throw new IllegalStateException("Initialization failed");
714        }
715        statusServletClient = new StatusServletClient(configurationGenerator);
716        statusServletClient.setKey(configurationGenerator.getUserConfig().getProperty(Environment.SERVER_STATUS_KEY));
717        processManager = getOSProcessManager();
718        processRegex = "^(?!/bin/sh).*" + Pattern.quote(configurationGenerator.getNuxeoConf().getPath()) + ".*"
719                + Pattern.quote(getServerPrint()) + ".*$";
720        // Set OS-specific decorations
721        if (SystemUtils.IS_OS_MAC) {
722            System.setProperty("com.apple.mrj.application.apple.menu.about.name", "NuxeoCtl");
723        }
724    }
725
726    private ProcessManager getOSProcessManager() {
727        if (SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_AIX) {
728            return new UnixProcessManager();
729        } else if (SystemUtils.IS_OS_MAC) {
730            return new MacProcessManager();
731        } else if (SystemUtils.IS_OS_SUN_OS) {
732            return new SolarisProcessManager();
733        } else if (SystemUtils.IS_OS_WINDOWS) {
734            WindowsProcessManager windowsProcessManager = new WindowsProcessManager();
735            return windowsProcessManager.isUsable() ? windowsProcessManager : new PureJavaProcessManager();
736        } else {
737            return new PureJavaProcessManager();
738        }
739    }
740
741    /**
742     * Do not directly call this method without a call to {@link #checkNoRunningServer()}
743     *
744     * @see #doStart()
745     * @throws IOException In case of issue with process.
746     * @throws InterruptedException If any thread has interrupted the current thread.
747     */
748    protected void start(boolean logProcessOutput) throws IOException, InterruptedException {
749        List<String> startCommand = new ArrayList<>();
750        startCommand.add(getJavaExecutable().getPath());
751        startCommand.addAll(getJavaOptsProperty(Function.identity()));
752        startCommand.add("-cp");
753        startCommand.add(getClassPath());
754        startCommand.addAll(getNuxeoProperties());
755        startCommand.addAll(getServerProperties());
756        if (strict) {
757            startCommand.add("-Dnuxeo.start.strict=true");
758        }
759        setServerStartCommand(startCommand);
760        startCommand.addAll(Arrays.asList(params));
761        ProcessBuilder pb = new ProcessBuilder(getOSCommand(startCommand));
762        pb.directory(configurationGenerator.getNuxeoHome());
763        log.debug("Server command: " + pb.command());
764        nuxeoProcess = pb.start();
765        Thread.sleep(1000);
766        boolean processExited = false;
767        // Check if process exited early
768        if (nuxeoProcess == null) {
769            log.error(String.format("Server start failed with command: %s", pb.command()));
770            if (SystemUtils.IS_OS_WINDOWS && configurationGenerator.getNuxeoHome().getPath().contains(" ")) {
771                // NXP-17679
772                log.error("The server path must not contain spaces under Windows.");
773            }
774            return;
775        }
776        try {
777            int exitValue = nuxeoProcess.exitValue();
778            if (exitValue != 0) {
779                log.error(String.format("Server start failed (%d).", exitValue));
780            }
781            processExited = true;
782        } catch (IllegalThreadStateException e) {
783            // Normal case
784        }
785        logProcessStreams(nuxeoProcess, processExited || logProcessOutput);
786        if (!processExited) {
787            if (getPid() != null) {
788                log.warn("Server started with process ID " + pid + ".");
789            } else {
790                log.warn("Sent server start command but could not get process ID.");
791            }
792        }
793    }
794
795    /**
796     * Gets the Java options defined in Nuxeo configuration files, e.g. <tt>bin/nuxeo.conf</tt> and
797     * <tt>bin/nuxeoctl</tt>.
798     *
799     * @return the Java options.
800     */
801    protected List<String> getJavaOptsProperty(Function<String, String> mapper) {
802        return configurationGenerator.getJavaOpts(mapper);
803    }
804
805    /**
806     * Check if some server is already running (from another thread) and throw a Runtime exception if it finds one. That
807     * method will work where {@link #isRunning()} won't.
808     *
809     * @throws IllegalThreadStateException Thrown if a server is already running.
810     */
811    public void checkNoRunningServer() throws IllegalStateException {
812        try {
813            String existingPid = getPid();
814            if (existingPid != null) {
815                errorValue = EXIT_CODE_OK;
816                throw new IllegalStateException("A server is running with process ID " + existingPid);
817            }
818        } catch (IOException e) {
819            log.warn("Could not check existing process: " + e.getMessage());
820        }
821    }
822
823    /**
824     * @return (since 5.5) Array list with created stream gobbler threads.
825     */
826    public ArrayList<ThreadedStreamGobbler> logProcessStreams(Process process, boolean logProcessOutput) {
827        ArrayList<ThreadedStreamGobbler> sgArray = new ArrayList<>();
828        ThreadedStreamGobbler inputSG;
829        ThreadedStreamGobbler errorSG;
830        if (logProcessOutput) {
831            inputSG = new ThreadedStreamGobbler(process.getInputStream(), System.out);
832            errorSG = new ThreadedStreamGobbler(process.getErrorStream(), System.err);
833        } else {
834            inputSG = new ThreadedStreamGobbler(process.getInputStream(), SimpleLog.LOG_LEVEL_OFF);
835            errorSG = new ThreadedStreamGobbler(process.getErrorStream(), SimpleLog.LOG_LEVEL_OFF);
836        }
837        inputSG.start();
838        errorSG.start();
839        sgArray.add(inputSG);
840        sgArray.add(errorSG);
841        return sgArray;
842    }
843
844    protected abstract String getServerPrint();
845
846    /**
847     * Will wrap, if necessary, the command within a Shell command
848     *
849     * @param roughCommand Java command which will be run
850     * @return wrapped command depending on the OS
851     */
852    private List<String> getOSCommand(List<String> roughCommand) {
853        if (SystemUtils.IS_OS_UNIX) {
854            return getUnixCommand(roughCommand);
855        }
856        if (SystemUtils.IS_OS_WINDOWS) {
857            return getWindowsCommand(roughCommand);
858        }
859        throw new IllegalStateException("Unknown os, can't launch server");
860    }
861
862    private List<String> getWindowsCommand(List<String> roughCommand) {
863        ArrayList<String> osCommand = new ArrayList<>();
864        for (String commandToken : roughCommand) {
865            if (StringUtils.isBlank(commandToken)) {
866                continue;
867            }
868            osCommand.add("\"" + commandToken + "\"");
869        }
870        return osCommand;
871    }
872
873    private List<String> getUnixCommand(List<String> roughCommand) {
874        ArrayList<String> osCommand = new ArrayList<>();
875        StringBuilder linearizedCommand = new StringBuilder("exec");
876        for (String commandToken : roughCommand) {
877            if (StringUtils.isBlank(commandToken)) {
878                continue;
879            }
880            if (commandToken.contains(" ")) {
881                commandToken = commandToken.replaceAll(" ", "\\\\ ");
882            }
883            linearizedCommand.append(' ').append(commandToken);
884        }
885        osCommand.add("/bin/sh");
886        osCommand.add("-c");
887        osCommand.add(linearizedCommand.toString());
888        return osCommand;
889    }
890
891    protected abstract Collection<? extends String> getServerProperties();
892
893    protected abstract void setServerStartCommand(List<String> command);
894
895    private File getJavaExecutable() {
896        return new File(System.getProperty("java.home"), "bin" + File.separator + "java");
897    }
898
899    protected abstract String getClassPath();
900
901    /**
902     * @since 5.6
903     */
904    protected abstract String getShutdownClassPath();
905
906    protected Collection<String> getNuxeoProperties() {
907        ArrayList<String> nuxeoProperties = new ArrayList<>();
908        nuxeoProperties.add(
909                String.format("-D%s=%s", Environment.NUXEO_HOME, configurationGenerator.getNuxeoHome().getPath()));
910        nuxeoProperties.add(String.format("-D%s=%s", ConfigurationGenerator.NUXEO_CONF,
911                configurationGenerator.getNuxeoConf().getPath()));
912        nuxeoProperties.add(getNuxeoProperty(Environment.NUXEO_LOG_DIR));
913        nuxeoProperties.add(getNuxeoProperty(Environment.NUXEO_DATA_DIR));
914        nuxeoProperties.add(getNuxeoProperty(Environment.NUXEO_TMP_DIR));
915        nuxeoProperties.add(getNuxeoProperty(Environment.NUXEO_MP_DIR));
916        if (!DEFAULT_NUXEO_CONTEXT_PATH.equals(
917                configurationGenerator.getUserConfig().getProperty(Environment.NUXEO_CONTEXT_PATH))) {
918            nuxeoProperties.add(getNuxeoProperty(Environment.NUXEO_CONTEXT_PATH));
919        }
920        if (overrideJavaTmpDir) {
921            nuxeoProperties.add("-Djava.io.tmpdir="
922                    + configurationGenerator.getUserConfig().getProperty(Environment.NUXEO_TMP_DIR));
923        }
924        return nuxeoProperties;
925    }
926
927    private String getNuxeoProperty(String property) {
928        return "-D" + property + "=" + configurationGenerator.getUserConfig().getProperty(property);
929    }
930
931    protected String addToClassPath(String cp, String filename) {
932        File classPathEntry = new File(configurationGenerator.getNuxeoHome(), filename);
933        if (!classPathEntry.exists()) {
934            classPathEntry = new File(filename);
935        }
936        if (!classPathEntry.exists()) {
937            throw new RuntimeException("Tried to add nonexistent classpath entry: " + filename);
938        }
939        cp += System.getProperty("path.separator") + classPathEntry.getPath();
940        return cp;
941    }
942
943    /**
944     * @since 5.6
945     */
946    protected static Options initParserOptions() {
947        Options options = new Options();
948        // help option
949        options.addOption(Option.builder("h").longOpt(OPTION_HELP).desc(OPTION_HELP_DESC).build());
950        // Quiet option
951        options.addOption(Option.builder("q").longOpt(OPTION_QUIET).desc(OPTION_QUIET_DESC).build());
952        { // Debug options (mutually exclusive)
953            OptionGroup debugOptions = new OptionGroup();
954            // Debug option
955            debugOptions.addOption(Option.builder("d")
956                                         .longOpt(OPTION_DEBUG)
957                                         .desc(OPTION_DEBUG_DESC)
958                                         .hasArgs()
959                                         .argName(OPTION_DEBUG_CATEGORY_ARG_NAME)
960                                         .optionalArg(true)
961                                         .valueSeparator(',')
962                                         .build());
963            // Debug category option
964            debugOptions.addOption(Option.builder(OPTION_DEBUG_CATEGORY)
965                                         .desc(OPTION_DEBUG_CATEGORY_DESC)
966                                         .hasArgs()
967                                         .argName(OPTION_DEBUG_CATEGORY_ARG_NAME)
968                                         .optionalArg(true)
969                                         .valueSeparator(',')
970                                         .build());
971            options.addOptionGroup(debugOptions);
972        }
973        // For help output purpose only: that option is managed and
974        // swallowed by the nuxeoctl Shell script
975        options.addOption(Option.builder()
976                                .longOpt("debug-launcher")
977                                .desc("Linux-only. Activate Java debugging mode on the Launcher.")
978                                .build());
979        // Instance CLID option
980        options.addOption(Option.builder().longOpt(OPTION_CLID).desc(OPTION_CLID_DESC).hasArg().build());
981        // Register offline option
982        options.addOption(Option.builder().longOpt(OPTION_OFFLINE).desc(OPTION_OFFLINE_DESC).build());
983        // Register renew option
984        options.addOption(Option.builder().longOpt(OPTION_RENEW).desc(OPTION_RENEW_DESC).build());
985        { // Output options (mutually exclusive)
986            OptionGroup outputOptions = new OptionGroup();
987            // XML option
988            outputOptions.addOption(Option.builder().longOpt(OPTION_XML).desc(OPTION_XML_DESC).build());
989            // JSON option
990            outputOptions.addOption(Option.builder().longOpt(OPTION_JSON).desc(OPTION_JSON_DESC).build());
991            options.addOptionGroup(outputOptions);
992        }
993        // GUI option
994        options.addOption(Option.builder()
995                                .longOpt(OPTION_GUI)
996                                .desc(OPTION_GUI_DESC)
997                                .hasArg()
998                                .argName("true|false|yes|no")
999                                .build());
1000        // Package management option
1001        options.addOption(Option.builder().longOpt(OPTION_NODEPS).desc(OPTION_NODEPS_DESC).build());
1002        // Relax on target platform option
1003        options.addOption(Option.builder()
1004                                .longOpt(OPTION_RELAX)
1005                                .desc(OPTION_RELAX_DESC)
1006                                .hasArg()
1007                                .argName("true|false|yes|no|ask")
1008                                .build());
1009        // Accept option
1010        options.addOption(Option.builder()
1011                                .longOpt(OPTION_ACCEPT)
1012                                .desc(OPTION_ACCEPT_DESC)
1013                                .hasArg()
1014                                .argName("true|false|yes|no|ask")
1015                                .build());
1016        // Allow SNAPSHOT option
1017        options.addOption(Option.builder("s").longOpt(OPTION_SNAPSHOT).desc(OPTION_SNAPSHOT_DESC).build());
1018        // Force option
1019        options.addOption(Option.builder("f").longOpt(OPTION_FORCE).desc(OPTION_FORCE_DESC).build());
1020        // Strict option
1021        options.addOption(Option.builder().longOpt(OPTION_STRICT).desc(OPTION_STRICT_DESC).build());
1022
1023        // Ignore missing option
1024        options.addOption(Option.builder("im").longOpt(OPTION_IGNORE_MISSING).desc(OPTION_IGNORE_MISSING_DESC).build());
1025        // Hide deprecation warnings option
1026        options.addOption(
1027                Option.builder("hdw").longOpt(OPTION_HIDE_DEPRECATION).desc(OPTION_HIDE_DEPRECATION_DESC).build());
1028        // Encrypt option
1029        options.addOption(Option.builder()
1030                                .longOpt(OPTION_ENCRYPT)
1031                                .desc(OPTION_ENCRYPT_DESC)
1032                                .hasArg()
1033                                .argName(OPTION_ENCRYPT_ARG_NAME)
1034                                .optionalArg(true)
1035                                .build());
1036        // Output options
1037        options.addOption(Option.builder()
1038                                .longOpt(OPTION_GZIP_OUTPUT)
1039                                .desc(OPTION_GZIP_DESC)
1040                                .hasArg()
1041                                .argName("true|false")
1042                                .optionalArg(true)
1043                                .build());
1044        options.addOption(Option.builder()
1045                                .longOpt(OPTION_PRETTY_PRINT)
1046                                .desc(OPTION_PRETTY_PRINT_DESC)
1047                                .hasArg()
1048                                .argName("true|false")
1049                                .optionalArg(true)
1050                                .build());
1051        options.addOption(Option.builder()
1052                                .longOpt(OPTION_OUTPUT)
1053                                .desc(OPTION_OUTPUT_DESC)
1054                                .hasArg()
1055                                .argName("file")
1056                                .optionalArg(true)
1057                                .build());
1058        { // Config options (mutually exclusive)
1059            OptionGroup configOptions = new OptionGroup();
1060            // Set option
1061            configOptions.addOption(Option.builder()
1062                                          .longOpt(OPTION_SET)
1063                                          .desc(OPTION_SET_DESC)
1064                                          .hasArg()
1065                                          .argName(OPTION_SET_ARG_NAME)
1066                                          .optionalArg(true)
1067                                          .build());
1068            configOptions.addOption(Option.builder().longOpt(OPTION_GET).desc(OPTION_GET_DESC).build());
1069            configOptions.addOption(Option.builder().longOpt(OPTION_GET_REGEXP).desc(OPTION_GET_REGEXP_DESC).build());
1070            options.addOptionGroup(configOptions);
1071        }
1072        return options;
1073    }
1074
1075    /**
1076     * @since 5.6
1077     */
1078    protected static CommandLine parseOptions(String[] args) throws ParseException {
1079        CommandLineParser parser = new DefaultParser();
1080        CommandLine cmdLine = parser.parse(options, args);
1081        if (cmdLine.hasOption(OPTION_HELP)) {
1082            cmdLine.getArgList().add(OPTION_HELP);
1083            setQuiet();
1084        } else if (cmdLine.getArgList().isEmpty()) {
1085            throw new ParseException("Missing command.");
1086        }
1087        // Common options to the Launcher and the ConfigurationGenerator
1088        if (cmdLine.hasOption(OPTION_QUIET) || cmdLine.hasOption(OPTION_XML) || cmdLine.hasOption(OPTION_JSON)) {
1089            setQuiet();
1090        }
1091        if (cmdLine.hasOption(OPTION_DEBUG)) {
1092            setDebug(cmdLine.getOptionValues(OPTION_DEBUG));
1093        }
1094        if (cmdLine.hasOption(OPTION_DEBUG_CATEGORY)) {
1095            setDebug(cmdLine.getOptionValues(OPTION_DEBUG_CATEGORY));
1096        }
1097        if (cmdLine.hasOption(OPTION_FORCE) || cmdLine.hasOption(OPTION_STRICT)) {
1098            setStrict(true);
1099        }
1100        return cmdLine;
1101    }
1102
1103    public static void main(String[] args) {
1104        NuxeoLauncher launcher = null;
1105        try {
1106            launcher = createLauncher(args);
1107            if (launcher.commandRequiresNoGUI()) {
1108                launcher.useGui = false;
1109            }
1110            if (launcher.useGui && launcher.getGUI() == null) {
1111                launcher.setGUI(new NuxeoLauncherGUI(launcher));
1112            }
1113            launch(launcher);
1114        } catch (LauncherRestartException e) {
1115            log.info("Restarting launcher...");
1116            System.exit(EXIT_CODE_LAUNCHER_CHANGED);
1117        } catch (ParseException e) {
1118            log.error("Invalid command line. " + e.getMessage());
1119            log.debug(e, e);
1120            printShortHelp();
1121            System.exit(
1122                    launcher == null || launcher.errorValue == EXIT_CODE_OK ? EXIT_CODE_INVALID : launcher.errorValue);
1123        } catch (IOException | PackageException | ConfigurationException | GeneralSecurityException e) {
1124            log.error(e.getMessage());
1125            log.debug(e, e);
1126            System.exit(
1127                    launcher == null || launcher.errorValue == EXIT_CODE_OK ? EXIT_CODE_INVALID : launcher.errorValue);
1128        } catch (Exception e) {
1129            log.error("Cannot execute command. " + e.getMessage(), e);
1130            log.debug(e, e);
1131            System.exit(EXIT_CODE_ERROR);
1132        }
1133    }
1134
1135    /**
1136     * @since 5.5
1137     */
1138    public static void launch(final NuxeoLauncher launcher)
1139            throws IOException, PackageException, ConfigurationException, ParseException, GeneralSecurityException {
1140        boolean commandSucceeded = true;
1141        if (launcher.commandIs(null)) {
1142            return;
1143        }
1144        if (launcher.commandRequiresNoRunningServer()) {
1145            launcher.checkNoRunningServer();
1146        }
1147        if (launcher.commandIs(OPTION_HELP)) {
1148            printLongHelp();
1149        } else if (launcher.commandIs("status")) {
1150            String statusMsg = launcher.status();
1151            launcher.errorValue = launcher.getStatus();
1152            if (!quiet) {
1153                log.warn(statusMsg);
1154                if (launcher.isStarted()) {
1155                    log.info("Go to " + launcher.getURL());
1156                    log.info(launcher.getStartupSummary());
1157                }
1158            }
1159        } else if (launcher.commandIs("startbg")) {
1160            commandSucceeded = launcher.doStart();
1161        } else if (launcher.commandIs("start")) {
1162            if (launcher.useGui) {
1163                launcher.getGUI().start();
1164            } else {
1165                commandSucceeded = launcher.doStartAndWait();
1166            }
1167        } else if (launcher.commandIs("console")) {
1168            launcher.executor.execute(() -> {
1169                launcher.addShutdownHook();
1170                try {
1171                    if (!launcher.doStart(true)) {
1172                        launcher.removeShutdownHook();
1173                        System.exit(1);
1174                    } else if (!quiet) {
1175                        log.info("Go to " + launcher.getURL());
1176                    }
1177                } catch (PackageException e) {
1178                    log.error("Could not initialize the packaging subsystem", e);
1179                    launcher.removeShutdownHook();
1180                    System.exit(EXIT_CODE_ERROR);
1181                }
1182            });
1183        } else if (launcher.commandIs("stop")) {
1184            if (launcher.useGui) {
1185                launcher.getGUI().stop();
1186            } else {
1187                launcher.stop();
1188            }
1189        } else if (launcher.commandIs("restartbg")) {
1190            launcher.stop();
1191            commandSucceeded = launcher.doStart();
1192        } else if (launcher.commandIs("restart")) {
1193            launcher.stop();
1194            commandSucceeded = launcher.doStartAndWait();
1195        } else if (launcher.commandIs("wizard")) {
1196            commandSucceeded = launcher.startWizard();
1197        } else if (launcher.commandIs("configure")) {
1198            launcher.configure();
1199        } else if (launcher.commandIs("pack")) {
1200            launcher.pack();
1201        } else if (launcher.commandIs("mp-list")) {
1202            launcher.pkgList();
1203        } else if (launcher.commandIs("mp-listall")) {
1204            launcher.pkgListAll();
1205        } else if (launcher.commandIs("mp-init")) {
1206            commandSucceeded = launcher.pkgInit();
1207        } else if (launcher.commandIs("mp-purge")) {
1208            commandSucceeded = launcher.pkgPurge();
1209        } else if (launcher.commandIs("mp-add")) {
1210            if (launcher.cmdLine.hasOption(OPTION_NODEPS)) {
1211                commandSucceeded = launcher.pkgAdd(launcher.params);
1212            } else {
1213                commandSucceeded = launcher.pkgRequest(Arrays.asList(launcher.params), null, null, null);
1214            }
1215        } else if (launcher.commandIs("mp-install")) {
1216            if (launcher.cmdLine.hasOption(OPTION_NODEPS)) {
1217                commandSucceeded = launcher.pkgInstall(launcher.params);
1218            } else {
1219                commandSucceeded = launcher.pkgRequest(null, Arrays.asList(launcher.params), null, null);
1220            }
1221        } else if (launcher.commandIs("mp-uninstall")) {
1222            if (launcher.cmdLine.hasOption(OPTION_NODEPS)) {
1223                commandSucceeded = launcher.pkgUninstall(launcher.params);
1224            } else {
1225                commandSucceeded = launcher.pkgRequest(null, null, Arrays.asList(launcher.params), null);
1226            }
1227        } else if (launcher.commandIs("mp-remove")) {
1228            if (launcher.cmdLine.hasOption(OPTION_NODEPS)) {
1229                commandSucceeded = launcher.pkgRemove(launcher.params);
1230            } else {
1231                commandSucceeded = launcher.pkgRequest(null, null, null, Arrays.asList(launcher.params));
1232            }
1233        } else if (launcher.commandIs("mp-request")) {
1234            if (launcher.cmdLine.hasOption(OPTION_NODEPS)) {
1235                throw new ParseException("The command mp-request is not available with the --nodeps option");
1236            } else {
1237                commandSucceeded = launcher.pkgCompoundRequest(Arrays.asList(launcher.params));
1238            }
1239        } else if (launcher.commandIs("mp-set")) {
1240            commandSucceeded = launcher.pkgSetRequest(Arrays.asList(launcher.params),
1241                    launcher.cmdLine.hasOption(OPTION_NODEPS));
1242        } else if (launcher.commandIs("mp-hotfix")) {
1243            commandSucceeded = launcher.pkgHotfix();
1244        } else if (launcher.commandIs("mp-upgrade")) {
1245            commandSucceeded = launcher.pkgUpgrade();
1246        } else if (launcher.commandIs("mp-reset")) {
1247            commandSucceeded = launcher.pkgReset();
1248        } else if (launcher.commandIs("mp-update")) {
1249            commandSucceeded = launcher.pkgRefreshCache();
1250        } else if (launcher.commandIs("showconf")) {
1251            launcher.showConfig();
1252        } else if (launcher.commandIs("mp-show")) {
1253            commandSucceeded = launcher.pkgShow(launcher.params);
1254        } else if (launcher.commandIs("encrypt")) {
1255            launcher.encrypt();
1256        } else if (launcher.commandIs("decrypt")) {
1257            launcher.decrypt();
1258        } else if (launcher.commandIs("config")) {
1259            launcher.config();
1260        } else if (launcher.commandIs("register")) {
1261            commandSucceeded = launcher.register();
1262        } else if (launcher.commandIs("register-trial")) {
1263            commandSucceeded = launcher.registerTrial();
1264        } else if (launcher.commandIs("connect-report")) {
1265            boolean gzip = Boolean.parseBoolean(launcher.cmdLine.getOptionValue(OPTION_GZIP_OUTPUT, "true"));
1266            boolean prettyprinting = Boolean.parseBoolean(
1267                    launcher.cmdLine.getOptionValue(OPTION_PRETTY_PRINT, "false"));
1268            Path outputpath;
1269            if (launcher.cmdLine.hasOption(OPTION_OUTPUT)) {
1270                outputpath = Paths.get(launcher.cmdLine.getOptionValue(OPTION_OUTPUT));
1271            } else {
1272                Path dir = Paths.get(
1273                        launcher.configurationGenerator.getUserConfig().getProperty(Environment.NUXEO_TMP_DIR));
1274                outputpath = dir.resolve("nuxeo-connect-tools-report.json".concat(gzip ? ".gz" : ""));
1275            }
1276            log.info("Dumping connect report in " + outputpath);
1277            try (OutputStream output = openOutput(outputpath, gzip)) {
1278                commandSucceeded = launcher.dumpConnectReport(output, prettyprinting);
1279            }
1280        } else {
1281            log.error("Unknown command " + launcher.command);
1282            printLongHelp();
1283            launcher.errorValue = EXIT_CODE_INVALID;
1284        }
1285        if (launcher.xmlOutput && launcher.command.startsWith("mp-")) {
1286            launcher.printXMLOutput();
1287        }
1288        commandSucceeded = commandSucceeded && launcher.errorValue == EXIT_CODE_OK;
1289        if (!commandSucceeded && !quiet || debug) {
1290            launcher.cset.log(commandSucceeded);
1291        }
1292        if (!commandSucceeded) {
1293            System.exit(launcher.errorValue);
1294        }
1295    }
1296
1297    protected static OutputStream openOutput(Path path, boolean gzip) throws IOException {
1298        OutputStream output = Files.newOutputStream(path, StandardOpenOption.TRUNCATE_EXISTING,
1299                StandardOpenOption.CREATE);
1300        if (gzip) {
1301            output = new GZIPOutputStream(output);
1302        }
1303        return output;
1304    }
1305
1306    /**
1307     * Prompts for a valid email address according to RFC 822 standards. The remote service may apply stricter
1308     * constraints on email validation such as some black listed domains.
1309     *
1310     * @return the user input. Never null.
1311     * @throws ConfigurationException If the user input is read from stdin and is {@code null} or does not match the
1312     *             {@code regex}
1313     * @since 8.3
1314     */
1315    public String promptEmail() throws IOException, ConfigurationException {
1316        EmailValidator validator = EmailValidator.getInstance();
1317        final String message = "Email Address: ";
1318        final String error = "Invalid email address.";
1319        return prompt(message, validator::isValid, error);
1320    }
1321
1322    /**
1323     * @since 8.3
1324     */
1325    public String promptDescription() throws ConfigurationException, IOException {
1326        return prompt("Description: ", null, null);
1327    }
1328
1329    /**
1330     * Prompt for a value read from the console or stdin.
1331     *
1332     * @param message message to display at prompt
1333     * @param predicate a predicate that must match a correct user input. Ignored if {@code null}.
1334     * @param error an error message to display or raise when the user input is {@code null} or does not match the
1335     *            {@code regex}
1336     * @return the user input. Never null.
1337     * @throws ConfigurationException If the user input is read from stdin and is {@code null} or does not match the
1338     *             {@code regex}
1339     * @since 8.3
1340     */
1341    public String prompt(String message, Predicate<String> predicate, String error)
1342            throws IOException, ConfigurationException {
1343        boolean doRegexMatch = predicate != null;
1344        String value;
1345        Console console = System.console();
1346        if (console != null) {
1347            value = console.readLine(message);
1348            while (value == null || doRegexMatch && !predicate.test(value)) {
1349                console.printf(error + "\n", value);
1350                value = console.readLine(message);
1351            }
1352        } else { // try reading from stdin
1353            value = IOUtils.toString(System.in, UTF_8);
1354            if (value == null || doRegexMatch && !predicate.test(value)) {
1355                throw new ConfigurationException(error);
1356            }
1357        }
1358        return value;
1359    }
1360
1361    /**
1362     * @param message message to display at prompt
1363     * @since 8.3
1364     */
1365    public char[] promptPassword(String message) throws IOException {
1366        Console console = System.console();
1367        if (console != null) {
1368            return console.readPassword(message);
1369        } else { // try reading from stdin
1370            return IOUtils.toCharArray(System.in, UTF_8);
1371        }
1372    }
1373
1374    /**
1375     * @param confirmation if true, password is asked twice.
1376     * @since 8.3
1377     */
1378    public char[] promptPassword(boolean confirmation) throws IOException, ConfigurationException {
1379        char[] pwd = promptPassword("Please enter your password: ");
1380        if (confirmation) {
1381            char[] pwdVerification = promptPassword("Please re-enter your password: ");
1382            if (!Arrays.equals(pwd, pwdVerification)) {
1383                throw new ConfigurationException("Passwords do not match.");
1384            }
1385        }
1386        return pwd;
1387    }
1388
1389    /**
1390     * @return a {@link NuxeoClientInstanceType}. Never {@code null}.
1391     * @since 8.3
1392     */
1393    public NuxeoClientInstanceType promptInstanceType() throws IOException, ConfigurationException {
1394        NuxeoClientInstanceType type;
1395        Console console = System.console();
1396        if (console == null) {
1397            String typeStr = IOUtils.toString(System.in, UTF_8);
1398            type = NuxeoClientInstanceType.fromString(typeStr);
1399            if (type == null) {
1400                throw new ConfigurationException("Unknown type: " + typeStr);
1401            }
1402            return type;
1403        }
1404        do {
1405            String s = console.readLine("Instance type (dev|preprod|prod): [dev] ");
1406            if (StringUtils.isBlank(s)) {
1407                type = NuxeoClientInstanceType.DEV;
1408            } else {
1409                type = NuxeoClientInstanceType.fromString(s);
1410            }
1411        } while (type == null);
1412        return type;
1413    }
1414
1415    /**
1416     * @param projects available projects the user must choose one amongst.
1417     * @return a project. Never null.
1418     * @throws ConfigurationException If {@code projects} is empty or if there is not such a project named as the
1419     *             parameter read from stdin.
1420     * @since 8.3
1421     */
1422    public ConnectProject promptProject(@NotNull List<ConnectProject> projects)
1423            throws ConfigurationException, IOException, PackageException {
1424        if (projects.isEmpty()) {
1425            throw new ConfigurationException("You don't have access to any project.");
1426        }
1427        if (projects.size() == 1) {
1428            return projects.get(0);
1429        }
1430
1431        String projectName;
1432        Console console = System.console();
1433        if (console == null) {
1434            projectName = IOUtils.toString(System.in, UTF_8);
1435            ConnectProject project = getConnectRegistrationBroker().getProjectByName(projectName, projects);
1436            if (project == null) {
1437                throw new ConfigurationException("Unknown project: " + projectName);
1438            }
1439            return project;
1440        }
1441
1442        System.out.println("Available projects:");
1443        int i = 0;
1444        boolean hasNextPage = true;
1445        while (true) {
1446            if (i > 0 && !SystemUtils.IS_OS_WINDOWS) {
1447                // Remove last line to only have projects
1448                System.out.print("\33[1A\33[2K");
1449            }
1450
1451            int fromIndex = i * PAGE_SIZE;
1452            int toIndex = (i + 1) * PAGE_SIZE;
1453            if (toIndex >= projects.size()) {
1454                toIndex = projects.size();
1455                hasNextPage = false;
1456            }
1457
1458            projects.subList(fromIndex, toIndex)
1459                    .forEach(project -> System.out.println("\t- " + project.getSymbolicName()));
1460            if (toIndex < projects.size()) {
1461                int pageLeft = (projects.size() - i * PAGE_SIZE + PAGE_SIZE - 1) / PAGE_SIZE;
1462                System.out.print(String.format("Project name (press Enter for next page; %d pages left): ", pageLeft));
1463            } else {
1464                System.out.print("Project name: ");
1465            }
1466            if (hasNextPage) {
1467                i++;
1468            }
1469            projectName = console.readLine();
1470            if (StringUtils.isNotEmpty(projectName)) {
1471                ConnectProject project = getConnectRegistrationBroker().getProjectByName(projectName, projects);
1472                if (project != null) {
1473                    return project;
1474                }
1475                System.err.println("Unknown project: " + projectName);
1476                i = 0;
1477                hasNextPage = true;
1478            }
1479        }
1480    }
1481
1482    /**
1483     * Register the instance, generating the CLID or using the passed one; or renew a registration.
1484     *
1485     * <pre>
1486     * {@code
1487     * nuxeoctl register [<username> [<project> [<type> <description>] [pwd]]]
1488     *
1489     * nuxeoctl register --clid <file>
1490     *
1491     * nuxeoctl register --renew [--clid <file>]
1492     * }
1493     * </pre>
1494     *
1495     * Missing parameters are read from stdin.
1496     *
1497     * @return true if succeed
1498     * @since 8.3
1499     */
1500    public boolean register() throws IOException, ConfigurationException, PackageException {
1501        // register --renew
1502        if (cmdLine.hasOption(OPTION_RENEW)) {
1503            if (params.length != 0) {
1504                throw new ConfigurationException("Unexpected arguments for --renew.");
1505            }
1506            return registerRenew();
1507        }
1508
1509        // register --clid <file>
1510        if (cmdLine.hasOption(OPTION_CLID) && params.length == 0) {
1511            return registerSaveCLID();
1512        }
1513
1514        // register --offline
1515        if (cmdLine.hasOption(OPTION_OFFLINE) && params.length == 0) {
1516            return registerOffline();
1517        }
1518
1519        return registerRemoteInstance();
1520    }
1521
1522    protected boolean registerRenew() throws IOException {
1523        try {
1524            getConnectRegistrationBroker().remoteRenewRegistration();
1525        } catch (RegistrationException e) {
1526            log.debug(e, e);
1527            errorValue = EXIT_CODE_ERROR;
1528            return false;
1529        }
1530        log.info("Server registration renewed");
1531        return true;
1532    }
1533
1534    protected boolean registerSaveCLID() throws IOException, ConfigurationException {
1535        // at this point the --clid option has already been processed and the file's CLID loaded
1536        try {
1537            getConnectBroker().saveCLID();
1538        } catch (NoCLID e) {
1539            throw new ConfigurationException(e);
1540        }
1541        log.info("Server registration saved");
1542        return true;
1543    }
1544
1545    protected boolean registerOffline() throws IOException, ConfigurationException {
1546        log.info("\nTo register your instance:");
1547        log.info(String.format("1. Visit %s/connect/registerInstance", ConnectUrlConfig.getBaseUrl()));
1548        log.info(String.format(
1549                "2. Select the project on which you want the instance to be registered and copy the technical identifier found below (CTID):\n\n%s\n",
1550                TechnicalInstanceIdentifier.instance().getCTID()));
1551        Date expirationDate = new Date();
1552        prompt("3. Enter the given identifier to register your instance (CLID): ", strCLID -> {
1553            try {
1554                getConnectRegistrationBroker().registerLocal(strCLID, "");
1555                long timestamp = Long.parseLong(StringUtils.substringBetween(strCLID, ".", "."));
1556                expirationDate.setTime(timestamp * 1000);
1557            } catch (IOException | ConfigurationException | NumberFormatException e) {
1558                return false;
1559            }
1560            return true;
1561        }, "This identifier is invalid or cannot be read properly.");
1562
1563        log.info("Server registration saved");
1564        log.info("Your Nuxeo Online Services is valid until " + expirationDate);
1565        return true;
1566    }
1567
1568    protected boolean registerRemoteInstance() throws IOException, ConfigurationException, PackageException {
1569        // 0/1 param: [<username>]
1570        // 2/3 params: <username> <project> [pwd]
1571        // 4/5 params: <username> <project> <type> <description> [pwd]
1572        if (params.length > 5) {
1573            throw new ConfigurationException("Wrong number of arguments.");
1574        }
1575        String username;
1576        if (params.length > 0) {
1577            username = params[0];
1578        } else {
1579            username = prompt("Username: ", StringUtils::isNotBlank, "Username cannot be empty.");
1580        }
1581        char[] password;
1582        if (params.length == 3 || params.length == 5) {
1583            password = params[params.length - 1].toCharArray();
1584        } else {
1585            password = promptPassword(false);
1586        }
1587        ConnectProject project;
1588        List<ConnectProject> projs = getConnectRegistrationBroker().getAvailableProjects(username, password);
1589        if (params.length > 1) {
1590            String projectName = params[1];
1591            project = getConnectRegistrationBroker().getProjectByName(projectName, projs);
1592            if (project == null) {
1593                throw new ConfigurationException("Unknown project: " + projectName);
1594            }
1595        } else {
1596            project = promptProject(projs);
1597        }
1598        NuxeoClientInstanceType type;
1599        String description;
1600        if (params.length > 3) {
1601            type = NuxeoClientInstanceType.fromString(params[2]);
1602            if (type == null) {
1603                throw new ConfigurationException("Unknown type: " + params[2]);
1604            }
1605            description = params[3];
1606        } else {
1607            type = promptInstanceType();
1608            description = promptDescription();
1609        }
1610
1611        return registerRemoteInstance(username, password, project, type, description);
1612    }
1613
1614    protected boolean registerRemoteInstance(String username, char[] password, ConnectProject project,
1615            NuxeoClientInstanceType type, String description) throws IOException, ConfigurationException {
1616        getConnectRegistrationBroker().registerRemote(username, password, project.getUuid(), type, description);
1617        log.info(String.format("Server registered to %s for project %s\nType: %s\nDescription: %s", username, project,
1618                type, description));
1619        return true;
1620    }
1621
1622    /**
1623     * Register a trial project. The command synopsis:
1624     *
1625     * <pre>
1626     * <code>
1627     * nuxeoctl register-trial [ &lt;first&gt; &lt;last&gt; &lt;email&gt; &lt;company&gt; &lt;project&gt; ]
1628     * </code>
1629     * </pre>
1630     *
1631     * @since 8.3
1632     * @deprecated Since 9.3: To register for a free 30 day trial on Nuxeo Online Services, please visit
1633     *             https://connect.nuxeo.com/register
1634     */
1635    @Deprecated
1636    public boolean registerTrial() throws IOException, ConfigurationException, PackageException {
1637        String msg = "This command is deprecated. To register for a free 30 day trial on Nuxeo Online Services,"
1638                + " please visit https://connect.nuxeo.com/register";
1639        throw new UnsupportedOperationException(msg);
1640    }
1641
1642    /**
1643     * @since 7.4
1644     */
1645    protected void encrypt() throws GeneralSecurityException {
1646        Crypto crypto = configurationGenerator.getCrypto();
1647        String algorithm = cmdLine.getOptionValue(OPTION_ENCRYPT, null);
1648        if (params.length == 0) {
1649            Console console = System.console();
1650            String encryptedString;
1651            if (console != null) {
1652                encryptedString = crypto.encrypt(algorithm,
1653                        Crypto.getBytes(console.readPassword("Please enter the value to encrypt: ")));
1654            } else { // try reading from stdin
1655                try {
1656                    encryptedString = crypto.encrypt(algorithm, IOUtils.toByteArray(System.in));
1657                } catch (IOException e) {
1658                    log.debug(e, e);
1659                    errorValue = EXIT_CODE_ERROR;
1660                    return;
1661                }
1662            }
1663            System.out.println(encryptedString);
1664        } else {
1665            for (String strToEncrypt : params) {
1666                String encryptedString = crypto.encrypt(algorithm, strToEncrypt.getBytes());
1667                System.out.println(encryptedString);
1668            }
1669        }
1670    }
1671
1672    /**
1673     * @since 7.4
1674     */
1675    protected void decrypt() {
1676        Crypto crypto = configurationGenerator.getCrypto();
1677        askCryptoKeyAndDecrypt(crypto, params).forEach(System.out::println);
1678    }
1679
1680    protected List<String> askCryptoKeyAndDecrypt(Crypto crypto, String... values) {
1681        boolean validKey;
1682        Console console = System.console();
1683        if (console != null) {
1684            validKey = crypto.verifyKey(console.readPassword("Please enter the secret key: "));
1685        } else { // try reading from stdin
1686            try {
1687                validKey = crypto.verifyKey(IOUtils.toByteArray(System.in));
1688            } catch (IOException e) {
1689                log.debug(e, e);
1690                errorValue = EXIT_CODE_ERROR;
1691                return null;
1692            }
1693        }
1694        if (!validKey) {
1695            errorValue = EXIT_CODE_INVALID;
1696            return null;
1697        }
1698        return Stream.of(values).map(crypto::decrypt).map(Crypto::getChars).map(String::new).collect(
1699                Collectors.toList());
1700    }
1701
1702    /**
1703     * @since 7.4
1704     */
1705    protected void config() throws ConfigurationException, IOException, GeneralSecurityException {
1706        if (cmdLine.hasOption(OPTION_SET)
1707                || !cmdLine.hasOption(OPTION_GET) && !cmdLine.hasOption(OPTION_GET_REGEXP) && params.length == 2) {
1708            setConfigProperties();
1709        } else { // OPTION_GET || OPTION_GET_REGEXP || !OPTION_SET &&
1710                 // params.length != 2
1711            getConfigProperties();
1712        }
1713    }
1714
1715    /**
1716     * @since 7.4
1717     */
1718    protected void getConfigProperties() {
1719        boolean isRegexp = cmdLine.hasOption(OPTION_GET_REGEXP);
1720        CryptoProperties userConfig = configurationGenerator.getUserConfig();
1721        List<String> keys;
1722        if (isRegexp) {
1723            keys = new ArrayList<>();
1724            for (Object key : userConfig.keySet()) {
1725                for (String param : params) {
1726                    Pattern pattern = Pattern.compile(param, Pattern.CASE_INSENSITIVE);
1727                    if (pattern.matcher((String) key).find()) {
1728                        keys.add((String) key);
1729                    }
1730                }
1731            }
1732            if (keys.isEmpty()) {
1733                errorValue = EXIT_CODE_NOT_CONFIGURED;
1734            }
1735        } else {
1736            keys = Arrays.asList(params);
1737        }
1738
1739        Crypto crypto = userConfig.getCrypto();
1740        boolean keyChecked = false; // Secret key is asked only once
1741        boolean raw = true;
1742        StringBuilder sb = new StringBuilder();
1743        final String newLine = System.getProperty("line.separator");
1744        for (String key : keys) {
1745            String value = userConfig.getProperty(key, raw);
1746            if (value == null) {
1747                errorValue = EXIT_CODE_NOT_CONFIGURED;
1748                sb.append(OUTPUT_UNSET_VALUE).append(newLine);
1749            } else {
1750                if (raw && !keyChecked && Crypto.isEncrypted(value)) {
1751                    keyChecked = true;
1752                    List<String> decryptedValues = askCryptoKeyAndDecrypt(crypto, value);
1753                    if (decryptedValues != null) {
1754                        raw = false;
1755                        value = decryptedValues.get(0);
1756                    }
1757                }
1758                if (isRegexp) {
1759                    sb.append(key).append('=');
1760                }
1761                sb.append(value).append(newLine);
1762            }
1763        }
1764        System.out.print(sb.toString());
1765    }
1766
1767    /**
1768     * @since 7.4
1769     */
1770    protected void setConfigProperties() throws ConfigurationException, IOException, GeneralSecurityException {
1771        Crypto crypto = configurationGenerator.getCrypto();
1772        boolean doEncrypt = cmdLine.hasOption(OPTION_ENCRYPT);
1773        String algorithm = cmdLine.getOptionValue(OPTION_ENCRYPT, null);
1774        Map<String, String> changedParameters = new HashMap<>();
1775        for (Iterator<String> iterator = Arrays.asList(params).iterator(); iterator.hasNext();) {
1776            String key = iterator.next();
1777            String value;
1778            if (iterator.hasNext()) {
1779                value = iterator.next();
1780                if (doEncrypt) {
1781                    value = crypto.encrypt(algorithm, value.getBytes());
1782                } else if (Environment.CRYPT_KEY.equals(key) || Environment.CRYPT_KEYSTORE_PASS.equals(key)) {
1783                    value = Base64.encodeBase64String(value.getBytes());
1784                }
1785            } else {
1786                Console console = System.console();
1787                if (console != null) {
1788                    final String fmt = "Please enter the value for %s: ";
1789                    if (doEncrypt) {
1790                        value = crypto.encrypt(algorithm, Crypto.getBytes(console.readPassword(fmt, key)));
1791                    } else if (Environment.CRYPT_KEY.equals(key) || Environment.CRYPT_KEYSTORE_PASS.equals(key)) {
1792                        value = Base64.encodeBase64String(Crypto.getBytes(console.readPassword(fmt, key)));
1793                    } else {
1794                        value = console.readLine(fmt, key);
1795                    }
1796                } else { // try reading from stdin
1797                    try {
1798                        if (doEncrypt) {
1799                            value = crypto.encrypt(algorithm, IOUtils.toByteArray(System.in));
1800                        } else if (Environment.CRYPT_KEY.equals(key) || Environment.CRYPT_KEYSTORE_PASS.equals(key)) {
1801                            value = Base64.encodeBase64String(IOUtils.toByteArray(System.in));
1802                        } else {
1803                            value = IOUtils.toString(System.in, UTF_8);
1804                        }
1805                    } catch (IOException e) {
1806                        log.debug(e, e);
1807                        errorValue = EXIT_CODE_ERROR;
1808                        return;
1809                    }
1810                }
1811            }
1812            changedParameters.put(key, value);
1813        }
1814        String template = cmdLine.getOptionValue(OPTION_SET);
1815        Map<String, String> oldValues;
1816        if (template == null) {
1817            oldValues = configurationGenerator.setProperties(changedParameters);
1818        } else {
1819            oldValues = configurationGenerator.setProperties(template, changedParameters);
1820        }
1821        log.debug("Old values: " + oldValues);
1822    }
1823
1824    /**
1825     * Since 5.5
1826     */
1827    protected boolean pack() {
1828        try {
1829            configurationGenerator.setProperty(PARAM_UPDATECENTER_DISABLED, "true");
1830            List<String> startCommand = new ArrayList<>();
1831            startCommand.add(getJavaExecutable().getPath());
1832            startCommand.addAll(getJavaOptsProperty(Function.identity()));
1833            startCommand.add("-cp");
1834            String classpath = getClassPath();
1835            classpath = addToClassPath(classpath, "bin" + File.separator + "nuxeo-launcher.jar");
1836            classpath = getClassPath(classpath, configurationGenerator.getServerConfigurator().getServerLibDir());
1837            classpath = getClassPath(classpath, configurationGenerator.getServerConfigurator().getNuxeoLibDir());
1838            classpath = getClassPath(classpath, new File(configurationGenerator.getRuntimeHome(), "bundles"));
1839            startCommand.add(classpath);
1840            startCommand.addAll(getNuxeoProperties());
1841            if (configurationGenerator.isTomcat) {
1842                startCommand.add(PACK_TOMCAT_CLASS);
1843            } else {
1844                errorValue = EXIT_CODE_ERROR;
1845                return false;
1846            }
1847            startCommand.add(configurationGenerator.getRuntimeHome().getPath());
1848            startCommand.addAll(Arrays.asList(params));
1849            ProcessBuilder pb = new ProcessBuilder(getOSCommand(startCommand));
1850            pb.directory(configurationGenerator.getNuxeoHome());
1851            log.debug("Pack command: " + pb.command());
1852            Process process = pb.start();
1853            ArrayList<ThreadedStreamGobbler> sgArray = logProcessStreams(process, true);
1854            Thread.sleep(100);
1855            process.waitFor();
1856            waitForProcessStreams(sgArray);
1857        } catch (InterruptedException e) {
1858            Thread.currentThread().interrupt();
1859            throw new RuntimeException(e);
1860        } catch (IOException e) {
1861            errorValue = EXIT_CODE_ERROR;
1862            log.error("Could not start process", e);
1863        } catch (ConfigurationException e) {
1864            errorValue = EXIT_CODE_ERROR;
1865            log.error(e);
1866        }
1867        return errorValue == EXIT_CODE_OK;
1868    }
1869
1870    protected boolean startWizard() throws PackageException {
1871        if (!configurationGenerator.getServerConfigurator().isWizardAvailable()) {
1872            log.error("Sorry, the wizard is not available within that server.");
1873            return false;
1874        }
1875        if (isRunning()) {
1876            log.error("Server already running. " + "Please stop it before calling \"wizard\" command "
1877                    + "or use the Admin Center instead of the wizard.");
1878            return false;
1879        }
1880        if (reloadConfiguration) {
1881            configurationGenerator = new ConfigurationGenerator(quiet, debug);
1882            configurationGenerator.init();
1883            reloadConfiguration = false;
1884        }
1885        configurationGenerator.getUserConfig().setProperty(ConfigurationGenerator.PARAM_WIZARD_DONE, "false");
1886        return doStart();
1887    }
1888
1889    /**
1890     * @see #doStartAndWait(boolean)
1891     */
1892    public boolean doStartAndWait() throws PackageException {
1893        boolean started = doStartAndWait(false);
1894        if (started && !quiet) {
1895            log.info("Go to " + getURL());
1896        }
1897        return started;
1898    }
1899
1900    /**
1901     * @see #stop(boolean)
1902     */
1903    public void stop() {
1904        stop(false);
1905    }
1906
1907    /**
1908     * Call {@link #doStart(boolean)} with false as parameter.
1909     *
1910     * @see #doStart(boolean)
1911     * @return true if the server started successfully
1912     */
1913    public boolean doStart() throws PackageException {
1914        boolean started = doStart(false);
1915        if (started && !quiet) {
1916            log.info("Go to " + getURL());
1917        }
1918        return started;
1919    }
1920
1921    /**
1922     * Whereas {@link #doStart()} considers the server as started when the process is running, {@link #doStartAndWait()}
1923     * waits for effective start by watching the logs
1924     *
1925     * @param logProcessOutput Must process output stream must be logged or not.
1926     * @return true if the server started successfully
1927     */
1928    public boolean doStartAndWait(boolean logProcessOutput) throws PackageException {
1929        boolean commandSucceeded = false;
1930        if (doStart(logProcessOutput)) {
1931            addShutdownHook();
1932            try {
1933                if (configurationGenerator.isWizardRequired() || waitForEffectiveStart()) {
1934                    commandSucceeded = true;
1935                }
1936                removeShutdownHook();
1937            } catch (InterruptedException e) {
1938                Thread.currentThread().interrupt();
1939                throw new RuntimeException(e);
1940            }
1941        }
1942        return commandSucceeded;
1943    }
1944
1945    protected void removeShutdownHook() {
1946        try {
1947            Runtime.getRuntime().removeShutdownHook(shutdownHook);
1948            log.debug("Removed shutdown hook");
1949        } catch (IllegalStateException e) {
1950            // the virtual machine is already in the process of shutting down
1951        }
1952    }
1953
1954    /**
1955     * @return true if Nuxeo is ready
1956     */
1957    protected boolean waitForEffectiveStart() throws InterruptedException {
1958        long startTime = new Date().getTime();
1959        int startMaxWait = Integer.parseInt(
1960                configurationGenerator.getUserConfig().getProperty(START_MAX_WAIT_PARAM, getDefaultMaxWait()));
1961        log.debug("Will wait for effective start during " + startMaxWait + " seconds.");
1962        final StringBuilder startSummary = new StringBuilder();
1963        final String newLine = System.getProperty("line.separator");
1964        boolean isReady = false;
1965        long deltaTime;
1966        // Wait for status servlet ready
1967        do {
1968            try {
1969                isReady = statusServletClient.init();
1970            } catch (SocketTimeoutException e) {
1971                if (!quiet) {
1972                    System.out.print(".");
1973                }
1974            }
1975            deltaTime = (new Date().getTime() - startTime) / 1000;
1976        } while (!isReady && deltaTime < startMaxWait && isRunning());
1977        // Wait for effective start reported from status servlet
1978        do {
1979            isReady = isStarted();
1980            if (!isReady) {
1981                if (!quiet) {
1982                    System.out.print(".");
1983                }
1984                Thread.sleep(1000);
1985            }
1986            deltaTime = (new Date().getTime() - startTime) / 1000;
1987        } while (!isReady && deltaTime < startMaxWait && isRunning());
1988        if (isReady) {
1989            startSummary.append(newLine).append(getStartupSummary());
1990            long duration = (new Date().getTime() - startTime) / 1000;
1991            startSummary.append(String.format("Started in %dmin%02ds", duration / 60, duration % 60));
1992            if (wasStartupFine()) {
1993                if (!quiet) {
1994                    System.out.println(startSummary);
1995                }
1996            } else {
1997                System.err.println(startSummary);
1998                if (strict) {
1999                    errorValue = EXIT_CODE_ERROR;
2000                    log.error("Shutting down because of unstarted component in strict mode...");
2001                    stop();
2002                    return false;
2003                }
2004            }
2005            return true;
2006        } else if (deltaTime >= startMaxWait) {
2007            if (!quiet) {
2008                System.out.println();
2009            }
2010            log.error("Starting process is taking too long - giving up.");
2011        }
2012        errorValue = EXIT_CODE_ERROR;
2013        return false;
2014    }
2015
2016    /**
2017     * Must be called after {@link #getStartupSummary()}
2018     *
2019     * @since 5.5
2020     * @return last detected status of running Nuxeo server
2021     */
2022    public boolean wasStartupFine() {
2023        return statusServletClient.isStartupFine();
2024    }
2025
2026    /**
2027     * @since 5.5
2028     * @return Nuxeo startup summary
2029     */
2030    public String getStartupSummary() {
2031        try {
2032            return statusServletClient.getStartupSummary();
2033        } catch (SocketTimeoutException e) {
2034            log.warn("Failed to contact Nuxeo for getting startup summary", e);
2035            return "";
2036        }
2037    }
2038
2039    /**
2040     * Starts the server in background.
2041     *
2042     * @return true if server successfully started
2043     */
2044    public boolean doStart(boolean logProcessOutput) throws PackageException {
2045        errorValue = EXIT_CODE_OK;
2046        boolean serverStarted = false;
2047        try {
2048            if (reloadConfiguration) {
2049                configurationGenerator = new ConfigurationGenerator(quiet, debug);
2050                configurationGenerator.init();
2051            } else {
2052                // Ensure reload on next start
2053                reloadConfiguration = true;
2054            }
2055            configure();
2056            configurationGenerator.verifyInstallation();
2057
2058            if (configurationGenerator.isWizardRequired()) {
2059                if (!configurationGenerator.isForceGeneration()) {
2060                    log.error("Cannot start setup wizard with " + ConfigurationGenerator.PARAM_FORCE_GENERATION
2061                            + "=false. Either set it to true or once, either set "
2062                            + ConfigurationGenerator.PARAM_WIZARD_DONE + "=true to skip the wizard.");
2063                    errorValue = EXIT_CODE_NOT_CONFIGURED;
2064                    return false;
2065                }
2066                StringBuilder paramsStr = new StringBuilder();
2067                for (String param : params) {
2068                    paramsStr.append(' ').append(param);
2069                }
2070                System.setProperty(ConfigurationGenerator.PARAM_WIZARD_RESTART_PARAMS, paramsStr.toString());
2071                configurationGenerator.prepareWizardStart();
2072            } else {
2073                configurationGenerator.cleanupPostWizard();
2074            }
2075
2076            log.debug("Check if install in progress...");
2077            if (configurationGenerator.isInstallInProgress()) {
2078                if (!getConnectBroker().executePending(configurationGenerator.getInstallFile(), true, true,
2079                        ignoreMissing)) {
2080                    errorValue = EXIT_CODE_ERROR;
2081                    log.error(String.format(
2082                            "Start interrupted due to failure on pending actions. You can resume with a new start;"
2083                                    + " or you can restore the file '%s', optionally using the '--%s' option.",
2084                            configurationGenerator.getInstallFile().getName(), OPTION_IGNORE_MISSING));
2085                    return false;
2086                }
2087
2088                // configuration will be reloaded, keep wizard value
2089                System.setProperty(ConfigurationGenerator.PARAM_WIZARD_DONE,
2090                        configurationGenerator.getUserConfig().getProperty(ConfigurationGenerator.PARAM_WIZARD_DONE,
2091                                "true"));
2092                return doStart(logProcessOutput);
2093            }
2094
2095            start(logProcessOutput);
2096            serverStarted = isRunning();
2097            if (pid != null) {
2098                File pidFile = new File(configurationGenerator.getPidDir(), "nuxeo.pid");
2099                try (FileWriter writer = new FileWriter(pidFile)) {
2100                    writer.write(pid);
2101                }
2102            }
2103        } catch (ConfigurationException e) {
2104            errorValue = EXIT_CODE_NOT_CONFIGURED;
2105            log.error("Could not run configuration: " + e.getMessage());
2106            log.debug(e, e);
2107        } catch (IOException e) {
2108            errorValue = EXIT_CODE_ERROR;
2109            log.error("Could not start process: " + e.getMessage());
2110            log.debug(e, e);
2111        } catch (InterruptedException e) {
2112            Thread.currentThread().interrupt();
2113            throw new RuntimeException(e);
2114        } catch (IllegalStateException e) {
2115            if (strict) {
2116                // assume program is not configured because of http port binding
2117                // conflict
2118                errorValue = EXIT_CODE_NOT_CONFIGURED;
2119            }
2120            log.error(e.getMessage());
2121        }
2122        return serverStarted;
2123    }
2124
2125    /**
2126     * @since 5.6
2127     */
2128    protected void printXMLOutput() {
2129        try {
2130            JAXBContext jaxbContext = JAXBContext.newInstance(CommandSetInfo.class, CommandInfo.class,
2131                    PackageInfo.class, MessageInfo.class);
2132            printXMLOutput(jaxbContext, cset);
2133        } catch (JAXBException e) {
2134            log.error("Output serialization failed: " + e.getMessage(), e);
2135            errorValue = EXIT_CODE_NOT_RUNNING;
2136        }
2137    }
2138
2139    /**
2140     * @since 5.6
2141     */
2142    protected void printXMLOutput(JAXBContext jaxbContext, Object objectToOutput) {
2143        try {
2144            printXMLOutput(jaxbContext, objectToOutput, System.out);
2145        } catch (JAXBException | XMLStreamException | FactoryConfigurationError e) {
2146            log.error("Output serialization failed: " + e.getMessage(), e);
2147            errorValue = EXIT_CODE_NOT_RUNNING;
2148        }
2149    }
2150
2151    /**
2152     * @since 8.3
2153     */
2154    protected void printXMLOutput(JAXBContext context, Object object, OutputStream out)
2155            throws XMLStreamException, FactoryConfigurationError, JAXBException {
2156        XMLStreamWriter writer = jsonOutput ? jsonWriter(context, out) : xmlWriter(context, out);
2157        Marshaller marshaller = context.createMarshaller();
2158        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
2159        marshaller.marshal(object, writer);
2160    }
2161
2162    protected XMLStreamWriter jsonWriter(JAXBContext context, OutputStream out) {
2163        JSONConfiguration config = JSONConfiguration.mapped()
2164                                                    .rootUnwrapping(true)
2165                                                    .attributeAsElement("key", "value")
2166                                                    .build();
2167        config = JSONConfiguration.createJSONConfigurationWithFormatted(config, true);
2168        return JsonXmlStreamWriter.createWriter(new OutputStreamWriter(out), config, "");
2169    }
2170
2171    protected XMLStreamWriter xmlWriter(JAXBContext context, OutputStream out)
2172            throws XMLStreamException, FactoryConfigurationError {
2173        return XMLOutputFactory.newInstance().createXMLStreamWriter(out);
2174    }
2175
2176    /**
2177     * Stop stream gobblers contained in the given ArrayList
2178     *
2179     * @since 5.5
2180     * @see #logProcessStreams(Process, boolean)
2181     */
2182    public void waitForProcessStreams(ArrayList<ThreadedStreamGobbler> sgArray) {
2183        for (ThreadedStreamGobbler streamGobbler : sgArray) {
2184            try {
2185                streamGobbler.join(STREAM_MAX_WAIT);
2186            } catch (InterruptedException e) {
2187                Thread.currentThread().interrupt();
2188                streamGobbler.interrupt();
2189                throw new RuntimeException(e);
2190            }
2191        }
2192    }
2193
2194    /**
2195     * @since 5.5
2196     * @return classpath with all jar files in baseDir
2197     */
2198    protected String getClassPath(String classpath, File baseDir) {
2199        File[] files = getFilename(baseDir, ".*");
2200        StringBuilder sb = new StringBuilder(classpath);
2201        for (File file : files) {
2202            sb.append(System.getProperty("path.separator")).append(file.getPath());
2203        }
2204        return sb.toString();
2205    }
2206
2207    /**
2208     * @since 5.5
2209     * @return filename matching filePattern in baseDir
2210     */
2211    protected File[] getFilename(File baseDir, final String filePattern) {
2212        return baseDir.listFiles((basedir, filename) -> filename.matches(filePattern + "(-[0-9].*)?\\.jar"));
2213    }
2214
2215    protected class ShutdownThread extends Thread {
2216
2217        private NuxeoLauncher launcher;
2218
2219        public ShutdownThread(NuxeoLauncher launcher) {
2220            super();
2221            this.launcher = launcher;
2222        }
2223
2224        @Override
2225        public void run() {
2226            log.debug("Shutting down...");
2227            if (launcher.isRunning()) {
2228                launcher.stop();
2229            }
2230            log.debug("Shutdown complete.");
2231        }
2232    }
2233
2234    protected void addShutdownHook() {
2235        log.debug("Add shutdown hook");
2236        shutdownHook = new ShutdownThread(this);
2237        Runtime.getRuntime().addShutdownHook(shutdownHook);
2238    }
2239
2240    /**
2241     * Stops the server. Will try to call specific class for a clean stop, retry, waiting between each try, then kill
2242     * the process if still running.
2243     */
2244    public void stop(boolean logProcessOutput) {
2245        long startTime = new Date().getTime();
2246        long deltaTime;
2247        try {
2248            if (!isRunning()) {
2249                log.warn("Server is not running.");
2250                return;
2251            }
2252            if (!quiet) {
2253                System.out.print("\nStopping server...");
2254            }
2255            int nbTry = 0;
2256            boolean retry = false;
2257            int stopMaxWait = Integer.parseInt(
2258                    configurationGenerator.getUserConfig().getProperty(STOP_MAX_WAIT_PARAM, STOP_MAX_WAIT_DEFAULT));
2259            do {
2260                List<String> stopCommand = new ArrayList<>();
2261                stopCommand.add(getJavaExecutable().getPath());
2262                stopCommand.add("-cp");
2263                stopCommand.add(getShutdownClassPath());
2264                stopCommand.addAll(getNuxeoProperties());
2265                stopCommand.addAll(getServerProperties());
2266                setServerStopCommand(stopCommand);
2267                stopCommand.addAll(Arrays.asList(params));
2268                ProcessBuilder pb = new ProcessBuilder(getOSCommand(stopCommand));
2269                pb.directory(configurationGenerator.getNuxeoHome());
2270                log.debug("Server command: " + pb.command());
2271                try {
2272                    Process stopProcess = pb.start();
2273                    ArrayList<ThreadedStreamGobbler> sgArray = logProcessStreams(stopProcess, logProcessOutput);
2274                    stopProcess.waitFor();
2275                    waitForProcessStreams(sgArray);
2276                    boolean wait = true;
2277                    while (wait) {
2278                        try {
2279                            if (stopProcess.exitValue() == 0) {
2280                                // Successful call for server stop
2281                                retry = false;
2282                            } else {
2283                                // Failed to call for server stop
2284                                retry = ++nbTry < STOP_NB_TRY;
2285                                if (!quiet) {
2286                                    System.out.print(".");
2287                                }
2288                                Thread.sleep(STOP_SECONDS_BEFORE_NEXT_TRY * 1000);
2289                            }
2290                            wait = false;
2291                        } catch (IllegalThreadStateException e) {
2292                            // Stop call is still running
2293                            wait = true;
2294                            if (!quiet) {
2295                                System.out.print(".");
2296                            }
2297                            Thread.sleep(1000);
2298                        }
2299                    }
2300                    // Exit if there's no way to check for server stop
2301                    if (!processManager.canFindPid()) {
2302                        log.warn("Can't check server status on your OS.");
2303                        return;
2304                    }
2305                    // Wait a few seconds for effective stop
2306                    do {
2307                        if (!quiet) {
2308                            System.out.print(".");
2309                        }
2310                        Thread.sleep(1000);
2311                        deltaTime = (new Date().getTime() - startTime) / 1000;
2312                    } while (!retry && getPid() != null && deltaTime < stopMaxWait);
2313                } catch (InterruptedException e) {
2314                    Thread.currentThread().interrupt();
2315                    throw new RuntimeException(e);
2316                }
2317            } while (retry);
2318            if (getPid() == null) {
2319                log.warn("Server stopped.");
2320            } else {
2321                log.info("No answer from server, try to kill process " + pid + "...");
2322                processManager.kill(nuxeoProcess, pid);
2323                if (getPid() == null) {
2324                    log.warn("Server forcibly stopped.");
2325                }
2326            }
2327        } catch (IOException e) {
2328            log.error("Could not manage process!", e);
2329        }
2330    }
2331
2332    protected abstract void setServerStopCommand(List<String> command);
2333
2334    private String getPid() throws IOException {
2335        pid = processManager.findPid(processRegex);
2336        log.debug("regexp: " + processRegex + " pid:" + pid);
2337        return pid;
2338    }
2339
2340    /**
2341     * Configure the server after checking installation
2342     *
2343     * @throws ConfigurationException If an installation error is detected or if configuration fails
2344     */
2345    public void configure() throws ConfigurationException {
2346        try {
2347            checkNoRunningServer();
2348            configurationGenerator.checkJavaVersion();
2349            configurationGenerator.run();
2350            overrideJavaTmpDir = Boolean.parseBoolean(
2351                    configurationGenerator.getUserConfig().getProperty(OVERRIDE_JAVA_TMPDIR_PARAM, "true"));
2352        } catch (ConfigurationException e) {
2353            errorValue = EXIT_CODE_NOT_CONFIGURED;
2354            throw e;
2355        }
2356    }
2357
2358    /**
2359     * @return Default max wait depending on server (ie JBoss takes much more time than Tomcat)
2360     */
2361    private String getDefaultMaxWait() {
2362        return START_MAX_WAIT_DEFAULT;
2363    }
2364
2365    /**
2366     * Return process status (running or not) as String, depending on OS capability to manage processes. Set status
2367     * value following "http://refspecs.freestandards.org/LSB_4.1.0/LSB-Core-generic/LSB-Core- generic/iniscrptact.html"
2368     *
2369     * @see #getStatus()
2370     */
2371    public String status() {
2372        try {
2373            if (processManager instanceof PureJavaProcessManager) {
2374                status = STATUS_CODE_UNKNOWN;
2375                return "Can't check server status on your OS.";
2376            }
2377            if (getPid() == null) {
2378                status = STATUS_CODE_OFF;
2379                return "Server is not running.";
2380            } else {
2381                status = STATUS_CODE_ON;
2382                return "Server is running with process ID " + getPid() + ".";
2383            }
2384        } catch (IOException e) {
2385            status = STATUS_CODE_UNKNOWN;
2386            return "Could not check existing process (" + e.getMessage() + ").";
2387        }
2388    }
2389
2390    /**
2391     * Last status value set by {@link #status()}.
2392     */
2393    public int getStatus() {
2394        return status;
2395    }
2396
2397    /**
2398     * Last error value set by any method. Exit code values are following the Linux Standard Base Core Specification
2399     * 4.1.
2400     */
2401    public int getErrorValue() {
2402        return errorValue;
2403    }
2404
2405    /**
2406     * @return a NuxeoLauncher instance specific to current server ( Tomcat or Jetty).
2407     * @throws ConfigurationException If server cannot be identified
2408     * @since 5.5
2409     */
2410    public static NuxeoLauncher createLauncher(String[] args)
2411            throws ConfigurationException, ParseException, IOException, PackageException {
2412        CommandLine cmdLine = parseOptions(args);
2413        ConfigurationGenerator cg = new ConfigurationGenerator(quiet, debug);
2414        if (cmdLine.hasOption(OPTION_HIDE_DEPRECATION)) {
2415            cg.hideDeprecationWarnings(true);
2416        }
2417        NuxeoLauncher launcher;
2418        if (cg.isJetty) {
2419            launcher = new NuxeoJettyLauncher(cg);
2420        } else if (cg.isTomcat) {
2421            launcher = new NuxeoTomcatLauncher(cg);
2422        } else {
2423            throw new ConfigurationException("Unknown server!");
2424        }
2425        launcher.connectBroker = new ConnectBroker(launcher.configurationGenerator.getEnv());
2426        launcher.setArgs(cmdLine);
2427        launcher.initConnectBroker();
2428        return launcher;
2429    }
2430
2431    /**
2432     * Sets from program arguments the launcher command and additional parameters.
2433     *
2434     * @param cmdLine Program arguments; may be used by launcher implementation. Must not be null or empty.
2435     */
2436    private void setArgs(CommandLine cmdLine) throws ConfigurationException {
2437        this.cmdLine = cmdLine;
2438        extractCommandAndParams(cmdLine.getArgs());
2439        // Use GUI?
2440        if (cmdLine.hasOption(OPTION_GUI)) {
2441            useGui = Boolean.parseBoolean(ConnectBroker.parseAnswer(cmdLine.getOptionValue(OPTION_GUI)));
2442            log.debug("GUI: " + cmdLine.getOptionValue(OPTION_GUI) + " -> " + useGui);
2443        } else if (OPTION_GUI.equalsIgnoreCase(command)) {
2444            useGui = true;
2445            // Shift params and extract command if there is one
2446            extractCommandAndParams(params);
2447        } else {
2448            if (SystemUtils.IS_OS_WINDOWS) {
2449                useGui = true;
2450                log.debug("GUI: option not set - platform is Windows -> start GUI");
2451            } else {
2452                useGui = false;
2453                log.debug("GUI: option not set - platform is not Windows -> do not start GUI");
2454            }
2455        }
2456        // Output format
2457        if (cmdLine.hasOption(OPTION_XML)) {
2458            setXMLOutput();
2459        }
2460        if (cmdLine.hasOption(OPTION_JSON)) {
2461            setJSONOutput();
2462        }
2463        if (cmdLine.hasOption(OPTION_CLID)) {
2464            try {
2465                getConnectBroker().setCLID(cmdLine.getOptionValue(OPTION_CLID));
2466            } catch (NoCLID e) {
2467                throw new ConfigurationException(e);
2468            }
2469        }
2470        if (cmdLine.hasOption(OPTION_IGNORE_MISSING)) {
2471            ignoreMissing = true;
2472        }
2473    }
2474
2475    private void extractCommandAndParams(String[] args) {
2476        if (args.length > 0) {
2477            command = args[0];
2478            log.debug("Launcher command: " + command);
2479            // Command parameters
2480            if (args.length > 1) {
2481                params = Arrays.copyOfRange(args, 1, args.length);
2482                if (log.isDebugEnabled()) {
2483                    log.debug("Command parameters: " + ArrayUtils.toString(params));
2484                }
2485            } else {
2486                params = new String[0];
2487            }
2488        } else {
2489            command = null;
2490        }
2491    }
2492
2493    /**
2494     * Set launcher in quiet mode
2495     *
2496     * @since 5.5
2497     */
2498    protected static void setQuiet() {
2499        quiet = true;
2500        Log4JHelper.setLevel(new String[] { ROOT_LOGGER_NAME }, Level.WARN, true);
2501    }
2502
2503    /**
2504     * @param loggerNames the loggers names to switch DEBUG on
2505     * @since 7.4
2506     */
2507    protected static void setDebug(String[] loggerNames) {
2508        debug = true;
2509        if (loggerNames == null) {
2510            loggerNames = new String[] { "org.nuxeo.launcher" };
2511        }
2512        Log4JHelper.setLevel(loggerNames, Level.DEBUG, true);
2513    }
2514
2515    /**
2516     * @param isStrict if {@code true}, set the launcher strict option
2517     * @since 7.4
2518     * @see #OPTION_STRICT_DESC
2519     */
2520    protected static void setStrict(boolean isStrict) {
2521        strict = isStrict;
2522    }
2523
2524    protected void setXMLOutput() {
2525        xmlOutput = true;
2526    }
2527
2528    protected void setJSONOutput() {
2529        jsonOutput = true;
2530        setXMLOutput();
2531    }
2532
2533    public static void printShortHelp() {
2534        System.out.println();
2535        HelpFormatter help = new HelpFormatter();
2536        help.setSyntaxPrefix("USAGE\n");
2537        help.setOptionComparator(null);
2538        help.setWidth(1000);
2539        help.printHelp(OPTION_HELP_USAGE, "OPTIONS", options, null);
2540        System.out.println(OPTION_HELP_DESC_COMMANDS);
2541    }
2542
2543    public static void printLongHelp() {
2544        System.out.println();
2545        HelpFormatter help = new HelpFormatter();
2546        help.setSyntaxPrefix("USAGE\n");
2547        help.setOptionComparator(null);
2548        help.setWidth(1000);
2549        help.printHelp(OPTION_HELP_USAGE, OPTION_HELP_HEADER, options, null);
2550        System.out.println(OPTION_HELP_DESC_ENV);
2551        System.out.println(OPTION_HELP_DESC_COMMANDS);
2552        System.out.println(OPTION_HELP_FOOTER);
2553    }
2554
2555    /**
2556     * Work best with current nuxeoProcess. If nuxeoProcess is null or has exited, then will try to get process ID (so,
2557     * result in that case depends on OS capabilities).
2558     *
2559     * @return true if current process is running or if a running PID is found
2560     */
2561    public boolean isRunning() {
2562        if (nuxeoProcess != null) {
2563            try {
2564                nuxeoProcess.exitValue();
2565                // Previous process has exited
2566                nuxeoProcess = null;
2567            } catch (IllegalThreadStateException exception) {
2568                return true;
2569            }
2570        }
2571        try {
2572            return (getPid() != null);
2573        } catch (IOException e) {
2574            log.error(e);
2575            return false;
2576        }
2577    }
2578
2579    /**
2580     * Provides this instance info
2581     *
2582     * @since 8.3
2583     */
2584    public InstanceInfo getInfo() {
2585        return info;
2586    }
2587
2588    /**
2589     * @since 5.5
2590     * @return true if Nuxeo finished starting
2591     */
2592    public boolean isStarted() {
2593        boolean isStarted;
2594        if (configurationGenerator.isWizardRequired()) {
2595            isStarted = isRunning();
2596        } else {
2597            try {
2598                isStarted = isRunning() && statusServletClient.isStarted();
2599            } catch (SocketTimeoutException e) {
2600                isStarted = false;
2601            }
2602        }
2603        return isStarted;
2604    }
2605
2606    /**
2607     * @return Server log file
2608     */
2609    public File getLogFile() {
2610        return new File(configurationGenerator.getLogDir(), "server.log");
2611    }
2612
2613    /**
2614     * @return Server URL
2615     */
2616    public String getURL() {
2617        return configurationGenerator.getUserConfig().getProperty(ConfigurationGenerator.PARAM_NUXEO_URL);
2618    }
2619
2620    protected void initConnectBroker() {
2621        if (cmdLine.hasOption(OPTION_ACCEPT)) {
2622            connectBroker.setAccept(cmdLine.getOptionValue(OPTION_ACCEPT));
2623        }
2624        if (cmdLine.hasOption(OPTION_RELAX)) {
2625            connectBroker.setRelax(cmdLine.getOptionValue(OPTION_RELAX));
2626        }
2627        if (cmdLine.hasOption(OPTION_SNAPSHOT)) {
2628            connectBroker.setAllowSNAPSHOT(true);
2629        }
2630        List<CommandInfo> csetCommands = cset.commands;
2631        cset = connectBroker.getCommandSet();
2632        cset.commands.addAll(0, csetCommands);
2633        try {
2634            clid = connectBroker.getCLID();
2635        } catch (NoCLID cause) {
2636            ;
2637        }
2638        info = configurationGenerator.getServerConfigurator().getInfo(clid, connectBroker.getPkgList());
2639        if (new Version(info.distribution.version).isSnapshot()) {
2640            connectBroker.setAllowSNAPSHOT(true);
2641        }
2642        connectBroker.setPendingFile(configurationGenerator.getInstallFile().toPath());
2643    }
2644
2645    protected ConnectBroker getConnectBroker() {
2646        return connectBroker;
2647    }
2648
2649    protected ConnectRegistrationBroker getConnectRegistrationBroker() {
2650        if (connectRegistrationBroker == null) {
2651            getConnectBroker(); // Ensure ConnectBroker is instantiated too.
2652            connectRegistrationBroker = new ConnectRegistrationBroker();
2653        }
2654        return connectRegistrationBroker;
2655    }
2656
2657    /**
2658     * List all local packages.
2659     */
2660    protected void pkgList() {
2661        getConnectBroker().listPending(configurationGenerator.getInstallFile());
2662        getConnectBroker().pkgList();
2663    }
2664
2665    /**
2666     * List all packages including remote ones.
2667     *
2668     * @since 5.6
2669     */
2670    protected void pkgListAll() {
2671        getConnectBroker().listPending(configurationGenerator.getInstallFile());
2672        getConnectBroker().pkgListAll();
2673    }
2674
2675    protected boolean pkgAdd(String[] pkgNames) {
2676        boolean cmdOK = getConnectBroker().pkgAdd(Arrays.asList(pkgNames), ignoreMissing);
2677        if (!cmdOK) {
2678            errorValue = EXIT_CODE_ERROR;
2679        }
2680        return cmdOK;
2681    }
2682
2683    protected boolean pkgInstall(String[] pkgIDs) {
2684        boolean cmdOK = true;
2685        if (configurationGenerator.isInstallInProgress()) {
2686            cmdOK = getConnectBroker().executePending(configurationGenerator.getInstallFile(), true,
2687                    !cmdLine.hasOption(OPTION_NODEPS), ignoreMissing);
2688        }
2689        cmdOK = cmdOK && getConnectBroker().pkgInstall(Arrays.asList(pkgIDs), ignoreMissing);
2690        if (!cmdOK) {
2691            errorValue = EXIT_CODE_ERROR;
2692        }
2693        return cmdOK;
2694    }
2695
2696    protected boolean pkgUninstall(String[] pkgIDs) {
2697        boolean cmdOK = getConnectBroker().pkgUninstall(Arrays.asList(pkgIDs));
2698        if (!cmdOK) {
2699            errorValue = EXIT_CODE_ERROR;
2700        }
2701        return cmdOK;
2702    }
2703
2704    protected boolean pkgRemove(String[] pkgIDs) {
2705        boolean cmdOK = getConnectBroker().pkgRemove(Arrays.asList(pkgIDs));
2706        if (!cmdOK) {
2707            errorValue = EXIT_CODE_ERROR;
2708        }
2709        return cmdOK;
2710    }
2711
2712    protected boolean pkgReset() {
2713        boolean cmdOK = getConnectBroker().pkgReset();
2714        if (!cmdOK) {
2715            errorValue = EXIT_CODE_ERROR;
2716        }
2717        return cmdOK;
2718    }
2719
2720    /**
2721     * @since 5.6
2722     */
2723    protected void printInstanceXMLOutput(InstanceInfo instance) {
2724        try {
2725            printInstanceXMLOutput(instance, System.out);
2726        } catch (JAXBException | XMLStreamException | FactoryConfigurationError e) {
2727            log.error("Output serialization failed: " + e.getMessage());
2728            log.debug(e, e);
2729            errorValue = EXIT_CODE_NOT_RUNNING;
2730        }
2731    }
2732
2733    protected void printInstanceXMLOutput(InstanceInfo instance, OutputStream out)
2734            throws JAXBException, XMLStreamException, FactoryConfigurationError {
2735        JAXBContext jaxbContext = JAXBContext.newInstance(InstanceInfo.class, DistributionInfo.class, PackageInfo.class,
2736                ConfigurationInfo.class, KeyValueInfo.class);
2737        printXMLOutput(jaxbContext, instance, out);
2738    }
2739
2740    /**
2741     * @since 5.6
2742     */
2743    protected void showConfig() {
2744        log.info("***** Nuxeo instance configuration *****");
2745        log.info("NUXEO_CONF: " + info.NUXEO_CONF);
2746        log.info("NUXEO_HOME: " + info.NUXEO_HOME);
2747        if (info.clid != null) {
2748            log.info("Instance CLID: " + info.clid);
2749        }
2750        // distribution.properties
2751        log.info("** Distribution");
2752        log.info("- name: " + info.distribution.name);
2753        log.info("- server: " + info.distribution.server);
2754        log.info("- version: " + info.distribution.version);
2755        log.info("- date: " + info.distribution.date);
2756        log.info("- packaging: " + info.distribution.packaging);
2757        // packages
2758        log.info("** Packages:");
2759        for (PackageInfo pkg : info.packages) {
2760            log.info(String.format("- %s (version: %s - id: %s - state: %s)", pkg.name, pkg.version, pkg.id,
2761                    pkg.state.getLabel()));
2762        }
2763        // nuxeo.conf
2764        log.info("** Templates:");
2765        log.info("Database template: " + info.config.dbtemplate);
2766        for (String template : info.config.pkgtemplates) {
2767            log.info("Package template: " + template);
2768        }
2769        for (String template : info.config.usertemplates) {
2770            log.info("User template: " + template);
2771        }
2772        for (String template : info.config.basetemplates) {
2773            log.info("Base template: " + template);
2774        }
2775        log.info("** Settings from nuxeo.conf:");
2776        for (KeyValueInfo keyval : info.config.keyvals) {
2777            log.info(String.format("%s=%s", keyval.key, keyval.value));
2778        }
2779        log.info("****************************************");
2780        if (xmlOutput) {
2781            printInstanceXMLOutput(info);
2782        }
2783    }
2784
2785    /**
2786     * @since 5.6
2787     * @return true if request execution was fine
2788     */
2789    protected boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
2790            List<String> pkgsToRemove) {
2791        boolean cmdOK = true;
2792        if (configurationGenerator.isInstallInProgress()) {
2793            cmdOK = getConnectBroker().executePending(configurationGenerator.getInstallFile(), true, true,
2794                    ignoreMissing);
2795        }
2796        cmdOK = cmdOK && getConnectBroker().pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, true,
2797                ignoreMissing);
2798        if (!cmdOK) {
2799            errorValue = EXIT_CODE_ERROR;
2800        }
2801        return cmdOK;
2802    }
2803
2804    /**
2805     * Update the cached list of remote packages
2806     *
2807     * @since 5.6
2808     * @return true
2809     */
2810    protected boolean pkgRefreshCache() {
2811        getConnectBroker().refreshCache();
2812        return true;
2813    }
2814
2815    /**
2816     * Add packages from the distribution to the local cache
2817     *
2818     * @since 5.6
2819     */
2820    protected boolean pkgInit() {
2821        return getConnectBroker().addDistributionPackages();
2822    }
2823
2824    /**
2825     * Uninstall and remove all packages from the local cache
2826     *
2827     * @return {@code true} if command succeed
2828     * @since 5.6
2829     */
2830    protected boolean pkgPurge() throws PackageException {
2831        return getConnectBroker().pkgPurge();
2832    }
2833
2834    /**
2835     * Install the hotfixes available for the instance
2836     *
2837     * @return {@code true} if command succeed
2838     * @since 5.6
2839     */
2840    protected boolean pkgHotfix() {
2841        return getConnectBroker().pkgHotfix();
2842    }
2843
2844    /**
2845     * Upgrade the Nuxeo Packages (addons) available for the instance
2846     *
2847     * @return {@code true} if command succeed
2848     * @since 5.6
2849     */
2850    protected boolean pkgUpgrade() {
2851        return getConnectBroker().pkgUpgrade();
2852    }
2853
2854    /**
2855     * Combined install/uninstall request
2856     *
2857     * @param request Space separated list of package names or IDs prefixed with + (install) or - (uninstall)
2858     * @since 5.6
2859     */
2860    protected boolean pkgCompoundRequest(List<String> request) {
2861        List<String> add = new ArrayList<>();
2862        List<String> install = new ArrayList<>();
2863        List<String> uninstall = new ArrayList<>();
2864        for (String param : request) {
2865            for (String subparam : param.split("[ ,]")) {
2866                if (subparam.charAt(0) == '-') {
2867                    uninstall.add(subparam.substring(1));
2868                } else if (subparam.charAt(0) == '+') {
2869                    install.add(subparam.substring(1));
2870                } else {
2871                    add.add(subparam);
2872                }
2873            }
2874        }
2875        return pkgRequest(add, install, uninstall, null);
2876    }
2877
2878    protected boolean pkgSetRequest(List<String> request, boolean nodeps) {
2879        boolean cmdOK;
2880        if (nodeps) {
2881            cmdOK = getConnectBroker().pkgSet(request, ignoreMissing);
2882        } else {
2883            cmdOK = getConnectBroker().pkgRequest(null, request, null, null, false, ignoreMissing);
2884        }
2885        if (!cmdOK) {
2886            errorValue = EXIT_CODE_ERROR;
2887        }
2888        return cmdOK;
2889    }
2890
2891    /**
2892     * dpkg-like command which returns package location, version, dependencies, conflicts, ...
2893     *
2894     * @param packages List of packages identified by their ID, name or local filename.
2895     * @return false if unable to show package information.
2896     * @since 5.7
2897     */
2898    protected boolean pkgShow(String[] packages) {
2899        boolean cmdOK = getConnectBroker().pkgShow(Arrays.asList(packages));
2900        if (!cmdOK) {
2901            errorValue = EXIT_CODE_ERROR;
2902        }
2903        return cmdOK;
2904    }
2905
2906    protected boolean dumpConnectReport(OutputStream out, boolean prettyPrint) {
2907        try (JsonGenerator generator = Json.createGeneratorFactory(
2908                Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, prettyPrint)).createGenerator(out)) {
2909            generator.writeStartObject();
2910            ReportConnector.of().feed(generator);
2911            generator.writeEnd();
2912        } catch (InterruptedException e) {
2913            Thread.currentThread().interrupt();
2914            throw new RuntimeException(e);
2915        } catch (IOException | ExecutionException cause) {
2916            log.error("Cannot dump connect report", cause);
2917            errorValue = EXIT_CODE_ERROR;
2918            return false;
2919        }
2920        return true;
2921    }
2922}