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