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