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