001/*
002 * (C) Copyright 2012-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Julien Carsique
018 *     Mathieu Guillaume
019 *     Yannis JULIENNE
020 *
021 */
022
023package org.nuxeo.launcher.connect;
024
025import java.io.Console;
026import java.io.File;
027import java.io.IOException;
028import java.nio.file.Files;
029import java.nio.file.Path;
030import java.nio.file.StandardCopyOption;
031import java.nio.file.StandardOpenOption;
032import java.util.ArrayList;
033import java.util.Collections;
034import java.util.HashSet;
035import java.util.LinkedList;
036import java.util.List;
037import java.util.Map;
038import java.util.Queue;
039import java.util.Set;
040import java.util.SortedMap;
041import java.util.TreeMap;
042import java.util.stream.Collectors;
043
044import org.apache.commons.collections.CollectionUtils;
045import org.apache.commons.io.FileUtils;
046import org.apache.commons.lang3.ArrayUtils;
047import org.apache.commons.lang3.StringUtils;
048import org.apache.commons.logging.impl.SimpleLog;
049import org.apache.logging.log4j.LogManager;
050import org.apache.logging.log4j.Logger;
051import org.nuxeo.common.Environment;
052import org.nuxeo.connect.CallbackHolder;
053import org.nuxeo.connect.NuxeoConnectClient;
054import org.nuxeo.connect.connector.ConnectServerError;
055import org.nuxeo.connect.data.DownloadablePackage;
056import org.nuxeo.connect.data.DownloadingPackage;
057import org.nuxeo.connect.identity.LogicalInstanceIdentifier;
058import org.nuxeo.connect.identity.LogicalInstanceIdentifier.InvalidCLID;
059import org.nuxeo.connect.identity.LogicalInstanceIdentifier.NoCLID;
060import org.nuxeo.connect.packages.LocalPackageAsDownloadablePackage;
061import org.nuxeo.connect.packages.PackageManager;
062import org.nuxeo.connect.packages.dependencies.CUDFHelper;
063import org.nuxeo.connect.packages.dependencies.DependencyResolution;
064import org.nuxeo.connect.packages.dependencies.TargetPlatformFilterHelper;
065import org.nuxeo.connect.platform.PlatformId;
066import org.nuxeo.connect.update.LocalPackage;
067import org.nuxeo.connect.update.Package;
068import org.nuxeo.connect.update.PackageException;
069import org.nuxeo.connect.update.PackageState;
070import org.nuxeo.connect.update.PackageType;
071import org.nuxeo.connect.update.PackageUtils;
072import org.nuxeo.connect.update.ValidationStatus;
073import org.nuxeo.connect.update.Version;
074import org.nuxeo.connect.update.model.PackageDefinition;
075import org.nuxeo.connect.update.standalone.StandaloneUpdateService;
076import org.nuxeo.connect.update.task.Task;
077import org.nuxeo.launcher.info.CommandInfo;
078import org.nuxeo.launcher.info.CommandSetInfo;
079import org.nuxeo.launcher.info.PackageInfo;
080
081/**
082 * @since 5.6
083 */
084public class ConnectBroker {
085
086    private static final Logger log = LogManager.getLogger(ConnectBroker.class);
087
088    public static final String[] POSITIVE_ANSWERS = { "true", "yes", "y" };
089
090    protected static final String LAUNCHER_CHANGED_PROPERTY = "launcher.changed";
091
092    private Environment env;
093
094    private StandaloneUpdateService service;
095
096    private CallbackHolder cbHolder;
097
098    private CommandSetInfo cset = new CommandSetInfo();
099
100    private PlatformId targetPlatform;
101
102    private String relax = OPTION_RELAX_DEFAULT;
103
104    public static final String OPTION_RELAX_DEFAULT = "ask";
105
106    private String accept = OPTION_ACCEPT_DEFAULT;
107
108    private boolean allowSNAPSHOT = CUDFHelper.defaultAllowSNAPSHOT;
109
110    private Path pendingFile;
111
112    public static final String OPTION_ACCEPT_DEFAULT = "ask";
113
114    public ConnectBroker(Environment env) throws IOException, PackageException {
115        this.env = env;
116        service = new StandaloneUpdateService(env);
117        service.initialize();
118        cbHolder = new StandaloneCallbackHolder(env, service);
119        NuxeoConnectClient.setCallBackHolder(cbHolder);
120        targetPlatform = PlatformId.parse(env.getProperty(Environment.DISTRIBUTION_NAME) + "-"
121                + env.getProperty(Environment.DISTRIBUTION_VERSION));
122    }
123
124    /**
125     * @since 10.2
126     */
127    public Path getPendingFile() {
128        return pendingFile;
129    }
130
131    /**
132     * @since 10.2
133     */
134    public void setPendingFile(Path pendingFile) {
135        this.pendingFile = pendingFile;
136    }
137
138    public String getCLID() throws NoCLID {
139        return LogicalInstanceIdentifier.instance().getCLID();
140    }
141
142    /**
143     * @throws NoCLID if the CLID is absent or invalid
144     * @since 6.0
145     */
146    public void setCLID(String file) throws NoCLID {
147        try {
148            LogicalInstanceIdentifier.load(file);
149        } catch (IOException | InvalidCLID e) {
150            throw new NoCLID("can not load CLID", e);
151        }
152    }
153
154    /**
155     * @since 8.10-HF15
156     */
157    public void saveCLID() throws IOException, NoCLID {
158        LogicalInstanceIdentifier.instance().save();
159    }
160
161    public StandaloneUpdateService getUpdateService() {
162        return service;
163    }
164
165    public PackageManager getPackageManager() {
166        return NuxeoConnectClient.getPackageManager(targetPlatform);
167    }
168
169    public void refreshCache() {
170        getPackageManager().flushCache();
171        getPackageManager().listAllPackages();
172    }
173
174    public CommandSetInfo getCommandSet() {
175        return cset;
176    }
177
178    protected LocalPackage getInstalledPackageByName(String pkgName) {
179        try {
180            return service.getPersistence().getActivePackage(pkgName);
181        } catch (PackageException e) {
182            log.error(e);
183            return null;
184        }
185    }
186
187    protected boolean isInstalledPackage(String pkgName) {
188        try {
189            return service.getPersistence().getActivePackageId(pkgName) != null;
190        } catch (PackageException e) {
191            log.error("Error checking installation of package {}", pkgName, e);
192            return false;
193        }
194    }
195
196    protected boolean isLocalPackageId(String pkgId) {
197        try {
198            return service.getPackage(pkgId) != null;
199        } catch (PackageException e) {
200            log.error("Error looking for local package {}", pkgId, e);
201            return false;
202        }
203    }
204
205    protected boolean isRemotePackageId(String pkgId) {
206        return PackageUtils.isValidPackageId(pkgId) && getPackageManager().getRemotePackage(pkgId) != null;
207    }
208
209    protected String getBestIdForNameInList(String pkgName, List<? extends Package> pkgList) {
210        String foundId = null;
211        SortedMap<Version, String> foundPkgs = new TreeMap<>();
212        SortedMap<Version, String> matchingPkgs = new TreeMap<>();
213        for (Package pkg : pkgList) {
214            if (pkg.getName().equals(pkgName)) {
215                foundPkgs.put(pkg.getVersion(), pkg.getId());
216                if (TargetPlatformFilterHelper.isCompatibleWithTargetPlatform(pkg, targetPlatform)) {
217                    matchingPkgs.put(pkg.getVersion(), pkg.getId());
218                }
219            }
220        }
221        if (matchingPkgs.size() != 0) {
222            foundId = matchingPkgs.get(matchingPkgs.lastKey());
223        } else if (foundPkgs.size() != 0) {
224            foundId = foundPkgs.get(foundPkgs.lastKey());
225        }
226        return foundId;
227    }
228
229    protected String getLocalPackageIdFromName(String pkgName) {
230        return getBestIdForNameInList(pkgName, getPkgList());
231    }
232
233    protected List<String> getAllLocalPackageIdsFromName(String pkgName) {
234        List<String> foundIds = new ArrayList<>();
235        for (Package pkg : getPkgList()) {
236            if (pkg.getName().equals(pkgName)) {
237                foundIds.add(pkg.getId());
238            }
239        }
240        return foundIds;
241    }
242
243    protected String getInstalledPackageIdFromName(String pkgName) {
244        List<LocalPackage> localPackages = getPkgList();
245        List<LocalPackage> installedPackages = new ArrayList<>();
246        for (LocalPackage pkg : localPackages) {
247            if (pkg.getPackageState().isInstalled()) {
248                installedPackages.add(pkg);
249            }
250        }
251        return getBestIdForNameInList(pkgName, installedPackages);
252    }
253
254    protected String getRemotePackageIdFromName(String pkgName) {
255        return getBestIdForNameInList(pkgName, getPackageManager().findRemotePackages(pkgName));
256    }
257
258    /**
259     * Looks for a remote package from its name or id
260     *
261     * @return the remote package Id; null if not found
262     * @since 5.7
263     */
264    protected String getRemotePackageId(String pkgNameOrId) {
265        String pkgId;
266        if (isRemotePackageId(pkgNameOrId)) {
267            pkgId = pkgNameOrId;
268        } else {
269            pkgId = getRemotePackageIdFromName(pkgNameOrId);
270        }
271        return pkgId;
272    }
273
274    /**
275     * Looks for a local package from its name or id
276     *
277     * @since 5.7
278     * @return the local package Id; null if not found
279     */
280    protected LocalPackage getLocalPackage(String pkgIdOrName) throws PackageException {
281        // Try as a package id
282        LocalPackage pkg = service.getPackage(pkgIdOrName);
283        if (pkg == null) {
284            // Check whether this is the name of a local package
285            String pkgId = getLocalPackageIdFromName(pkgIdOrName);
286            if (pkgId != null) {
287                pkg = service.getPackage(pkgId);
288            }
289        }
290        return pkg;
291    }
292
293    /**
294     * Looks for a package file from its path
295     *
296     * @param pkgFile Absolute or relative package file path
297     * @return the file if found, else null
298     */
299    protected File getLocalPackageFile(String pkgFile) {
300        if (pkgFile.startsWith("file:")) {
301            pkgFile = pkgFile.substring(5);
302        }
303        // Try absolute path
304        File fileToCheck = new File(pkgFile);
305        if (!fileToCheck.exists()) { // Try relative path
306            fileToCheck = new File(env.getServerHome(), pkgFile);
307        }
308        if (fileToCheck.exists()) {
309            return fileToCheck;
310        } else {
311            return null;
312        }
313    }
314
315    /**
316     * Load package definition from a local file or directory and get package Id from it.
317     *
318     * @return null the package definition cannot be loaded for any reason.
319     * @since 8.4
320     */
321    protected String getLocalPackageFileId(File pkgFile) {
322        PackageDefinition packageDefinition;
323        try {
324            if (pkgFile.isFile()) {
325                packageDefinition = service.loadPackageFromZip(pkgFile);
326            } else if (pkgFile.isDirectory()) {
327                File manifest = new File(pkgFile, LocalPackage.MANIFEST);
328                packageDefinition = service.loadPackage(manifest);
329            } else {
330                throw new PackageException("Unknown file type (not a file and not a directory) for " + pkgFile);
331            }
332        } catch (PackageException e) {
333            log.error("Error trying to load package id from {}", pkgFile, e);
334            return null;
335        }
336        return packageDefinition == null ? null : packageDefinition.getId();
337    }
338
339    protected boolean isLocalPackageFile(String pkgFile) {
340        return (getLocalPackageFile(pkgFile) != null);
341    }
342
343    public List<LocalPackage> getPkgList() {
344        try {
345            return service.getPackages();
346        } catch (PackageException e) {
347            log.error("Could not read package list", e);
348            return null;
349        }
350    }
351
352    public void pkgList() {
353        log.info("Local packages:");
354        pkgList(getPkgList().stream().map(LocalPackageAsDownloadablePackage::new).collect(Collectors.toList()));
355    }
356
357    public void pkgListAll() {
358        if (Boolean.parseBoolean(relax)) {
359            log.info("All packages (all platforms):");
360            pkgList(getPackageManager().getAllPackages(getPackageManager().getAllSources(), null, null));
361        } else {
362            log.info("All packages:");
363            pkgList(getPackageManager().listAllPackages());
364        }
365    }
366
367    private String getPrivateOrSubscriptionTag(DownloadablePackage pkg) {
368        String tag = "";
369        if (pkg.getOwner() != null) {
370            tag = "[PRIVATE (Owner: " + pkg.getOwner() + ")]";
371        } else if (pkg.getPackageState() == PackageState.REMOTE //
372                && pkg.getType() != PackageType.STUDIO //
373                && pkg.hasSubscriptionRequired() //
374                && !LogicalInstanceIdentifier.isRegistered()) {
375            tag = "[REGISTRATION REQUIRED]";
376        }
377        return tag;
378    }
379
380    public void pkgList(List<? extends DownloadablePackage> packagesList) {
381        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_LIST);
382        try {
383            if (packagesList.isEmpty()) {
384                log.info("None");
385            } else {
386                getPackageManager().sort(packagesList);
387                StringBuilder sb = new StringBuilder();
388                for (DownloadablePackage pkg : packagesList) {
389                    newPackageInfo(cmdInfo, pkg);
390                    String packageDescription = String.format("%6s %11s\t%s (id: %s) %s\n", pkg.getType(),
391                            pkg.getPackageState().getLabel(), pkg.getName(), pkg.getId(),
392                            getPrivateOrSubscriptionTag(pkg));
393                    sb.append(packageDescription);
394                }
395                log.info(sb::toString);
396            }
397        } catch (Exception e) {
398            log.error(e);
399            cmdInfo.exitCode = 1;
400        }
401    }
402
403    protected void performTask(Task task) throws PackageException {
404        ValidationStatus validationStatus = task.validate();
405        if (validationStatus.hasErrors()) {
406            throw new PackageException(
407                    "Failed to validate package " + task.getPackage().getId() + " -> " + validationStatus.getErrors());
408        }
409        if (validationStatus.hasWarnings()) {
410            log.warn("Got warnings on package validation {} -> {}", () -> task.getPackage().getId(),
411                    validationStatus::getWarnings);
412        }
413        task.run(null);
414    }
415
416    public boolean pkgReset() {
417        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_RESET);
418        if ("ask".equalsIgnoreCase(accept)) {
419            accept = readConsole(
420                    "The reset will erase the Nuxeo Packages history.\n" + "Do you want to continue (yes/no)? [yes] ",
421                    "yes");
422        }
423        if (!Boolean.parseBoolean(accept)) {
424            cmdInfo.exitCode = 1;
425            return false;
426        }
427        try {
428            service.reset();
429            log.info("Packages reset done: all packages were marked as DOWNLOADED");
430            List<LocalPackage> localPackages = service.getPackages();
431            for (LocalPackage localPackage : localPackages) {
432                localPackage.getUninstallFile().delete();
433                FileUtils.deleteDirectory(localPackage.getData().getEntry(LocalPackage.BACKUP_DIR));
434                newPackageInfo(cmdInfo, localPackage);
435            }
436            service.getRegistry().delete();
437            FileUtils.deleteDirectory(service.getBackupDir());
438        } catch (PackageException | IOException e) {
439            log.error(e);
440            cmdInfo.exitCode = 1;
441        }
442        return cmdInfo.exitCode == 0;
443    }
444
445    public boolean pkgPurge() throws PackageException {
446        List<String> localNames = new ArrayList<>();
447        // Remove packages in DOWNLOADED state first
448        // This will avoid extending the CUDF universe needlessly
449        for (LocalPackage pkg : service.getPackages()) {
450            if (pkg.getPackageState() == PackageState.DOWNLOADED) {
451                pkgRemove(pkg.getId());
452            }
453        }
454        // Process the remaining packages
455        for (LocalPackage pkg : service.getPackages()) {
456            localNames.add(pkg.getName());
457        }
458        return pkgRequest(null, null, null, localNames, true, false);
459    }
460
461    /**
462     * Uninstall a list of packages. If the list contains a package name (versus an ID), only the considered as best
463     * matching package is uninstalled.
464     *
465     * @param packageIdsToRemove The list can contain package IDs and names
466     * @see #pkgUninstall(String)
467     */
468    public boolean pkgUninstall(List<String> packageIdsToRemove) {
469        log.debug("Uninstalling: {}", packageIdsToRemove);
470        Queue<String> remaining = new LinkedList<>(packageIdsToRemove);
471        while (!remaining.isEmpty()) {
472            String pkgId = remaining.poll();
473            if (pkgUninstall(pkgId) == null) {
474                log.error("Unable to uninstall {}", pkgId);
475                return false;
476            }
477            if (isRestartRequired()) {
478                remaining.forEach(pkg -> persistCommand(CommandInfo.CMD_UNINSTALL + " " + pkg));
479                throw new LauncherRestartException();
480            }
481        }
482        return true;
483    }
484
485    /**
486     * Uninstall a local package. The package is not removed from cache.
487     *
488     * @param pkgId Package ID or Name
489     * @return The uninstalled LocalPackage or null if failed
490     */
491    public LocalPackage pkgUninstall(String pkgId) {
492        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_UNINSTALL);
493        cmdInfo.param = pkgId;
494        try {
495            LocalPackage pkg = service.getPackage(pkgId);
496            if (pkg == null) {
497                // Check whether this is the name of an installed package
498                String realPkgId = getInstalledPackageIdFromName(pkgId);
499                if (realPkgId != null) {
500                    pkgId = realPkgId;
501                    pkg = service.getPackage(realPkgId);
502                }
503            }
504            if (pkg == null) {
505                throw new PackageException("Package not found: " + pkgId);
506            }
507            log.info("Uninstalling {}", pkgId);
508            Task uninstallTask = pkg.getUninstallTask();
509            try {
510                performTask(uninstallTask);
511            } catch (PackageException e) {
512                uninstallTask.rollback();
513                throw e;
514            }
515            // Refresh state
516            pkg = service.getPackage(pkgId);
517            newPackageInfo(cmdInfo, pkg);
518            return pkg;
519        } catch (Exception e) {
520            log.error("Failed to uninstall package: {}", pkgId, e);
521            cmdInfo.exitCode = 1;
522            return null;
523        }
524    }
525
526    /**
527     * Remove a list of packages from cache. If the list contains a package name (versus an ID), all matching packages
528     * are removed.
529     *
530     * @param pkgsToRemove The list can contain package IDs and names
531     * @see #pkgRemove(String)
532     */
533    public boolean pkgRemove(List<String> pkgsToRemove) {
534        boolean cmdOk = true;
535        if (pkgsToRemove != null) {
536            log.debug("Removing: {}", pkgsToRemove);
537            for (String pkgNameOrId : pkgsToRemove) {
538                List<String> allIds;
539                if (isLocalPackageId(pkgNameOrId)) {
540                    allIds = new ArrayList<>();
541                    allIds.add(pkgNameOrId);
542                } else {
543                    // Request made on a name: remove all matching packages
544                    allIds = getAllLocalPackageIdsFromName(pkgNameOrId);
545                }
546                for (String pkgId : allIds) {
547                    if (pkgRemove(pkgId) == null) {
548                        log.warn("Unable to remove {}", pkgId);
549                        // Don't error out on failed (cache) removal
550                        cmdOk = false;
551                    }
552                }
553            }
554        }
555        return cmdOk;
556    }
557
558    /**
559     * Remove a package from cache. If it was installed, the package is uninstalled then removed.
560     *
561     * @param pkgId Package ID or Name
562     * @return The removed LocalPackage or null if failed
563     */
564    public LocalPackage pkgRemove(String pkgId) {
565        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_REMOVE);
566        cmdInfo.param = pkgId;
567        try {
568            LocalPackage pkg = service.getPackage(pkgId);
569            if (pkg == null) {
570                // Check whether this is the name of a local package
571                String realPkgId = getLocalPackageIdFromName(pkgId);
572                if (realPkgId != null) {
573                    pkgId = realPkgId;
574                    pkg = service.getPackage(realPkgId);
575                }
576            }
577            if (pkg == null) {
578                throw new PackageException("Package not found: " + pkgId);
579            }
580            if (pkg.getPackageState().isInstalled()) {
581                pkgUninstall(pkgId);
582                // Refresh state
583                pkg = service.getPackage(pkgId);
584            }
585            if (pkg.getPackageState() != PackageState.DOWNLOADED) {
586                throw new PackageException("Can only remove packages in DOWNLOADED, INSTALLED or STARTED state");
587            }
588            service.removePackage(pkgId);
589            log.info("Removed {}", pkgId);
590            newPackageInfo(cmdInfo, pkg).state = PackageState.REMOTE;
591            return pkg;
592        } catch (Exception e) {
593            log.error("Failed to remove package: {}", pkgId, e);
594            cmdInfo.exitCode = 1;
595            return null;
596        }
597    }
598
599    /**
600     * Add a list of packages into the cache, downloading them if needed and possible.
601     *
602     * @since 6.0
603     * @return true if command succeeded
604     * @see #pkgAdd(String, boolean)
605     */
606    public boolean pkgAdd(List<String> pkgsToAdd, boolean ignoreMissing) {
607        boolean cmdOk = true;
608        if (pkgsToAdd == null || pkgsToAdd.isEmpty()) {
609            return cmdOk;
610        }
611        List<String> pkgIdsToDownload = new ArrayList<>();
612        for (String pkgToAdd : pkgsToAdd) {
613            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
614            cmdInfo.param = pkgToAdd;
615            try {
616                File fileToAdd = getLocalPackageFile(pkgToAdd);
617                if (fileToAdd == null) {
618                    String pkgId = getRemotePackageId(pkgToAdd);
619                    if (pkgId == null) {
620                        if (ignoreMissing) {
621                            log.warn("Could not add package: {}", pkgToAdd);
622                            cmdInfo.newMessage(SimpleLog.LOG_LEVEL_INFO, "Could not add package.");
623                        } else {
624                            throw new PackageException("Could not find a remote or local (relative to "
625                                    + "current directory or to NUXEO_HOME) " + "package with name or ID " + pkgToAdd);
626                        }
627                    } else {
628                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_INFO, "Waiting for download...");
629                        pkgIdsToDownload.add(pkgId);
630                    }
631                } else {
632                    LocalPackage pkg = service.addPackage(fileToAdd);
633                    log.info("Added {}", pkg);
634                    newPackageInfo(cmdInfo, pkg);
635                }
636            } catch (PackageException e) {
637                cmdOk = false;
638                cmdInfo.exitCode = 1;
639                cmdInfo.newMessage(e);
640            }
641        }
642        cmdOk = downloadPackages(pkgIdsToDownload) && cmdOk;
643        return cmdOk;
644    }
645
646    /**
647     * Add a package file into the cache
648     *
649     * @since 6.0
650     * @return The added LocalPackage or null if failed
651     * @see #pkgAdd(List, boolean)
652     */
653    public LocalPackage pkgAdd(String packageFileName, boolean ignoreMissing) {
654        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
655        cmdInfo.param = packageFileName;
656        LocalPackage pkg = null;
657        try {
658            File fileToAdd = getLocalPackageFile(packageFileName);
659            if (fileToAdd == null) {
660                String pkgId = getRemotePackageId(packageFileName);
661                if (pkgId == null) {
662                    if (ignoreMissing) {
663                        log.warn("Could not add package: {}", packageFileName);
664                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_INFO, "Could not add package.");
665                        return null;
666                    } else {
667                        throw new PackageException("Could not find a remote or local (relative to "
668                                + "current directory or to NUXEO_HOME) " + "package with name or ID "
669                                + packageFileName);
670                    }
671                } else if (!downloadPackages(new ArrayList<>(Collections.singleton(pkgId)))) {
672                    throw new PackageException("Could not download package " + pkgId);
673                }
674                pkg = service.getPackage(pkgId);
675                if (pkg == null) {
676                    throw new PackageException("Could not find downloaded package in cache " + pkgId);
677                }
678            } else {
679                pkg = service.addPackage(fileToAdd);
680                log.info("Added {}", packageFileName);
681            }
682            newPackageInfo(cmdInfo, pkg);
683        } catch (PackageException e) {
684            cmdInfo.exitCode = 1;
685            cmdInfo.newMessage(e);
686        }
687        return pkg;
688    }
689
690    /**
691     * Install a list of local packages. If the list contains a package name (versus an ID), only the considered as best
692     * matching package is installed.
693     *
694     * @since 6.0
695     * @param packageIdsToInstall The list can contain package IDs and names
696     * @param ignoreMissing If true, doesn't throw an exception on unknown packages
697     * @see #pkgInstall(String, boolean)
698     */
699    public boolean pkgInstall(List<String> packageIdsToInstall, boolean ignoreMissing) {
700        log.debug("Installing: {}", packageIdsToInstall);
701        Queue<String> remaining = new LinkedList<>(packageIdsToInstall);
702        while (!remaining.isEmpty()) {
703            String pkgId = remaining.poll();
704            if (pkgInstall(pkgId, ignoreMissing) == null && !ignoreMissing) {
705                return false;
706            }
707            if (isRestartRequired()) {
708                remaining.forEach(pkg -> persistCommand(CommandInfo.CMD_INSTALL + " " + pkg));
709                throw new LauncherRestartException();
710            }
711        }
712        return true;
713    }
714
715    /**
716     * Persists the pending package operation into file system. It's useful when Nuxeo launcher is about to exit. Empty
717     * command line won't be persisted.
718     * <p>
719     * The given command will be appended as a new line into target file {@link #pendingFile}. NOTE: the command line
720     * options are not serialized. Therefore, they should be provided by nuxeoctl again after launcher's restart.
721     *
722     * @param command command to persist (appended as a new line)
723     * @throws IllegalStateException if any exception occurs
724     * @see #pendingFile
725     * @since 10.2
726     */
727    protected void persistCommand(String command) {
728        if (command.isEmpty()) {
729            return;
730        }
731        try {
732            Files.write(pendingFile, Collections.singletonList(command), StandardOpenOption.CREATE,
733                    StandardOpenOption.APPEND);
734        } catch (IOException e) {
735            throw new IllegalStateException("Cannot write to file " + pendingFile, e);
736        }
737    }
738
739    /**
740     * @since 10.2
741     */
742    protected boolean isRestartRequired() {
743        return "true".equals(env.getProperty(LAUNCHER_CHANGED_PROPERTY));
744    }
745
746    /**
747     * Install a local package.
748     *
749     * @since 6.0
750     * @param pkgId Package ID or Name
751     * @param ignoreMissing If true, doesn't throw an exception on unknown packages
752     * @return The installed LocalPackage or null if failed
753     * @see #pkgInstall(List, boolean)
754     */
755    public LocalPackage pkgInstall(String pkgId, boolean ignoreMissing) {
756        CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_INSTALL);
757        cmdInfo.param = pkgId;
758        try {
759            LocalPackage pkg = getLocalPackage(pkgId);
760            if (pkg != null && pkg.getPackageState().isInstalled()) {
761                if (pkg.getVersion().isSnapshot()) {
762                    log.info("Updating package {}...", pkg);
763                    // First remove it to allow SNAPSHOT upgrade
764                    pkgRemove(pkgId);
765                    pkg = null;
766                } else {
767                    log.info("Package {} is already installed.", pkg);
768                    return pkg;
769                }
770            }
771            if (pkg == null) {
772                // We don't know this package, try to add it first
773                pkg = pkgAdd(pkgId, ignoreMissing);
774            }
775            if (pkg == null) {
776                // Nothing worked - can't find the package anywhere
777                if (ignoreMissing) {
778                    log.warn("Unable to install package: {}", pkgId);
779                    return null;
780                } else {
781                    throw new PackageException("Package not found: " + pkgId);
782                }
783            }
784            pkgId = pkg.getId();
785            cmdInfo.param = pkgId;
786            log.info("Installing {}", pkgId);
787            Task installTask = pkg.getInstallTask();
788            try {
789                performTask(installTask);
790            } catch (PackageException e) {
791                installTask.rollback();
792                throw e;
793            }
794            // Refresh state
795            pkg = service.getPackage(pkgId);
796            newPackageInfo(cmdInfo, pkg);
797            return pkg;
798        } catch (PackageException e) {
799            log.error("Failed to install package: {} ({})", pkgId, e.getMessage());
800            log.debug(e, e);
801            cmdInfo.exitCode = 1;
802            cmdInfo.newMessage(e);
803            return null;
804        }
805    }
806
807    public boolean listPending(File commandsFile) {
808        return executePending(commandsFile, false, false, false);
809    }
810
811    /**
812     * @since 5.6
813     * @param commandsFile File containing the commands to execute
814     * @param doExecute Whether to execute or list the actions
815     * @param useResolver Whether to use full resolution or just execute individual actions
816     */
817    public boolean executePending(File commandsFile, boolean doExecute, boolean useResolver, boolean ignoreMissing) {
818        int errorValue = 0;
819        if (!commandsFile.isFile()) {
820            return false;
821        }
822        List<String> pkgsToAdd = new ArrayList<>();
823        List<String> pkgsToInstall = new ArrayList<>();
824        List<String> pkgsToUninstall = new ArrayList<>();
825        List<String> pkgsToRemove = new ArrayList<>();
826
827        Path commandsPath = commandsFile.toPath();
828        pendingFile = commandsPath;
829        Path backup = commandsPath.resolveSibling(commandsFile.getName() + ".bak");
830        try {
831            Queue<String> remainingCmds = new LinkedList<>(Files.readAllLines(commandsFile.toPath()));
832            if (doExecute) {
833                // backup the commandsFile before any real execution
834                Files.move(commandsPath, backup, StandardCopyOption.REPLACE_EXISTING);
835            }
836            while (!remainingCmds.isEmpty()) {
837                String line = remainingCmds.poll().trim();
838                String[] split = line.split("\\s+", 2);
839                if (split.length == 2) {
840                    if (split[0].equals(CommandInfo.CMD_INSTALL)) {
841                        if (doExecute) {
842                            if (useResolver) {
843                                pkgsToInstall.add(split[1]);
844                            } else {
845                                pkgInstall(split[1], ignoreMissing);
846                            }
847                        } else {
848                            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_INSTALL);
849                            cmdInfo.param = split[1];
850                            cmdInfo.pending = true;
851                        }
852                    } else if (split[0].equals(CommandInfo.CMD_ADD)) {
853                        if (doExecute) {
854                            if (useResolver) {
855                                pkgsToAdd.add(split[1]);
856                            } else {
857                                pkgAdd(split[1], ignoreMissing);
858                            }
859                        } else {
860                            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
861                            cmdInfo.param = split[1];
862                            cmdInfo.pending = true;
863                        }
864                    } else if (split[0].equals(CommandInfo.CMD_UNINSTALL)) {
865                        if (doExecute) {
866                            if (useResolver) {
867                                pkgsToUninstall.add(split[1]);
868                            } else {
869                                pkgUninstall(split[1]);
870                            }
871                        } else {
872                            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_UNINSTALL);
873                            cmdInfo.param = split[1];
874                            cmdInfo.pending = true;
875                        }
876                    } else if (split[0].equals(CommandInfo.CMD_REMOVE)) {
877                        if (doExecute) {
878                            if (useResolver) {
879                                pkgsToRemove.add(split[1]);
880                            } else {
881                                pkgRemove(split[1]);
882                            }
883                        } else {
884                            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_REMOVE);
885                            cmdInfo.param = split[1];
886                            cmdInfo.pending = true;
887                        }
888                    } else {
889                        errorValue = 1;
890                    }
891                } else if (split.length == 1) {
892                    if (line.length() > 0 && !line.startsWith("#")) {
893                        if (doExecute) {
894                            if (useResolver) {
895                                pkgsToInstall.add(line);
896                            } else {
897                                pkgInstall(line, ignoreMissing);
898                            }
899                        } else {
900                            if ("init".equals(line)) {
901                                CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_INIT);
902                                cmdInfo.pending = true;
903                            } else {
904                                CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_INSTALL);
905                                cmdInfo.param = line;
906                                cmdInfo.pending = true;
907                            }
908                        }
909                    }
910                }
911                if (errorValue != 0) {
912                    log.error("Error processing pending package/command: {}", line);
913                }
914                if (doExecute && !useResolver && isRestartRequired()) {
915                    remainingCmds.forEach(this::persistCommand);
916                    throw new LauncherRestartException();
917                }
918            }
919            if (doExecute) {
920                if (useResolver) {
921                    String oldAccept = accept;
922                    String oldRelax = relax;
923                    accept = "true";
924                    if ("ask".equalsIgnoreCase(relax)) {
925                        log.info("Relax mode changed from 'ask' to 'false' for executing the pending actions.");
926                        relax = "false";
927                    }
928                    boolean success = pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, true,
929                            ignoreMissing);
930                    accept = oldAccept;
931                    relax = oldRelax;
932                    if (!success) {
933                        errorValue = 2;
934                    }
935                }
936                if (errorValue != 0) {
937                    log.error("Pending actions execution failed. The commands file has been moved to: {}", backup);
938                }
939            } else {
940                cset.log(true);
941            }
942        } catch (IOException e) {
943            log.error(e, e);
944        }
945        return errorValue == 0;
946    }
947
948    @SuppressWarnings("unused")
949    protected boolean downloadPackages(List<String> packagesToDownload) {
950        boolean isRegistered = LogicalInstanceIdentifier.isRegistered();
951        List<String> packagesAlreadyDownloaded = new ArrayList<>();
952        for (String pkg : packagesToDownload) {
953            LocalPackage localPackage;
954            try {
955                localPackage = getLocalPackage(pkg);
956            } catch (PackageException e) {
957                log.error("Looking for package '{}' in local cache raised an error. Aborting.", pkg, e);
958                return false;
959            }
960            if (localPackage == null) {
961                continue;
962            }
963            if (localPackage.getPackageState().isInstalled()) {
964                log.error("Package '{}' is installed. Download skipped.", pkg);
965                packagesAlreadyDownloaded.add(pkg);
966            } else if (localPackage.getVersion().isSnapshot()) {
967                // Check if registration is required
968                DownloadablePackage downloadablePkg = getPackageManager().findRemotePackageById(pkg);
969                if (downloadablePkg != null && downloadablePkg.hasSubscriptionRequired() && !isRegistered) {
970                    log.info("Registration is required for package '{}'. Download skipped.", pkg);
971                    packagesAlreadyDownloaded.add(pkg);
972                    continue;
973                }
974                log.info("Download of '{}' will replace the one already in local cache.", pkg);
975            } else {
976                log.info("Package '{}' is already in local cache.", pkg);
977                packagesAlreadyDownloaded.add(pkg);
978            }
979        }
980
981        packagesToDownload.removeAll(packagesAlreadyDownloaded);
982        if (packagesToDownload.isEmpty()) {
983            return true;
984        }
985        // Queue downloads
986        log.info("Downloading {}...", packagesToDownload);
987        boolean downloadOk = true;
988        List<DownloadingPackage> pkgs = new ArrayList<>();
989        for (String pkg : packagesToDownload) {
990            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_DOWNLOAD);
991            cmdInfo.param = pkg;
992
993            // Check if registration is required
994            DownloadablePackage downloadablePkg = getPackageManager().findRemotePackageById(pkg);
995            if (downloadablePkg != null && downloadablePkg.hasSubscriptionRequired() && !isRegistered) {
996                downloadOk = false;
997                cmdInfo.exitCode = 1;
998                cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Registration required.");
999                continue;
1000            }
1001
1002            // Download
1003            try {
1004                DownloadingPackage download = getPackageManager().download(pkg);
1005                if (download != null) {
1006                    pkgs.add(download);
1007                    cmdInfo.param = download.getId();
1008                    cmdInfo.newMessage(SimpleLog.LOG_LEVEL_DEBUG, "Downloading...");
1009                } else {
1010                    downloadOk = false;
1011                    cmdInfo.exitCode = 1;
1012                    cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download failed (not found).");
1013                }
1014            } catch (ConnectServerError e) {
1015                log.debug(e, e);
1016                downloadOk = false;
1017                cmdInfo.exitCode = 1;
1018                cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download failed: " + e.getMessage());
1019            }
1020        }
1021        // Check and display progress
1022        final String progress = "|/-\\";
1023        int x = 0;
1024        boolean stopDownload = false;
1025        do {
1026            System.out.print(progress.charAt(x++ % progress.length()) + "\r");
1027            try {
1028                Thread.sleep(1000);
1029            } catch (InterruptedException e) {
1030                Thread.currentThread().interrupt();
1031                throw new RuntimeException(e);
1032            }
1033            List<DownloadingPackage> pkgsCompleted = new ArrayList<>();
1034            for (DownloadingPackage pkg : pkgs) {
1035                if (pkg.isCompleted()) {
1036                    pkgsCompleted.add(pkg);
1037                    CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_DOWNLOAD);
1038                    cmdInfo.param = pkg.getId();
1039                    // Digest check not correctly implemented
1040                    if (false && !pkg.isDigestOk()) {
1041                        downloadOk = false;
1042                        cmdInfo.exitCode = 1;
1043                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Wrong digest.");
1044                    } else if (pkg.getPackageState() == PackageState.DOWNLOADED) {
1045                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_DEBUG, "Downloaded.");
1046                    } else {
1047                        downloadOk = false;
1048                        cmdInfo.exitCode = 1;
1049                        cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download failed: " + pkg.getErrorMessage());
1050                        if (pkg.isServerError()) { // Wasted effort to continue other downloads
1051                            stopDownload = true;
1052                        }
1053                    }
1054                }
1055            }
1056            pkgs.removeAll(pkgsCompleted);
1057        } while (!stopDownload && !pkgs.isEmpty());
1058        if (!pkgs.isEmpty()) {
1059            downloadOk = false;
1060            log.error("Packages download was interrupted");
1061            for (DownloadingPackage pkg : pkgs) {
1062                CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_ADD);
1063                cmdInfo.param = pkg.getId();
1064                cmdInfo.exitCode = 1;
1065                cmdInfo.newMessage(SimpleLog.LOG_LEVEL_ERROR, "Download interrupted.");
1066            }
1067        }
1068        return downloadOk;
1069    }
1070
1071    /**
1072     * @param keepExisting If false, the request will remove existing packages that are not part of the resolution
1073     * @param ignoreMissing Do not error out on missing packages, just handle the rest
1074     * @since 5.9.2
1075     */
1076    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
1077            List<String> pkgsToRemove, boolean keepExisting, boolean ignoreMissing) {
1078        // default is install mode
1079        return pkgRequest(pkgsToAdd, pkgsToInstall, pkgsToUninstall, pkgsToRemove, keepExisting, ignoreMissing, false);
1080    }
1081
1082    /**
1083     * @param keepExisting If false, the request will remove existing packages that are not part of the resolution
1084     * @param ignoreMissing Do not error out on missing packages, just handle the rest
1085     * @param upgradeMode If true, all packages will be upgraded to their last compliant version
1086     * @throws LauncherRestartException if launcher is required to restart
1087     * @since 8.4
1088     */
1089    public boolean pkgRequest(List<String> pkgsToAdd, List<String> pkgsToInstall, List<String> pkgsToUninstall,
1090            List<String> pkgsToRemove, boolean keepExisting, boolean ignoreMissing, boolean upgradeMode) {
1091        PlatformId actualTargetPlatform = targetPlatform;
1092        try {
1093            boolean cmdOk;
1094            if (Boolean.parseBoolean(relax)) {
1095                targetPlatform = null;
1096            }
1097            // Add local files
1098            cmdOk = pkgAdd(pkgsToAdd, ignoreMissing);
1099            // Build solver request
1100            List<String> solverInstall = new ArrayList<>();
1101            List<String> solverRemove = new ArrayList<>();
1102            List<String> solverUpgrade = new ArrayList<>();
1103            // Potential local cache snapshots to replace
1104            Set<String> localSnapshotsToMaybeReplace = new HashSet<>();
1105            if (pkgsToInstall != null) {
1106                List<String> namesOrIdsToInstall = new ArrayList<>();
1107                Set<String> localSnapshotsToUninstall = new HashSet<>();
1108                Set<String> localSnapshotsToReplace = new HashSet<>();
1109                cmdOk = checkLocalPackagesAndAddLocalFiles(pkgsToInstall, upgradeMode, ignoreMissing,
1110                        namesOrIdsToInstall, localSnapshotsToUninstall, localSnapshotsToReplace,
1111                        localSnapshotsToMaybeReplace);
1112
1113                // Replace snapshots to install but already in cache (requested by id or filename)
1114                if (CollectionUtils.isNotEmpty(localSnapshotsToReplace)) {
1115                    log.info("The following SNAPSHOT package(s) will be replaced in local cache (if available): {}",
1116                            localSnapshotsToReplace);
1117                    String initialAccept = accept;
1118                    if ("ask".equalsIgnoreCase(accept)) {
1119                        accept = readConsole("Do you want to continue (yes/no)? [yes] ", "yes");
1120                    }
1121                    if (!Boolean.parseBoolean(accept)) {
1122                        log.warn("Exit");
1123                        return false;
1124                    }
1125                    accept = initialAccept;
1126                    for (String pkgId : localSnapshotsToUninstall) {
1127                        LocalPackage uninstalledPkg = pkgUninstall(pkgId);
1128                        if (uninstalledPkg == null) {
1129                            cmdOk = false;
1130                        }
1131                    }
1132                    for (String pkgIdOrFileName : localSnapshotsToReplace) {
1133                        if (isLocalPackageFile(pkgIdOrFileName) || isRemotePackageId(pkgIdOrFileName)) {
1134                            LocalPackage addedPkg = pkgAdd(pkgIdOrFileName, ignoreMissing);
1135                            if (addedPkg == null) {
1136                                cmdOk = false;
1137                            }
1138                        } else {
1139                            log.info("The SNAPSHOT package {} is not available remotely, local cache will be used.",
1140                                    pkgIdOrFileName);
1141                        }
1142                    }
1143                }
1144
1145                if (upgradeMode) {
1146                    solverUpgrade.addAll(namesOrIdsToInstall);
1147                } else {
1148                    solverInstall.addAll(namesOrIdsToInstall);
1149                }
1150            }
1151            if (pkgsToUninstall != null) {
1152                solverRemove.addAll(pkgsToUninstall);
1153            }
1154            if (pkgsToRemove != null) {
1155                // Add packages to remove to uninstall list
1156                solverRemove.addAll(pkgsToRemove);
1157            }
1158            if (!solverInstall.isEmpty() || !solverRemove.isEmpty() || !solverUpgrade.isEmpty()) {
1159                // Check whether we need to relax restriction to targetPlatform
1160                PlatformId requestPlatform = actualTargetPlatform;
1161                List<String> requestPackages = new ArrayList<>();
1162                requestPackages.addAll(solverInstall);
1163                requestPackages.addAll(solverRemove);
1164                requestPackages.addAll(solverUpgrade);
1165                if (ignoreMissing) {
1166                    // Remove unknown packages from the list
1167                    Map<String, List<DownloadablePackage>> knownNames = getPackageManager().getAllPackagesByName();
1168                    List<String> solverInstallCopy = new ArrayList<>(solverInstall);
1169                    for (String pkgToInstall : solverInstallCopy) {
1170                        if (!knownNames.containsKey(pkgToInstall)) {
1171                            log.warn("Unable to install unknown package: {}", pkgToInstall);
1172                            solverInstall.remove(pkgToInstall);
1173                            requestPackages.remove(pkgToInstall);
1174                        }
1175                    }
1176                }
1177                List<String> nonCompliantPkg = getPackageManager().getNonCompliantList(requestPackages,
1178                        actualTargetPlatform);
1179                if (!nonCompliantPkg.isEmpty()) {
1180                    requestPlatform = null;
1181                    if ("ask".equalsIgnoreCase(relax)) {
1182                        relax = readConsole(
1183                                "Package(s) %s not available on platform version %s.\n"
1184                                        + "Do you want to relax the constraint (yes/no)? [no] ",
1185                                "no", StringUtils.join(nonCompliantPkg, ", "), actualTargetPlatform.asString());
1186                    }
1187
1188                    if (Boolean.parseBoolean(relax)) {
1189                        log.warn("Relax restriction to target platform {} because of package(s) {}",
1190                                actualTargetPlatform::asString, () -> String.join(", ", nonCompliantPkg));
1191                    } else {
1192                        if (ignoreMissing) {
1193                            for (String pkgToInstall : nonCompliantPkg) {
1194                                log.warn("Unable to install package: {}", pkgToInstall);
1195                                solverInstall.remove(pkgToInstall);
1196                            }
1197                        } else {
1198                            throw new PackageException(String.format(
1199                                    "Package(s) %s not available on platform version %s (relax is not allowed)",
1200                                    StringUtils.join(nonCompliantPkg, ", "), actualTargetPlatform.asString()));
1201                        }
1202                    }
1203                }
1204
1205                log.debug("solverInstall: {}", () -> solverInstall);
1206                log.debug("solverRemove: {}", () -> solverRemove);
1207                log.debug("solverUpgrade: {}", () -> solverUpgrade);
1208                DependencyResolution resolution = getPackageManager().resolveDependencies(solverInstall, solverRemove,
1209                        solverUpgrade, requestPlatform, allowSNAPSHOT, keepExisting);
1210                log.info(resolution);
1211                if (resolution.isFailed()) {
1212                    return false;
1213                }
1214                if (resolution.isEmpty()) {
1215                    pkgRemove(pkgsToRemove);
1216                    return cmdOk;
1217                }
1218                if ("ask".equalsIgnoreCase(accept)) {
1219                    accept = readConsole("Do you want to continue (yes/no)? [yes] ", "yes");
1220                }
1221                if (!Boolean.parseBoolean(accept)) {
1222                    log.warn("Exit");
1223                    return false;
1224                }
1225
1226                LinkedList<String> packageIdsToRemove = new LinkedList<>(resolution.getOrderedPackageIdsToRemove());
1227                LinkedList<String> packageIdsToUpgrade = new LinkedList<>(resolution.getUpgradePackageIds());
1228                LinkedList<String> packageIdsToInstall = new LinkedList<>(resolution.getOrderedPackageIdsToInstall());
1229                LinkedList<String> packagesIdsToReInstall = new LinkedList<>();
1230
1231                // Replace snapshots to install but already in cache (requested by name)
1232                if (CollectionUtils.containsAny(packageIdsToInstall, localSnapshotsToMaybeReplace)) {
1233                    for (Object pkgIdObj : CollectionUtils.intersection(packageIdsToInstall,
1234                            localSnapshotsToMaybeReplace)) {
1235                        String pkgId = (String) pkgIdObj;
1236                        LocalPackage addedPkg = pkgAdd(pkgId, ignoreMissing);
1237                        if (addedPkg == null) {
1238                            cmdOk = false;
1239                        }
1240                    }
1241                }
1242
1243                // Download remote packages
1244                if (!downloadPackages(resolution.getDownloadPackageIds())) {
1245                    log.error("Aborting packages change request");
1246                    return false;
1247                }
1248
1249                // Uninstall
1250                if (!packageIdsToUpgrade.isEmpty()) {
1251                    // Add packages to upgrade to uninstall list
1252                    // Don't use IDs to avoid downgrade instead of uninstall
1253                    packageIdsToRemove.addAll(resolution.getLocalPackagesToUpgrade().keySet());
1254                    DependencyResolution uninstallResolution = getPackageManager().resolveDependencies(null,
1255                            packageIdsToRemove, null, requestPlatform, allowSNAPSHOT, keepExisting, true);
1256                    log.debug("Sub-resolution (uninstall) {}", uninstallResolution);
1257                    if (uninstallResolution.isFailed()) {
1258                        return false;
1259                    }
1260                    LinkedList<String> newPackageIdsToRemove = new LinkedList<>(
1261                            uninstallResolution.getOrderedPackageIdsToRemove());
1262                    packagesIdsToReInstall = new LinkedList<>(newPackageIdsToRemove);
1263                    packagesIdsToReInstall.removeAll(packageIdsToRemove);
1264                    packagesIdsToReInstall.removeAll(packageIdsToUpgrade);
1265                    packageIdsToRemove = newPackageIdsToRemove;
1266                }
1267                log.debug("Uninstalling: {}", packageIdsToRemove);
1268                while (!packageIdsToRemove.isEmpty()) {
1269                    String pkgId = packageIdsToRemove.poll();
1270                    if (pkgUninstall(pkgId) == null) {
1271                        log.error("Unable to uninstall {}", pkgId);
1272                        return false;
1273                    }
1274                    if (isRestartRequired()) {
1275                        packageIdsToRemove.forEach(pkg -> persistCommand(CommandInfo.CMD_UNINSTALL + " " + pkg));
1276                        packageIdsToInstall.forEach(pkg -> persistCommand(CommandInfo.CMD_INSTALL + " " + pkg));
1277                        throw new LauncherRestartException();
1278                    }
1279                }
1280
1281                // Install
1282                if (!packagesIdsToReInstall.isEmpty()) {
1283                    // Add list of packages uninstalled because of upgrade
1284                    packageIdsToInstall.addAll(packagesIdsToReInstall);
1285                    DependencyResolution installResolution = getPackageManager().resolveDependencies(
1286                            packageIdsToInstall, null, null, requestPlatform, allowSNAPSHOT, keepExisting, true);
1287                    log.debug("Sub-resolution (install) {}", installResolution);
1288                    if (installResolution.isFailed()) {
1289                        return false;
1290                    }
1291                    packageIdsToInstall = new LinkedList<>(installResolution.getOrderedPackageIdsToInstall());
1292                }
1293                if (!pkgInstall(packageIdsToInstall, ignoreMissing)) {
1294                    return false;
1295                }
1296
1297                pkgRemove(pkgsToRemove);
1298            }
1299            return cmdOk;
1300        } catch (PackageException e) {
1301            log.error(e);
1302            log.debug(e, e);
1303            return false;
1304        } finally {
1305            targetPlatform = actualTargetPlatform;
1306        }
1307    }
1308
1309    private boolean checkLocalPackagesAndAddLocalFiles(List<String> pkgsToInstall, boolean upgradeMode,
1310            boolean ignoreMissing, List<String> namesOrIdsToInstall, Set<String> localSnapshotsToUninstall,
1311            Set<String> localSnapshotsToReplace, Set<String> localSnapshotsToMaybeReplace) throws PackageException {
1312        boolean cmdOk = true;
1313        for (String pkgToInstall : pkgsToInstall) {
1314            String nameOrIdToInstall = pkgToInstall;
1315            if (!upgradeMode) {
1316                boolean isLocalPackageFile = isLocalPackageFile(pkgToInstall);
1317                if (isLocalPackageFile) {
1318                    // If install request is a file name, get the id
1319                    nameOrIdToInstall = getLocalPackageFileId(getLocalPackageFile(pkgToInstall));
1320                }
1321                // get corresponding local package if present.
1322                // if request is a name, prefer installed package
1323                LocalPackage localPackage = getInstalledPackageByName(nameOrIdToInstall);
1324                if (localPackage != null) {
1325                    // as not in upgrade mode, replace the package name by the installed package id
1326                    nameOrIdToInstall = localPackage.getId();
1327                } else {
1328                    if (isLocalPackageId(nameOrIdToInstall)) {
1329                        // if request is an id, get potential package in local cache
1330                        localPackage = getLocalPackage(nameOrIdToInstall);
1331                    } else {
1332                        // if request is a name but there is no installed package matching, get the best version
1333                        // in local cache to replace it if it is a snapshot and it happens to be the actual
1334                        // version to install afterward
1335                        LocalPackage potentialMatchingPackage = getLocalPackage(nameOrIdToInstall);
1336                        if (potentialMatchingPackage != null && potentialMatchingPackage.getVersion().isSnapshot()) {
1337                            localSnapshotsToMaybeReplace.add(potentialMatchingPackage.getId());
1338                        }
1339                    }
1340                }
1341                // first install of local file or directory
1342                if (localPackage == null && isLocalPackageFile) {
1343                    LocalPackage addedPkg = pkgAdd(pkgToInstall, ignoreMissing);
1344                    if (addedPkg == null) {
1345                        cmdOk = false;
1346                    }
1347                }
1348                // if a requested SNAPSHOT package is present, mark it for replacement in local cache
1349                if (localPackage != null && localPackage.getVersion().isSnapshot()) {
1350                    if (localPackage.getPackageState().isInstalled()) {
1351                        // if it's already installed, uninstall it
1352                        localSnapshotsToUninstall.add(nameOrIdToInstall);
1353                    }
1354                    // use the local file name if given and ensure we replace the right version, in case
1355                    // nameOrIdToInstall is a name
1356                    String pkgToAdd = isLocalPackageFile ? pkgToInstall : localPackage.getId();
1357                    localSnapshotsToReplace.add(pkgToAdd);
1358                }
1359            }
1360            namesOrIdsToInstall.add(nameOrIdToInstall);
1361        }
1362        return cmdOk;
1363    }
1364
1365    /**
1366     * Installs a list of packages and uninstalls the rest (no dependency check)
1367     *
1368     * @since 6.0
1369     */
1370    public boolean pkgSet(List<String> pkgList, boolean ignoreMissing) {
1371        boolean cmdOK = pkgInstall(pkgList, ignoreMissing);
1372        List<DownloadablePackage> installedPkgs = getPackageManager().listInstalledPackages();
1373        List<String> pkgsToUninstall = new ArrayList<>();
1374        for (DownloadablePackage pkg : installedPkgs) {
1375            if ((!pkgList.contains(pkg.getName())) && (!pkgList.contains(pkg.getId()))) {
1376                pkgsToUninstall.add(pkg.getId());
1377            }
1378        }
1379        if (!pkgsToUninstall.isEmpty()) {
1380            cmdOK = cmdOK && pkgUninstall(pkgsToUninstall);
1381        }
1382        return cmdOK;
1383    }
1384
1385    /**
1386     * Prompt user for yes/no answer
1387     *
1388     * @param message The message to display
1389     * @param defaultValue The default answer if there's no console or if "Enter" key is pressed.
1390     * @param objects Parameters to use in the message (like in {@link String#format(String, Object...)})
1391     * @return {@code "true"} if answer is in {@link #POSITIVE_ANSWERS}, else return {@code "false"}
1392     */
1393    protected String readConsole(String message, String defaultValue, Object... objects) {
1394        String answer;
1395        Console console = System.console();
1396        if (console == null || StringUtils.isEmpty(answer = console.readLine(message, objects))) {
1397            answer = defaultValue;
1398        }
1399        answer = answer.trim().toLowerCase();
1400        return parseAnswer(answer);
1401    }
1402
1403    /**
1404     * @return {@code "true"} if answer is in {@link #POSITIVE_ANSWERS}, and {@code "ask"} if answer values
1405     *         {@code "ask"}, else return {@code "false"}
1406     * @since 6.0
1407     */
1408    public static String parseAnswer(String answer) {
1409        if ("ask".equalsIgnoreCase(answer)) {
1410            return "ask";
1411        }
1412        if ("false".equalsIgnoreCase(answer)) {
1413            return "false";
1414        }
1415        for (String positive : POSITIVE_ANSWERS) {
1416            if (positive.equalsIgnoreCase(answer)) {
1417                return "true";
1418            }
1419        }
1420        return "false";
1421    }
1422
1423    public boolean pkgHotfix() {
1424        List<String> lastHotfixes = getPackageManager().listLastHotfixes(targetPlatform, allowSNAPSHOT);
1425        return pkgRequest(null, lastHotfixes, null, null, true, false);
1426    }
1427
1428    public boolean pkgUpgrade() {
1429        List<String> upgradeNames = getPackageManager().listInstalledPackagesNames(null);
1430        // use upgrade mode
1431        return pkgRequest(null, upgradeNames, null, null, true, false, true);
1432    }
1433
1434    /**
1435     * Must be called after {@link #setAccept(String)} which overwrites its value.
1436     *
1437     * @param relaxValue true, false or ask; ignored if null
1438     */
1439    public void setRelax(String relaxValue) {
1440        if (relaxValue != null) {
1441            relax = parseAnswer(relaxValue);
1442        }
1443    }
1444
1445    /**
1446     * @param acceptValue true, false or ask; if true or ask, then calls {@link #setRelax(String)} with the same value;
1447     *            ignored if null
1448     */
1449    public void setAccept(String acceptValue) {
1450        if (acceptValue != null) {
1451            accept = parseAnswer(acceptValue);
1452            if ("ask".equals(accept) || "true".equals(accept)) {
1453                setRelax(acceptValue);
1454            }
1455        }
1456    }
1457
1458    /*
1459     * Helper for adding a new PackageInfo initialized with informations gathered from the given package. It is not put
1460     * into CommandInfo to avoid adding a dependency on Connect Client
1461     */
1462    private PackageInfo newPackageInfo(CommandInfo cmdInfo, Package pkg) {
1463        PackageInfo packageInfo = new PackageInfo(pkg);
1464        cmdInfo.packages.add(packageInfo);
1465        return packageInfo;
1466    }
1467
1468    /**
1469     * @param packages List of packages identified by their ID, name or local filename.
1470     * @since 5.7
1471     */
1472    public boolean pkgShow(List<String> packages) {
1473        boolean cmdOk = true;
1474        if (packages == null || packages.isEmpty()) {
1475            return cmdOk;
1476        }
1477        StringBuilder sb = new StringBuilder();
1478        sb.append("****************************************");
1479        for (String pkgId : packages) {
1480            CommandInfo cmdInfo = cset.newCommandInfo(CommandInfo.CMD_SHOW);
1481            cmdInfo.param = pkgId;
1482            try {
1483                Package pkg = findPackage(pkgId);
1484                PackageInfo packageInfo = newPackageInfo(cmdInfo, pkg);
1485                sb.append("\nPackage: ").append(packageInfo.id);
1486                sb.append("\nState: ").append(packageInfo.state);
1487                if (pkg instanceof DownloadablePackage) {
1488                    sb.append(" " + getPrivateOrSubscriptionTag((DownloadablePackage) pkg));
1489                }
1490                sb.append("\nVersion: ").append(packageInfo.version);
1491                sb.append("\nName: ").append(packageInfo.name);
1492                sb.append("\nType: ").append(packageInfo.type);
1493                sb.append("\nTarget platforms: ").append(ArrayUtils.toString(packageInfo.targetPlatforms));
1494                appendIfNotEmpty(sb, "\nVendor: ", packageInfo.vendor);
1495                sb.append("\nSupports hot-reload: ").append(packageInfo.supportsHotReload);
1496                appendIfNotEmpty(sb, "\nProvides: ", packageInfo.provides);
1497                appendIfNotEmpty(sb, "\nDepends: ", packageInfo.dependencies);
1498                appendIfNotEmpty(sb, "\nConflicts: ", packageInfo.conflicts);
1499                appendIfNotEmpty(sb, "\nTitle: ", packageInfo.title);
1500                appendIfNotEmpty(sb, "\nDescription: ", packageInfo.description);
1501                appendIfNotEmpty(sb, "\nLicense: ", packageInfo.licenseType);
1502                appendIfNotEmpty(sb, "\nLicense URL: ", packageInfo.licenseUrl);
1503                sb.append("\n****************************************");
1504            } catch (PackageException e) {
1505                cmdOk = false;
1506                cmdInfo.exitCode = 1;
1507                cmdInfo.newMessage(e);
1508            }
1509        }
1510        log.info(sb::toString);
1511        return cmdOk;
1512    }
1513
1514    private void appendIfNotEmpty(StringBuilder sb, String label, Object[] array) {
1515        if (ArrayUtils.isNotEmpty(array)) {
1516            sb.append(label).append(ArrayUtils.toString(array));
1517        }
1518    }
1519
1520    private void appendIfNotEmpty(StringBuilder sb, String label, String value) {
1521        if (StringUtils.isNotEmpty(value)) {
1522            sb.append(label).append(value);
1523        }
1524    }
1525
1526    /**
1527     * Looks for a package. First look if it's a local ZIP file, second if it's a local package and finally if it's a
1528     * remote package.
1529     *
1530     * @param pkg A ZIP filename or file path, or package ID or a package name.
1531     * @return The first package found matching the given string.
1532     * @throws PackageException If no package is found or if an issue occurred while searching.
1533     * @see PackageDefinition
1534     * @see LocalPackage
1535     * @see DownloadablePackage
1536     */
1537    protected Package findPackage(String pkg) throws PackageException {
1538        // Is it a local ZIP file?
1539        File localPackageFile = getLocalPackageFile(pkg);
1540        if (localPackageFile != null) {
1541            return service.loadPackageFromZip(localPackageFile);
1542        }
1543
1544        // Is it a local package ID or name?
1545        LocalPackage localPackage = getLocalPackage(pkg);
1546        if (localPackage != null) {
1547            return localPackage;
1548        }
1549
1550        // Is it a remote package ID or name?
1551        String pkgId = getRemotePackageId(pkg);
1552        if (pkgId != null) {
1553            return getPackageManager().findPackageById(pkgId);
1554        }
1555
1556        throw new PackageException("Could not find a remote or local (relative to "
1557                + "current directory or to NUXEO_HOME) " + "package with name or ID " + pkg);
1558    }
1559
1560    /**
1561     * @since 5.9.1
1562     */
1563    public void setAllowSNAPSHOT(boolean allow) {
1564        CUDFHelper.defaultAllowSNAPSHOT = allow;
1565        allowSNAPSHOT = allow;
1566    }
1567
1568}