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