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}