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