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