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