1 /******************************************************************************* 2 * Copyright (c) 2007, 2020 IBM Corporation and others. 3 * All rights reserved. 4 * This program and the accompanying materials are made available under the 5 * terms of the Eclipse Public License 2.0 which accompanies this distribution, 6 * and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * IBM Corporation - initial implementation and ideas 13 * Sonatype, Inc. - ongoing development 14 * RedHat, Inc. - Bug 397216, Bug 460967 15 ******************************************************************************/ 16 package org.eclipse.equinox.internal.p2.reconciler.dropins; 17 18 import java.io.*; 19 import java.net.*; 20 import java.util.*; 21 import java.util.Map.Entry; 22 import org.eclipse.core.runtime.*; 23 import org.eclipse.equinox.internal.p2.core.helpers.*; 24 import org.eclipse.equinox.internal.p2.director.ProfileChangeRequest; 25 import org.eclipse.equinox.internal.p2.extensionlocation.Constants; 26 import org.eclipse.equinox.internal.provisional.configurator.Configurator; 27 import org.eclipse.equinox.internal.provisional.p2.directorywatcher.RepositoryListener; 28 import org.eclipse.equinox.p2.core.IProvisioningAgent; 29 import org.eclipse.equinox.p2.core.ProvisionException; 30 import org.eclipse.equinox.p2.engine.*; 31 import org.eclipse.equinox.p2.engine.query.IUProfilePropertyQuery; 32 import org.eclipse.equinox.p2.metadata.*; 33 import org.eclipse.equinox.p2.planner.IPlanner; 34 import org.eclipse.equinox.p2.planner.ProfileInclusionRules; 35 import org.eclipse.equinox.p2.query.*; 36 import org.eclipse.equinox.p2.repository.IRepository; 37 import org.eclipse.equinox.p2.repository.artifact.IArtifactRepository; 38 import org.eclipse.equinox.p2.repository.artifact.IFileArtifactRepository; 39 import org.eclipse.equinox.p2.repository.metadata.IMetadataRepository; 40 import org.eclipse.osgi.service.environment.EnvironmentInfo; 41 import org.eclipse.osgi.util.NLS; 42 import org.osgi.framework.BundleContext; 43 import org.osgi.framework.ServiceReference; 44 45 /** 46 * Synchronizes a profile with a set of repositories. 47 */ 48 public class ProfileSynchronizer { 49 private static final String RECONCILER_APPLICATION_ID = "org.eclipse.equinox.p2.reconciler.application"; //$NON-NLS-1$ 50 private static final String TIMESTAMPS_FILE_PREFIX = "timestamps"; //$NON-NLS-1$ 51 private static final String PROFILE_TIMESTAMP = "PROFILE"; //$NON-NLS-1$ 52 private static final String NO_TIMESTAMP = "-1"; //$NON-NLS-1$ 53 private static final String PROP_FROM_DROPINS = "org.eclipse.equinox.p2.reconciler.dropins"; //$NON-NLS-1$ 54 private static final String INCLUSION_RULES = "org.eclipse.equinox.p2.internal.inclusion.rules"; //$NON-NLS-1$ 55 private static final String INCLUSION_OPTIONAL = "OPTIONAL"; //$NON-NLS-1$ 56 private static final String INCLUSION_STRICT = "STRICT"; //$NON-NLS-1$ 57 58 private static final String CACHE_EXTENSIONS = "org.eclipse.equinox.p2.cache.extensions"; //$NON-NLS-1$ 59 private static final String PIPE = "|"; //$NON-NLS-1$ 60 private static final String EXPLANATION = "org.eclipse.equinox.p2.director.explain"; //$NON-NLS-1$ 61 62 static final String PROP_IGNORE_USER_CONFIGURATION = "eclipse.ignoreUserConfiguration"; //$NON-NLS-1$ 63 64 final IProfile profile; 65 66 final Map<String, IMetadataRepository> repositoryMap; 67 private Map<String, String> timestamps; 68 private final IProvisioningAgent agent; 69 70 /* 71 * Specialized profile change request so we can keep track of IUs which have moved 72 * locations on disk. 73 */ 74 static class ReconcilerProfileChangeRequest extends ProfileChangeRequest { 75 List<IInstallableUnit> toMove = new ArrayList<>(); 76 ReconcilerProfileChangeRequest(IProfile profile)77 public ReconcilerProfileChangeRequest(IProfile profile) { 78 super(profile); 79 } 80 moveAll(Collection<IInstallableUnit> list)81 void moveAll(Collection<IInstallableUnit> list) { 82 toMove.addAll(list); 83 } 84 getMoves()85 Collection<IInstallableUnit> getMoves() { 86 return toMove; 87 } 88 } 89 90 /* 91 * Constructor for the class. 92 */ ProfileSynchronizer(IProvisioningAgent agent, IProfile profile, Collection<IMetadataRepository> repositories)93 public ProfileSynchronizer(IProvisioningAgent agent, IProfile profile, Collection<IMetadataRepository> repositories) { 94 this.agent = agent; 95 this.profile = profile; 96 this.repositoryMap = new HashMap<>(); 97 for (IMetadataRepository repository : repositories) { 98 repositoryMap.put(repository.getLocation().toString(), repository); 99 } 100 } 101 102 /* 103 * Synchronize the profile with the list of metadata repositories. 104 * TODO fix progress monitoring (although in practice the user doesn't see it or have a chance to cancel) 105 */ synchronize(IProgressMonitor monitor)106 public IStatus synchronize(IProgressMonitor monitor) { 107 readTimestamps(); 108 if (isUpToDate()) 109 return Status.OK_STATUS; 110 111 ProvisioningContext context = getContext(); 112 context.setProperty(EXPLANATION, Boolean.valueOf(Tracing.DEBUG_RECONCILER).toString()); 113 114 String updatedCacheExtensions = synchronizeCacheExtensions(); 115 116 // figure out if we really have anything to install/uninstall. 117 ReconcilerProfileChangeRequest request = createProfileChangeRequest(context); 118 if (request == null) { 119 if (updatedCacheExtensions == null) 120 return Status.OK_STATUS; 121 IStatus engineResult = setProperty(CACHE_EXTENSIONS, updatedCacheExtensions, context, null); 122 if (engineResult.getSeverity() != IStatus.ERROR && engineResult.getSeverity() != IStatus.CANCEL) 123 writeTimestamps(); 124 return engineResult; 125 } 126 if (updatedCacheExtensions != null) 127 request.setProfileProperty(CACHE_EXTENSIONS, updatedCacheExtensions); 128 129 // if some of the IUs move locations then construct a special plan and execute that first 130 IStatus moveResult = performRemoveForMovedIUs(request, context, monitor); 131 if (moveResult.getSeverity() == IStatus.ERROR || moveResult.getSeverity() == IStatus.CANCEL) 132 return moveResult; 133 134 if (!request.getRemovals().isEmpty()) { 135 Collection<IRequirement> requirements = new ArrayList<>(); 136 for (IInstallableUnit unit : request.getRemovals()) { 137 IRequirement req = MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, unit.getId(), new VersionRange(unit.getVersion(), true, unit.getVersion(), true), null, 0, 0, false); 138 requirements.add(req); 139 } 140 request.addExtraRequirements(requirements); 141 } 142 143 // now create a plan for the rest of the work and execute it 144 IStatus addRemoveResult = performAddRemove(request, context, monitor); 145 if (addRemoveResult.getSeverity() == IStatus.ERROR || addRemoveResult.getSeverity() == IStatus.CANCEL) 146 return addRemoveResult; 147 148 // write out the new timestamps (for caching) and apply the configuration 149 writeTimestamps(); 150 IStatus applyResult = applyConfiguration(false); 151 152 // Mark the state update as hidden so it does not appear in the Installation History UI list 153 // TODO We need to determine if it is ok to use this copy of the profile. 154 // See https://bugs.eclipse.org/334670 155 IProfileRegistry profileRegistry = agent.getService(IProfileRegistry.class); 156 if (profileRegistry != null) { 157 IStatus result = profileRegistry.setProfileStateProperty(profile.getProfileId(), profile.getTimestamp(), IProfile.STATE_PROP_HIDDEN, Boolean.TRUE.toString()); 158 if (!result.isOK()) { 159 // we don't get here but if we do, we will ignore the problem and continue. We 160 // still want the install operation to succeed. The consequence of this failure is the 161 // profile state appears in the UI in the Install History page, which isn't horrible. 162 LogHelper.log(result); 163 } 164 } 165 166 return applyResult; 167 } 168 169 /* 170 * Return a list of the roots in the profile. 171 */ getStrictRoots()172 private IQueryResult<IInstallableUnit> getStrictRoots() { 173 return profile.query(new IUProfilePropertyQuery(INCLUSION_RULES, INCLUSION_STRICT), null); 174 } 175 176 /* 177 * Convert the profile change request into operands and have the engine execute them. There 178 * is fancy logic here in case we are trying to remove IUs which are depended on by something 179 * which is installed via the UI. Since the bundle has been removed from the file-system it is a forced 180 * removal so we have to uninstall the UI-installed IU. 181 */ performAddRemove(ReconcilerProfileChangeRequest request, ProvisioningContext context, IProgressMonitor monitor)182 private IStatus performAddRemove(ReconcilerProfileChangeRequest request, ProvisioningContext context, IProgressMonitor monitor) { 183 // if we have moves then we have previously removed them. 184 // now we need to add them back (at the new location) 185 for (IInstallableUnit iu : request.getMoves()) { 186 request.add(iu); 187 request.setInstallableUnitProfileProperty(iu, PROP_FROM_DROPINS, Boolean.TRUE.toString()); 188 request.setInstallableUnitInclusionRules(iu, ProfileInclusionRules.createOptionalInclusionRule(iu)); 189 request.setInstallableUnitProfileProperty(iu, IProfile.PROP_PROFILE_LOCKED_IU, Integer.toString(IProfile.LOCK_UNINSTALL)); 190 } 191 192 Collection<IInstallableUnit> additions = request.getAdditions(); 193 Collection<IInstallableUnit> removals = request.getRemovals(); 194 // see if there is any work to do 195 if (additions.isEmpty() && removals.isEmpty()) 196 return Status.OK_STATUS; 197 198 // TODO See bug 270195. Eventually we will attempt to remove strictly installed IUs if their 199 // dependent bundles have been deleted. 200 boolean removeStrictRoots = false; 201 if (removeStrictRoots) 202 return performStrictRootRemoval(request, context, monitor); 203 IProvisioningPlan plan = createProvisioningPlan(request, context, monitor); 204 debug(request, plan); 205 return executePlan(plan, context, monitor); 206 } 207 208 // TODO re-enable after resolving bug 270195. performStrictRootRemoval(ReconcilerProfileChangeRequest request, ProvisioningContext context, IProgressMonitor monitor)209 private IStatus performStrictRootRemoval(ReconcilerProfileChangeRequest request, ProvisioningContext context, IProgressMonitor monitor) { 210 Collection<IInstallableUnit> removals = request.getRemovals(); 211 // if we don't have any removals then we don't have to worry about potentially 212 // invalidating things we already have installed, removal of roots, etc so just 213 // create a regular plan. 214 if (removals.isEmpty()) { 215 IProvisioningPlan plan = createProvisioningPlan(request, context, monitor); 216 debug(request, plan); 217 return executePlan(plan, context, monitor); 218 } 219 220 // We are now creating a backup of the original request that will be used to create the final plan (where no optional magic is used) 221 ProfileChangeRequest finalRequest = request.clone(); 222 223 // otherwise collect the roots, pretend they are optional, and see 224 // if the resulting plan affects them 225 Set<IInstallableUnit> strictRoots = getStrictRoots().toUnmodifiableSet(); 226 Collection<IRequirement> forceNegation = new ArrayList<>(removals.size()); 227 for (IInstallableUnit iu : removals) 228 forceNegation.add(createNegation(iu)); 229 request.addExtraRequirements(forceNegation); 230 231 // set all the profile roots to be optional to see how they would be effected by the plan 232 for (IInstallableUnit iu : strictRoots) 233 request.setInstallableUnitProfileProperty(iu, INCLUSION_RULES, INCLUSION_OPTIONAL); 234 235 // get the tentative plan back from the planner 236 IProvisioningPlan plan = createProvisioningPlan(request, context, monitor); 237 debug(request, plan); 238 if (!plan.getStatus().isOK()) 239 return plan.getStatus(); 240 241 // Analyze the plan to see if any of the strict roots are being uninstalled. 242 int removedRoots = 0; 243 for (IInstallableUnit initialRoot : strictRoots) { 244 // if the root wasn't uninstalled, then continue 245 if (plan.getRemovals().query(QueryUtil.createIUQuery(initialRoot), null).isEmpty()) 246 continue; 247 // otherwise add its removal to the change request, along with a negation and 248 // change of strict to optional for their inclusion rule. 249 finalRequest.remove(initialRoot); 250 finalRequest.setInstallableUnitProfileProperty(initialRoot, INCLUSION_RULES, INCLUSION_OPTIONAL); 251 IRequirement negation = createNegation(initialRoot); 252 Collection<IRequirement> extra = new ArrayList<>(); 253 extra.add(negation); 254 request.addExtraRequirements(extra); 255 LogHelper.log(new Status(IStatus.INFO, Activator.ID, NLS.bind(Messages.remove_root, initialRoot.getId(), initialRoot.getVersion()))); 256 removedRoots++; 257 } 258 259 // Check for the case where all the strict roots are being removed. 260 if (removedRoots == strictRoots.size()) 261 return new Status(IStatus.ERROR, Activator.ID, Messages.remove_all_roots); 262 plan = createProvisioningPlan(finalRequest, context, monitor); 263 if (!plan.getStatus().isOK()) { 264 System.out.println("original request"); //$NON-NLS-1$ 265 System.out.println(request); 266 System.out.println("final request"); //$NON-NLS-1$ 267 System.out.println(finalRequest); 268 throw new IllegalStateException("The second plan is not resolvable."); //$NON-NLS-1$ 269 } 270 271 // execute the plan and return the status 272 return executePlan(plan, context, monitor); 273 } 274 275 /* 276 * If the request contains IUs to be moved then create and execute a plan which 277 * removes them. Otherwise just return. 278 */ performRemoveForMovedIUs(ReconcilerProfileChangeRequest request, ProvisioningContext context, IProgressMonitor monitor)279 private IStatus performRemoveForMovedIUs(ReconcilerProfileChangeRequest request, ProvisioningContext context, IProgressMonitor monitor) { 280 Collection<IInstallableUnit> moves = request.getMoves(); 281 if (moves.isEmpty()) 282 return Status.OK_STATUS; 283 IEngine engine = agent.getService(IEngine.class); 284 IProvisioningPlan plan = engine.createPlan(profile, context); 285 for (IInstallableUnit unit : moves) 286 plan.removeInstallableUnit(unit); 287 return executePlan(plan, context, monitor); 288 } 289 290 /* 291 * Write out the timestamps of various repositories and folders/file to help 292 * us cache and detect cases where we don't have to perform a reconciliation. 293 */ writeTimestamps()294 private void writeTimestamps() { 295 timestamps.clear(); 296 timestamps.put(PROFILE_TIMESTAMP, Long.toString(profile.getTimestamp())); 297 for (Entry<String, IMetadataRepository> entry : repositoryMap.entrySet()) { 298 IMetadataRepository repository = entry.getValue(); 299 Map<String, String> props = repository.getProperties(); 300 String timestamp = null; 301 if (props != null) 302 timestamp = props.get(IRepository.PROP_TIMESTAMP); 303 if (timestamp == null) 304 timestamp = NO_TIMESTAMP; 305 306 timestamps.put(entry.getKey(), timestamp); 307 } 308 309 try { 310 File file = Activator.getContext().getDataFile(TIMESTAMPS_FILE_PREFIX + profile.getProfileId().hashCode()); 311 Activator.trace("Writing timestamp file to : " + file.getAbsolutePath()); //$NON-NLS-1$ 312 try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { 313 CollectionUtils.storeProperties(timestamps, os, "Timestamps for " + profile.getProfileId()); //$NON-NLS-1$ 314 if (Tracing.DEBUG_RECONCILER) { 315 for (String key : timestamps.keySet()) { 316 Object value = timestamps.get(key); 317 Activator.trace(key + '=' + value); 318 } 319 } 320 } 321 } catch (FileNotFoundException e) { 322 //Ignore 323 } catch (IOException e) { 324 //Ignore 325 } 326 } 327 328 /* 329 * Check timestamps and return true if the profile is considered to be up-to-date or 330 * false if we should perform a reconciliation. 331 */ isUpToDate()332 private boolean isUpToDate() { 333 // the user might want to force a reconciliation 334 if ("true".equals(Activator.getContext().getProperty("osgi.checkConfiguration"))) { //$NON-NLS-1$//$NON-NLS-2$ 335 Activator.trace("User requested forced reconciliation via \"osgi.checkConfiguration=true\" System property."); //$NON-NLS-1$ 336 Activator.trace("Performing reconciliation."); //$NON-NLS-1$ 337 return false; 338 } 339 340 String lastKnownProfileTimeStamp = timestamps.remove(PROFILE_TIMESTAMP); 341 if (lastKnownProfileTimeStamp == null) { 342 Activator.trace("Profile timestamp not found in cache."); //$NON-NLS-1$ 343 Activator.trace("Performing reconciliation."); //$NON-NLS-1$ 344 return false; 345 } 346 String currentProfileTimestamp = Long.toString(profile.getTimestamp()); 347 if (!lastKnownProfileTimeStamp.equals(currentProfileTimestamp)) { 348 Activator.trace("Profile timestamps not equal, expected: " + lastKnownProfileTimeStamp + ", actual=" + currentProfileTimestamp); //$NON-NLS-1$ //$NON-NLS-2$ 349 Activator.trace("Performing reconciliation."); //$NON-NLS-1$ 350 return false; 351 } 352 353 //When we get here the timestamps map only contains information related to repos 354 for (Entry<String, IMetadataRepository> entry : repositoryMap.entrySet()) { 355 IMetadataRepository repository = entry.getValue(); 356 357 Map<String, String> props = repository.getProperties(); 358 String currentTimestamp = null; 359 if (props != null) 360 currentTimestamp = props.get(IRepository.PROP_TIMESTAMP); 361 362 if (currentTimestamp == null) 363 currentTimestamp = NO_TIMESTAMP; 364 365 String key = entry.getKey(); 366 String lastKnownTimestamp = timestamps.remove(key); 367 //A repo has been added 368 if (lastKnownTimestamp == null) { 369 Activator.trace("No cached timestamp found for: " + key); //$NON-NLS-1$ 370 Activator.trace("Performing reconciliation."); //$NON-NLS-1$ 371 return false; 372 } 373 if (!lastKnownTimestamp.equals(currentTimestamp)) { 374 Activator.trace("Timestamps not equal for file: " + key + ", expected: " + lastKnownTimestamp + ", actual: " + currentTimestamp); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ 375 Activator.trace("Performing reconciliation."); //$NON-NLS-1$ 376 return false; 377 } 378 } 379 if (timestamps.size() == 0) { 380 Activator.trace("Timestamps valid."); //$NON-NLS-1$ 381 Activator.trace("Skipping reconciliation."); //$NON-NLS-1$ 382 return true; 383 } 384 385 //A repo has been removed 386 if (Tracing.DEBUG_RECONCILER) { 387 Activator.trace("Extra values in timestamp file:"); //$NON-NLS-1$ 388 for (String string : timestamps.keySet()) 389 Activator.trace(string); 390 Activator.trace("Performing reconciliation."); //$NON-NLS-1$ 391 } 392 return false; 393 } 394 395 /* 396 * Read the values of the stored timestamps that we use for caching. 397 */ readTimestamps()398 private void readTimestamps() { 399 if (Boolean.TRUE.toString().equalsIgnoreCase(System.getProperty(PROP_IGNORE_USER_CONFIGURATION))) { 400 timestamps = new HashMap<>(); 401 Activator.trace("Master profile changed."); //$NON-NLS-1$ 402 Activator.trace("Performing reconciliation."); //$NON-NLS-1$ 403 return; 404 } 405 File file = Activator.getContext().getDataFile(TIMESTAMPS_FILE_PREFIX + profile.getProfileId().hashCode()); 406 try { 407 try (InputStream is = new BufferedInputStream(new FileInputStream(file))) { 408 timestamps = CollectionUtils.loadProperties(is); 409 } 410 } catch (FileNotFoundException e) { 411 //Ignore 412 timestamps = new HashMap<>(); 413 Activator.trace("Timestamp file does not exist."); //$NON-NLS-1$ 414 Activator.trace("Performing reconciliation."); //$NON-NLS-1$ 415 } catch (IOException e) { 416 //Ignore 417 timestamps = new HashMap<>(); 418 Activator.trace("Exception loading timestamp file: " + e.getMessage()); //$NON-NLS-1$ 419 Activator.trace("Performing reconciliation."); //$NON-NLS-1$ 420 } 421 } 422 getContext()423 private ProvisioningContext getContext() { 424 ArrayList<URI> repoURLs = new ArrayList<>(); 425 for (String string : repositoryMap.keySet()) { 426 try { 427 repoURLs.add(new URI(string)); 428 } catch (URISyntaxException e) { 429 //ignore 430 } 431 } 432 ProvisioningContext result = new ProvisioningContext(agent); 433 result.setMetadataRepositories(repoURLs.toArray(new URI[repoURLs.size()])); 434 result.setArtifactRepositories(new URI[0]); 435 return result; 436 } 437 synchronizeCacheExtensions()438 private String synchronizeCacheExtensions() { 439 List<String> currentExtensions = new ArrayList<>(); 440 StringBuilder buffer = new StringBuilder(); 441 442 List<String> repositories = new ArrayList<>(repositoryMap.keySet()); 443 URL installArea = Activator.getOSGiInstallArea(); 444 final String OSGiInstallArea; 445 try { 446 // The OSGi install area is an unencoded URL and repository locations are encoded URIs 447 // so make them the same so we can compare them. 448 // See https://bugs.eclipse.org/346565. 449 OSGiInstallArea = URIUtil.toURI(installArea).toString() + Constants.EXTENSION_LOCATION; 450 // Sort the repositories so the extension location at the OSGi install folder is first. 451 // See https://bugs.eclipse.org/246310. 452 repositories.sort((left, right) -> { 453 if (OSGiInstallArea.equals(left)) 454 return -1; 455 if (OSGiInstallArea.equals(right)) 456 return 1; 457 return left.compareTo(right); 458 }); 459 } catch (URISyntaxException e) { 460 // This shouldn't happen but if it does we will log the error and continue 461 // with the repositories in the default order. 462 LogHelper.log(new Status(IStatus.ERROR, Activator.ID, "Unable to convert OSGi install area: " + installArea + " into URI.", e)); //$NON-NLS-1$ //$NON-NLS-2$ 463 } 464 for (Iterator<String> it = repositories.iterator(); it.hasNext();) { 465 String repositoryId = it.next(); 466 try { 467 IArtifactRepository repository = Activator.loadArtifactRepository(new URI(repositoryId), null); 468 if (repository instanceof IFileArtifactRepository) { 469 currentExtensions.add(escapePipe(repositoryId)); 470 buffer.append(repositoryId); 471 if (it.hasNext()) 472 buffer.append(PIPE); 473 } 474 } catch (ProvisionException e) { 475 // ignore 476 } catch (URISyntaxException e) { 477 // unexpected 478 e.printStackTrace(); 479 } 480 } 481 String currentExtensionsProperty = (buffer.length() == 0) ? null : buffer.toString(); 482 483 List<String> previousExtensions = new ArrayList<>(); 484 String previousExtensionsProperty = profile.getProperty(CACHE_EXTENSIONS); 485 if (previousExtensionsProperty != null) { 486 StringTokenizer tokenizer = new StringTokenizer(previousExtensionsProperty, PIPE); 487 while (tokenizer.hasMoreTokens()) { 488 previousExtensions.add(tokenizer.nextToken()); 489 } 490 } 491 492 if (previousExtensions.size() == currentExtensions.size() && previousExtensions.containsAll(currentExtensions)) 493 return null; 494 495 return currentExtensionsProperty; 496 } 497 498 /** 499 * Escapes the pipe ('|') character in a URI using the standard URI escape sequence. 500 * This is done because the pipe character is used as the delimiter between locations 501 * in the cache extensions profile property. 502 */ escapePipe(String location)503 private String escapePipe(String location) { 504 String result = location; 505 int pipeIndex; 506 while ((pipeIndex = result.indexOf(',')) != -1) 507 result = result.substring(0, pipeIndex) + "%7C" + result.substring(pipeIndex + 1); //$NON-NLS-1$ 508 return result; 509 } 510 511 /* 512 * Return a map of all the IUs in the profile 513 * Use a map here so we have a copy of the original IU from the profile... we will need it later. 514 */ getProfileIUs()515 private Map<IInstallableUnit, IInstallableUnit> getProfileIUs() { 516 IQueryResult<IInstallableUnit> profileQueryResult = profile.query(QueryUtil.createIUAnyQuery(), null); 517 Map<IInstallableUnit, IInstallableUnit> result = new HashMap<>(); 518 for (IInstallableUnit iu : profileQueryResult) { 519 result.put(iu, iu); 520 } 521 return result; 522 } 523 524 /* 525 * Return a map of all the IUs available in the profile. This takes the shared parents into consideration, if applicable. 526 * Use a map here so we have a copy of the original IU from the profile... we will need it later. 527 */ getAvailableProfileIUs()528 private Map<IInstallableUnit, IInstallableUnit> getAvailableProfileIUs() { 529 IQueryResult<IInstallableUnit> profileQueryResult = profile.available(QueryUtil.createIUAnyQuery(), null); 530 Map<IInstallableUnit, IInstallableUnit> result = new HashMap<>(); 531 for (IInstallableUnit iu : profileQueryResult) { 532 result.put(iu, iu); 533 } 534 return result; 535 } 536 537 /* 538 * Return the profile change requests that we need to execute in order to install everything from the 539 * dropins folder(s). (or uninstall things that have been removed) We use a collection here because if 540 * the user has moved bundles from the dropins to the plugins (for instance) then we need to uninstall 541 * the old bundle and then re-install the new one. This is because the IUs for the moved bundles are 542 * considered the same but they really differ in an IU property. (file location, which is not considered 543 * as part of equality) 544 */ createProfileChangeRequest(ProvisioningContext context)545 public ReconcilerProfileChangeRequest createProfileChangeRequest(ProvisioningContext context) { 546 ReconcilerProfileChangeRequest request = new ReconcilerProfileChangeRequest(profile); 547 548 boolean resolve = Boolean.parseBoolean(profile.getProperty("org.eclipse.equinox.p2.resolve")); //$NON-NLS-1$ 549 if (resolve) 550 request.removeProfileProperty("org.eclipse.equinox.p2.resolve"); //$NON-NLS-1$ 551 552 List<IInstallableUnit> toRemove = new ArrayList<>(); 553 List<IInstallableUnit> toMove = new ArrayList<>(); 554 555 boolean foundIUsToAdd = false; 556 Map<IInstallableUnit, IInstallableUnit> profileIUs = getProfileIUs(); 557 558 // we use IProfile.available(...) here so that we also gather any shared IUs 559 Map<IInstallableUnit, IInstallableUnit> availableProfileIUs = getAvailableProfileIUs(); 560 561 // get all IUs from all our repos 562 IQueryResult<IInstallableUnit> allIUs = getAllIUsFromRepos(); 563 for (Iterator<IInstallableUnit> iter = allIUs.iterator(); iter.hasNext();) { 564 final IInstallableUnit iu = iter.next(); 565 IInstallableUnit existing = profileIUs.get(iu); 566 // check to see if this IU has moved locations 567 if (existing != null) { 568 // if the IU is already installed in the profile then check to see if it was moved. 569 String one = iu.getProperty(RepositoryListener.FILE_NAME); 570 String two = existing.getProperty(RepositoryListener.FILE_NAME); 571 // cheat here... since we always set the filename property for bundles in the dropins, 572 // if the existing IU's filename is null then it isn't from the dropins. a better 573 // (and more expensive) way to find this out is to do an IU profile property query. 574 if (two == null) { 575 // the IU is already installed so don't mark it as a dropin now - see bug 404619. 576 iter.remove(); 577 continue; 578 } 579 // if we have an IU which has been moved, keep track of it. 580 if (one != null && !one.equals(two)) { 581 toMove.add(iu); 582 continue; 583 } 584 } 585 // even though we are adding all IUs below, we need to explicitly set the properties for 586 // them as well. Do that here. 587 if (QueryUtil.isGroup(iu)) 588 request.setInstallableUnitProfileProperty(iu, IProfile.PROP_PROFILE_ROOT_IU, Boolean.TRUE.toString()); 589 // mark all IUs with special property 590 request.setInstallableUnitProfileProperty(iu, PROP_FROM_DROPINS, Boolean.TRUE.toString()); 591 request.setInstallableUnitInclusionRules(iu, ProfileInclusionRules.createOptionalInclusionRule(iu)); 592 request.setInstallableUnitProfileProperty(iu, IProfile.PROP_PROFILE_LOCKED_IU, Integer.toString(IProfile.LOCK_UNINSTALL)); 593 594 // as soon as we find something locally that needs to be installed, then 595 // everything from the parent's dropins must be installed locally as well. 596 if (!foundIUsToAdd && availableProfileIUs.get(iu) == null) { 597 foundIUsToAdd = true; 598 } 599 } 600 601 // get all IUs from profile with marked property (existing) 602 IQueryResult<IInstallableUnit> dropinIUs = profile.query(new IUProfilePropertyQuery(PROP_FROM_DROPINS, Boolean.TRUE.toString()), null); 603 Set<IInstallableUnit> all = allIUs.toUnmodifiableSet(); 604 for (IInstallableUnit iu : dropinIUs) { 605 // the STRICT policy is set when we install things via the UI, we use it to differentiate between IUs installed 606 // via the dropins and the UI. (dropins are considered optional) If an IU has both properties set it means that 607 // it was initially installed via the dropins but then upgraded via the UI. (properties are copied from the old IU 608 // to the new IU during an upgrade) In this case we want to remove the "from dropins" property so the upgrade 609 // will stick. 610 if ("STRICT".equals(profile.getInstallableUnitProperty(iu, "org.eclipse.equinox.p2.internal.inclusion.rules"))) { //$NON-NLS-1$//$NON-NLS-2$ 611 request.removeInstallableUnitProfileProperty(iu, PROP_FROM_DROPINS); 612 request.removeInstallableUnitProfileProperty(iu, IProfile.PROP_PROFILE_LOCKED_IU); 613 continue; 614 } 615 // if the IU from the profile is in the "all available" list, then it is already added 616 // otherwise if it isn't in the repo then we have to remove it from the profile. 617 if (!all.contains(iu)) 618 toRemove.add(iu); 619 } 620 621 if (!foundIUsToAdd && toRemove.isEmpty() && !resolve && toMove.isEmpty()) { 622 if (Tracing.DEBUG_RECONCILER) 623 Tracing.debug("[reconciler] Nothing to do."); //$NON-NLS-1$ 624 return null; 625 } 626 627 // everything from the drop-ins must be considered for addition/removal everytime so add all here 628 request.addAll(all); 629 request.removeAll(toRemove); 630 request.moveAll(toMove); 631 632 debug(request); 633 return request; 634 } 635 636 /* 637 * Create and return a negated requirement saying that the given IU must not exist in the profile. 638 */ createNegation(IInstallableUnit unit)639 private IRequirement createNegation(IInstallableUnit unit) { 640 return MetadataFactory.createRequirement(IInstallableUnit.NAMESPACE_IU_ID, unit.getId(), // 641 new VersionRange(unit.getVersion(), true, unit.getVersion(), true), null, 0, 0, false); 642 } 643 644 /* 645 * If in debug mode, print out information which tells us whether or not the given 646 * provisioning plan matches the request. 647 */ debug(ReconcilerProfileChangeRequest request, IProvisioningPlan plan)648 private void debug(ReconcilerProfileChangeRequest request, IProvisioningPlan plan) { 649 if (!Tracing.DEBUG_RECONCILER) 650 return; 651 final String PREFIX = "[reconciler] [plan] "; //$NON-NLS-1$ 652 // get the request 653 List<IInstallableUnit> toAdd = new ArrayList<>(request.getAdditions()); 654 List<IInstallableUnit> toRemove = new ArrayList<>(request.getRemovals()); 655 List<IInstallableUnit> toMove = new ArrayList<>(request.getMoves()); 656 657 // remove from the request everything that is in the plan 658 for (IInstallableUnit iu : plan.getRemovals().query(QueryUtil.createIUAnyQuery(), null)) { 659 if (!toRemove.remove(iu)) { 660 Tracing.debug(PREFIX + iu + " will be removed"); //$NON-NLS-1$ 661 } 662 } 663 for (IInstallableUnit iu : plan.getAdditions().query(QueryUtil.createIUAnyQuery(), null)) { 664 if (!toAdd.remove(iu)) { 665 Tracing.debug(PREFIX + iu + " will be added"); //$NON-NLS-1$ 666 } 667 } 668 // Move operations are treated as doing a remove/add. The removes have already happened 669 // and at this point we are adding the moved IUs back at their new location. Remove the moved 670 // IUs from the added list because this will just confuse the user. 671 toAdd.removeAll(toMove); 672 673 // if anything is left in the request, then something is wrong with the plan 674 if (toAdd.size() == 0 && toRemove.size() == 0) 675 Tracing.debug(PREFIX + "Plan matches the request."); //$NON-NLS-1$ 676 if (toAdd.size() != 0) { 677 Tracing.debug(PREFIX + "Some units will not be installed, because they are already installed or there are dependency issues:"); //$NON-NLS-1$ 678 for (IInstallableUnit unit : toAdd) 679 Tracing.debug(PREFIX + unit); 680 } 681 if (toRemove.size() != 0) { 682 Tracing.debug(PREFIX + "Some units will not be uninstalled:"); //$NON-NLS-1$ 683 for (IInstallableUnit unit : toRemove) 684 Tracing.debug(PREFIX + unit); 685 } 686 } 687 688 /* 689 * If debugging is turned on, then print out the details for the given profile change request. 690 */ debug(ReconcilerProfileChangeRequest request)691 private void debug(ReconcilerProfileChangeRequest request) { 692 if (!Tracing.DEBUG_RECONCILER) 693 return; 694 final String PREFIX = "[reconciler] "; //$NON-NLS-1$ 695 Collection<IInstallableUnit> toAdd = request.getAdditions(); 696 if (toAdd == null || toAdd.size() == 0) { 697 Tracing.debug(PREFIX + "No installable units to add."); //$NON-NLS-1$ 698 } else { 699 for (IInstallableUnit add : toAdd) { 700 Tracing.debug(PREFIX + "Adding IU: " + add.getId() + ' ' + add.getVersion()); //$NON-NLS-1$ 701 } 702 } 703 Map<IInstallableUnit, Map<String, String>> propsToAdd = request.getInstallableUnitProfilePropertiesToAdd(); 704 if (propsToAdd == null || propsToAdd.isEmpty()) { 705 Tracing.debug(PREFIX + "No IU properties to add."); //$NON-NLS-1$ 706 } else { 707 for (Entry<IInstallableUnit, Map<String, String>> entry : propsToAdd.entrySet()) { 708 Tracing.debug(PREFIX + "Adding IU property: " + entry.getKey() + "->" + entry.getValue()); //$NON-NLS-1$ //$NON-NLS-2$ 709 } 710 } 711 712 Collection<IInstallableUnit> toRemove = request.getRemovals(); 713 if (toRemove == null || toRemove.size() == 0) { 714 Tracing.debug(PREFIX + "No installable units to remove."); //$NON-NLS-1$ 715 } else { 716 for (IInstallableUnit remove : toRemove) { 717 Tracing.debug(PREFIX + "Removing IU: " + remove.getId() + ' ' + remove.getVersion()); //$NON-NLS-1$ 718 } 719 } 720 Map<IInstallableUnit, List<String>> propsToRemove = request.getInstallableUnitProfilePropertiesToRemove(); 721 if (propsToRemove == null || propsToRemove.isEmpty()) { 722 Tracing.debug(PREFIX + "No IU properties to remove."); //$NON-NLS-1$ 723 } else { 724 for (Entry<IInstallableUnit, List<String>> entry : propsToRemove.entrySet()) { 725 Tracing.debug(PREFIX + "Removing IU property: " + entry.getKey() + "->" + entry.getValue()); //$NON-NLS-1$ //$NON-NLS-2$ 726 } 727 } 728 729 Collection<IInstallableUnit> toMove = request.getMoves(); 730 if (toMove == null || toMove.isEmpty()) { 731 Tracing.debug(PREFIX + "No installable units to move."); //$NON-NLS-1$ 732 } else { 733 for (IInstallableUnit move : toMove) 734 Tracing.debug(PREFIX + "Moving IU: " + move.getId() + ' ' + move.getVersion()); //$NON-NLS-1$ 735 } 736 737 Collection<IRequirement> extra = request.getExtraRequirements(); 738 if (extra == null || extra.isEmpty()) { 739 Tracing.debug(PREFIX + "No extra requirements."); //$NON-NLS-1$ 740 } else { 741 for (IRequirement requirement : extra) 742 Tracing.debug(PREFIX + "Extra requirement: " + requirement); //$NON-NLS-1$ 743 } 744 } 745 746 /* 747 * Return all of the IUs available in all of our repos. This usually includes the dropins and plugins folders 748 * as well as any sites specified in the platform.xml file. 749 */ getAllIUsFromRepos()750 private IQueryResult<IInstallableUnit> getAllIUsFromRepos() { 751 // TODO: Should consider using a sequenced iterator here instead of collecting 752 Collector<IInstallableUnit> allRepos = new Collector<>(); 753 for (IMetadataRepository repository : repositoryMap.values()) { 754 allRepos.addAll(repository.query(QueryUtil.createIUAnyQuery(), null)); 755 } 756 return allRepos; 757 } 758 759 /* 760 * Create and return a provisioning plan for the given change request. 761 */ createProvisioningPlan(ProfileChangeRequest request, ProvisioningContext provisioningContext, IProgressMonitor monitor)762 private IProvisioningPlan createProvisioningPlan(ProfileChangeRequest request, ProvisioningContext provisioningContext, IProgressMonitor monitor) { 763 IPlanner planner = agent.getService(IPlanner.class); 764 return planner.getProvisioningPlan(request, provisioningContext, monitor); 765 } 766 767 /* 768 * Call the engine to set the given property on the profile. 769 */ setProperty(String key, String value, ProvisioningContext provisioningContext, IProgressMonitor monitor)770 private IStatus setProperty(String key, String value, ProvisioningContext provisioningContext, IProgressMonitor monitor) { 771 IEngine engine = agent.getService(IEngine.class); 772 IProvisioningPlan plan = engine.createPlan(profile, provisioningContext); 773 plan.setProfileProperty(key, value); 774 IPhaseSet phaseSet = PhaseSetFactory.createPhaseSetIncluding(new String[] {PhaseSetFactory.PHASE_PROPERTY}); 775 return engine.perform(plan, phaseSet, monitor); 776 } 777 778 /* 779 * Execute the given plan. 780 */ executePlan(IProvisioningPlan plan, ProvisioningContext provisioningContext, IProgressMonitor monitor)781 private IStatus executePlan(IProvisioningPlan plan, ProvisioningContext provisioningContext, IProgressMonitor monitor) { 782 IEngine engine = agent.getService(IEngine.class); 783 IPhaseSet phaseSet = PhaseSetFactory.createDefaultPhaseSetExcluding(new String[] {PhaseSetFactory.PHASE_COLLECT, PhaseSetFactory.PHASE_CHECK_TRUST}); 784 785 if (plan.getInstallerPlan() != null) { 786 IStatus installerPlanStatus = engine.perform(plan.getInstallerPlan(), phaseSet, monitor); 787 if (!installerPlanStatus.isOK()) 788 return installerPlanStatus; 789 790 applyConfiguration(true); 791 } 792 return engine.perform(plan, phaseSet, monitor); 793 } 794 795 /* 796 * Write out the configuration file. 797 */ applyConfiguration(boolean isInstaller)798 private IStatus applyConfiguration(boolean isInstaller) { 799 if (!isInstaller && isReconciliationApplicationRunning()) 800 return Status.OK_STATUS; 801 BundleContext context = Activator.getContext(); 802 ServiceReference<Configurator> reference = context.getServiceReference(Configurator.class); 803 Configurator configurator = context.getService(reference); 804 try { 805 configurator.applyConfiguration(); 806 } catch (IOException e) { 807 return new Status(IStatus.ERROR, Activator.ID, "Unexpected failure applying configuration", e); //$NON-NLS-1$ 808 } finally { 809 context.ungetService(reference); 810 } 811 return Status.OK_STATUS; 812 } 813 isReconciliationApplicationRunning()814 static boolean isReconciliationApplicationRunning() { 815 EnvironmentInfo info = ServiceHelper.getService(Activator.getContext(), EnvironmentInfo.class); 816 if (info == null) 817 return false; 818 String[] args = info.getCommandLineArgs(); 819 if (args == null) 820 return false; 821 for (String arg : args) { 822 if (arg != null && RECONCILER_APPLICATION_ID.equals(arg.trim())) 823 return true; 824 } 825 return false; 826 } 827 } 828