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 API and implementation
13  *     Red Hat, Inc (Krzysztof Daniel) - Bug 421935: Extend simpleconfigurator to
14  * read .info files from many locations, Bug 460967
15  *******************************************************************************/
16 package org.eclipse.equinox.internal.simpleconfigurator;
17 
18 import java.io.*;
19 import java.net.*;
20 import java.util.*;
21 import java.util.concurrent.CountDownLatch;
22 import org.eclipse.equinox.internal.simpleconfigurator.utils.*;
23 import org.osgi.framework.*;
24 import org.osgi.framework.namespace.*;
25 import org.osgi.framework.startlevel.BundleStartLevel;
26 import org.osgi.framework.wiring.*;
27 import org.osgi.resource.Namespace;
28 import org.osgi.resource.Requirement;
29 import org.osgi.service.packageadmin.PackageAdmin;
30 
31 class ConfigApplier {
32 	private static final String LAST_BUNDLES_INFO = "last.bundles.info"; //$NON-NLS-1$
33 	private static final String PROP_DEVMODE = "osgi.dev"; //$NON-NLS-1$
34 
35 	private final BundleContext manipulatingContext;
36 	private final PackageAdmin packageAdminService;
37 	private final FrameworkWiring frameworkWiring;
38 	private final boolean runningOnEquinox;
39 	private final boolean inDevMode;
40 
41 	private final Bundle callingBundle;
42 	private final URI baseLocation;
43 
ConfigApplier(BundleContext context, Bundle callingBundle)44 	ConfigApplier(BundleContext context, Bundle callingBundle) {
45 		manipulatingContext = context;
46 		this.callingBundle = callingBundle;
47 		runningOnEquinox = "Eclipse".equals(context.getProperty(Constants.FRAMEWORK_VENDOR)); //$NON-NLS-1$
48 		inDevMode = manipulatingContext.getProperty(PROP_DEVMODE) != null;
49 		baseLocation = runningOnEquinox ? EquinoxUtils.getInstallLocationURI(context) : null;
50 
51 		ServiceReference<PackageAdmin> packageAdminRef = manipulatingContext.getServiceReference(PackageAdmin.class);
52 		if (packageAdminRef == null)
53 			throw new IllegalStateException("No PackageAdmin service is available."); //$NON-NLS-1$
54 		packageAdminService = manipulatingContext.getService(packageAdminRef);
55 
56 		frameworkWiring = manipulatingContext.getBundle(Constants.SYSTEM_BUNDLE_LOCATION).adapt(FrameworkWiring.class);
57 	}
58 
install(URL url, boolean exclusiveMode)59 	void install(URL url, boolean exclusiveMode) throws IOException {
60 		List<BundleInfo> bundleInfoList = SimpleConfiguratorUtils.readConfiguration(url, baseLocation);
61 		if (Activator.DEBUG)
62 			System.out.println("applyConfiguration() bundleInfoList.size()=" + bundleInfoList.size());
63 		if (bundleInfoList.size() == 0)
64 			return;
65 
66 		BundleInfo[] expectedState = Utils.getBundleInfosFromList(bundleInfoList);
67 
68 		// check for an update to the system bundle
69 		String systemBundleSymbolicName = manipulatingContext.getBundle(0).getSymbolicName();
70 		Version systemBundleVersion = manipulatingContext.getBundle(0).getVersion();
71 		if (systemBundleSymbolicName != null) {
72 			for (BundleInfo element : expectedState) {
73 				String symbolicName = element.getSymbolicName();
74 				if (!systemBundleSymbolicName.equals(symbolicName))
75 					continue;
76 
77 				Version version = Version.parseVersion(element.getVersion());
78 				if (!systemBundleVersion.equals(version))
79 					throw new IllegalStateException("The System Bundle was updated. The framework must be restarted to finalize the configuration change");
80 			}
81 		}
82 
83 		HashSet<BundleInfo> toUninstall = null;
84 		if (!exclusiveMode) {
85 			BundleInfo[] lastInstalledBundles = getLastState();
86 			if (lastInstalledBundles != null) {
87 				toUninstall = new HashSet<>(Arrays.asList(lastInstalledBundles));
88 				toUninstall.removeAll(Arrays.asList(expectedState));
89 			}
90 			saveStateAsLast(url);
91 		}
92 
93 		Set<Bundle> prevouslyResolved = getResolvedBundles();
94 		Collection<Bundle> toRefresh = new ArrayList<>();
95 		Collection<Bundle> toStart = new ArrayList<>();
96 		if (exclusiveMode) {
97 			toRefresh.addAll(installBundles(expectedState, toStart));
98 			toRefresh.addAll(uninstallBundles(expectedState, packageAdminService));
99 		} else {
100 			toRefresh.addAll(installBundles(expectedState, toStart));
101 			if (toUninstall != null)
102 				toRefresh.addAll(uninstallBundles(toUninstall));
103 		}
104 		refreshPackages(toRefresh.toArray(new Bundle[toRefresh.size()]), manipulatingContext);
105 		if (toRefresh.size() > 0) {
106 			Bundle[] additionalRefresh = getAdditionalRefresh(prevouslyResolved, toRefresh);
107 			if (additionalRefresh.length > 0)
108 				refreshPackages(additionalRefresh, manipulatingContext);
109 		}
110 		startBundles(toStart.toArray(new Bundle[toStart.size()]));
111 	}
112 
getAdditionalRefresh(Set<Bundle> previouslyResolved, Collection<Bundle> toRefresh)113 	private Bundle[] getAdditionalRefresh(Set<Bundle> previouslyResolved, Collection<Bundle> toRefresh) {
114 		// This is the luna equinox framework or a non-equinox framework.
115 		// Use standard OSGi API.
116 		final Set<Bundle> additionalRefresh = new HashSet<>();
117 		final Set<Bundle> originalRefresh = new HashSet<>(toRefresh);
118 		for (Bundle bundle : toRefresh) {
119 			BundleRevision revision = bundle.adapt(BundleRevision.class);
120 			if (bundle.getState() == Bundle.INSTALLED && revision != null && (revision.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0) {
121 				// this is an unresolved fragment; look to see if it has additional payload requirements
122 				boolean foundPayLoadReq = false;
123 				BundleRequirement hostReq = null;
124 				Collection<Requirement> requirements = revision.getRequirements(null);
125 				for (Requirement requirement : requirements) {
126 					BundleRequirement req = (BundleRequirement) requirement;
127 					if (HostNamespace.HOST_NAMESPACE.equals(req.getNamespace())) {
128 						hostReq = req;
129 					}
130 					if (!HostNamespace.HOST_NAMESPACE.equals(req.getNamespace()) && !ExecutionEnvironmentNamespace.EXECUTION_ENVIRONMENT_NAMESPACE.equals(req.getNamespace())) {
131 						// found a payload requirement
132 						foundPayLoadReq = true;
133 					}
134 				}
135 				if (foundPayLoadReq) {
136 					Collection<BundleCapability> candidates = frameworkWiring.findProviders(hostReq);
137 					for (BundleCapability candidate : candidates) {
138 						if (!originalRefresh.contains(candidate.getRevision().getBundle())) {
139 							additionalRefresh.add(candidate.getRevision().getBundle());
140 						}
141 					}
142 				}
143 			}
144 		}
145 
146 		for (Bundle bundle : previouslyResolved) {
147 			BundleRevision revision = bundle.adapt(BundleRevision.class);
148 			BundleWiring wiring = revision == null ? null : revision.getWiring();
149 			if (wiring != null) {
150 				Collection<BundleRequirement> reqs = revision.getDeclaredRequirements(null);
151 				Set<BundleRequirement> optionalReqs = new HashSet<>();
152 				for (BundleRequirement req : reqs) {
153 					String namespace = req.getNamespace();
154 					// only do this for package and bundle namespaces
155 					if (PackageNamespace.PACKAGE_NAMESPACE.equals(namespace) || BundleNamespace.BUNDLE_NAMESPACE.equals(namespace)) {
156 						if (Namespace.RESOLUTION_OPTIONAL.equals(req.getDirectives().get(Namespace.REQUIREMENT_RESOLUTION_DIRECTIVE))) {
157 							optionalReqs.add(req);
158 						}
159 					}
160 				}
161 				if (!optionalReqs.isEmpty()) {
162 					wiring = getHostWiring(wiring);
163 					// check that all optional requirements are wired
164 					Collection<BundleWire> requiredWires = wiring.getRequiredWires(null);
165 					for (BundleWire requiredWire : requiredWires) {
166 						optionalReqs.remove(requiredWire.getRequirement());
167 					}
168 					if (!optionalReqs.isEmpty()) {
169 						// there are a number of optional requirements not wired
170 						for (BundleRequirement bundleRequirement : optionalReqs) {
171 							Collection<BundleCapability> candidates = frameworkWiring.findProviders(bundleRequirement);
172 							// Filter out candidates that were previously resolved or are currently not resolved.
173 							// There is no need to refresh the resource if the candidate was previously available.
174 							for (Iterator<BundleCapability> iCandidates = candidates.iterator(); iCandidates.hasNext();) {
175 								BundleCapability candidate = iCandidates.next();
176 								Bundle candidateBundle = candidate.getRevision().getBundle();
177 								// The candidate is not from the original refresh set, but
178 								// it could have just became resolved as a result of new bundles.
179 								if (previouslyResolved.contains(candidateBundle) || candidateBundle.getState() == Bundle.INSTALLED) {
180 									iCandidates.remove();
181 								}
182 							}
183 							if (!candidates.isEmpty()) {
184 								additionalRefresh.add(wiring.getBundle());
185 								break;
186 							}
187 						}
188 					}
189 				}
190 			}
191 		}
192 		return additionalRefresh.toArray(new Bundle[additionalRefresh.size()]);
193 	}
194 
getHostWiring(BundleWiring wiring)195 	private BundleWiring getHostWiring(BundleWiring wiring) {
196 		if ((wiring.getRevision().getTypes() & BundleRevision.TYPE_FRAGMENT) == 0) {
197 			// not a fragment
198 			return wiring;
199 		}
200 		Collection<BundleWire> hostWires = wiring.getRequiredWires(HostNamespace.HOST_NAMESPACE);
201 		// just use the first host wiring
202 		if (hostWires.isEmpty()) {
203 			return wiring;
204 		}
205 		BundleWire hostWire = hostWires.iterator().next();
206 		return hostWire.getProviderWiring();
207 	}
208 
getResolvedBundles()209 	private Set<Bundle> getResolvedBundles() {
210 		Set<Bundle> resolved = new HashSet<>();
211 		Bundle[] allBundles = manipulatingContext.getBundles();
212 		for (Bundle bundle : allBundles)
213 			if ((bundle.getState() & (Bundle.INSTALLED | Bundle.UNINSTALLED)) == 0)
214 				resolved.add(bundle);
215 		return resolved;
216 	}
217 
uninstallBundles(HashSet<BundleInfo> toUninstall)218 	private Collection<Bundle> uninstallBundles(HashSet<BundleInfo> toUninstall) {
219 		Collection<Bundle> removedBundles = new ArrayList<>(toUninstall.size());
220 		for (BundleInfo current : toUninstall) {
221 			Bundle[] matchingBundles = packageAdminService.getBundles(current.getSymbolicName(), getVersionRange(current.getVersion()));
222 			for (int j = 0; matchingBundles != null && j < matchingBundles.length; j++) {
223 				try {
224 					removedBundles.add(matchingBundles[j]);
225 					matchingBundles[j].uninstall();
226 				} catch (BundleException e) {
227 					//TODO log in debug mode...
228 				}
229 			}
230 		}
231 		return removedBundles;
232 	}
233 
saveStateAsLast(URL url)234 	private void saveStateAsLast(URL url) {
235 
236 		File lastBundlesTxt = getLastBundleInfo();
237 		try (OutputStream destinationStream = new FileOutputStream(lastBundlesTxt)) {
238 			ArrayList<File> sourcesLocation = SimpleConfiguratorUtils.getInfoFiles();
239 			List<InputStream> sourceStreams = new ArrayList<>(sourcesLocation.size() + 1);
240 			sourceStreams.add(url.openStream());
241 			if (Activator.EXTENDED) {
242 				for (File source : sourcesLocation) {
243 					sourceStreams.add(new FileInputStream(source));
244 				}
245 			}
246 			SimpleConfiguratorUtils.transferStreams(sourceStreams, destinationStream);
247 		} catch (URISyntaxException e) {
248 			// nothing, was discovered when starting framework
249 		} catch (IOException e) {
250 			//nothing
251 		}
252 	}
253 
getLastBundleInfo()254 	private File getLastBundleInfo() {
255 		return manipulatingContext.getDataFile(LAST_BUNDLES_INFO);
256 	}
257 
getLastState()258 	private BundleInfo[] getLastState() {
259 		File lastBundlesInfo = getLastBundleInfo();
260 		if (!lastBundlesInfo.isFile())
261 			return null;
262 		try {
263 			return SimpleConfiguratorUtils.readConfiguration(lastBundlesInfo.toURL(), baseLocation).toArray(new BundleInfo[1]);
264 		} catch (IOException e) {
265 			return null;
266 		}
267 	}
268 
installBundles(BundleInfo[] finalList, Collection<Bundle> toStart)269 	private ArrayList<Bundle> installBundles(BundleInfo[] finalList, Collection<Bundle> toStart) {
270 		ArrayList<Bundle> toRefresh = new ArrayList<>();
271 
272 		String useReferenceProperty = manipulatingContext.getProperty(SimpleConfiguratorConstants.PROP_KEY_USE_REFERENCE);
273 		boolean useReference = useReferenceProperty == null ? runningOnEquinox : Boolean.parseBoolean(useReferenceProperty);
274 
275 		for (BundleInfo element : finalList) {
276 			if (element == null)
277 				continue;
278 			//TODO here we do not deal with bundles that don't have a symbolic id
279 			//TODO Need to handle the case where getBundles return multiple value
280 
281 			String symbolicName = element.getSymbolicName();
282 			String version = element.getVersion();
283 
284 			Bundle[] matches = null;
285 			if (symbolicName != null && version != null)
286 				matches = packageAdminService.getBundles(symbolicName, getVersionRange(version));
287 
288 			String bundleLocation = SimpleConfiguratorUtils.getBundleLocation(element, useReference);
289 
290 			Bundle current = matches == null ? null : (matches.length == 0 ? null : matches[0]);
291 			if (current == null) {
292 				try {
293 					current = manipulatingContext.installBundle(bundleLocation);
294 					if (symbolicName != null && version != null) {
295 						Version v;
296 						try {
297 							v = new Version(version);
298 							if (!symbolicName.equals(current.getSymbolicName()) || !v.equals(current.getVersion())) {
299 								// can happen if, for example, the new version of the bundle is installed
300 								// to the same bundle location as the old version
301 								current.update();
302 							}
303 						} catch (IllegalArgumentException e) {
304 							// invalid version string; should log
305 							if (Activator.DEBUG)
306 								e.printStackTrace();
307 						}
308 					}
309 
310 					if (Activator.DEBUG)
311 						System.out.println("installed bundle:" + element); //$NON-NLS-1$
312 					toRefresh.add(current);
313 				} catch (BundleException e) {
314 					if (Activator.DEBUG) {
315 						System.err.println("Can't install " + symbolicName + '/' + version + " from location " + element.getLocation()); //$NON-NLS-1$ //$NON-NLS-2$
316 						e.printStackTrace();
317 					}
318 					continue;
319 				}
320 			} else if (inDevMode && current.getBundleId() != 0 && current != manipulatingContext.getBundle() && !bundleLocation.equals(current.getLocation()) && !current.getLocation().startsWith("initial@")) {
321 				// We do not do this for the system bundle (id==0), the manipulating bundle or any bundle installed from the osgi.bundles list (locations starting with "@initial"
322 				// The bundle exists; but the location is different. Uninstall the current and install the new one (bug 229700)
323 				try {
324 					current.uninstall();
325 					toRefresh.add(current);
326 				} catch (BundleException e) {
327 					if (Activator.DEBUG) {
328 						System.err.println("Can't uninstall " + symbolicName + '/' + version + " from location " + current.getLocation()); //$NON-NLS-1$ //$NON-NLS-2$
329 						e.printStackTrace();
330 					}
331 					continue;
332 				}
333 				try {
334 					current = manipulatingContext.installBundle(bundleLocation);
335 					if (Activator.DEBUG)
336 						System.out.println("installed bundle:" + element); //$NON-NLS-1$
337 					toRefresh.add(current);
338 				} catch (BundleException e) {
339 					if (Activator.DEBUG) {
340 						System.err.println("Can't install " + symbolicName + '/' + version + " from location " + element.getLocation()); //$NON-NLS-1$ //$NON-NLS-2$
341 						e.printStackTrace();
342 					}
343 					continue;
344 				}
345 			}
346 
347 			// Mark Started
348 			if (element.isMarkedAsStarted()) {
349 				toStart.add(current);
350 			}
351 
352 			// Set Start Level
353 			int startLevel = element.getStartLevel();
354 			if (startLevel < 1)
355 				continue;
356 			if (current.getBundleId() == 0)
357 				continue;
358 			if (isFragment(current))
359 				continue;
360 			if (SimpleConfiguratorConstants.TARGET_CONFIGURATOR_NAME.equals(current.getSymbolicName()))
361 				continue;
362 
363 			try {
364 				current.adapt(BundleStartLevel.class).setStartLevel(startLevel);
365 			} catch (IllegalArgumentException ex) {
366 				Utils.log(4, null, null, "Failed to set start level of Bundle:" + element, ex); //$NON-NLS-1$
367 			}
368 		}
369 		return toRefresh;
370 	}
371 
isFragment(Bundle current)372 	private boolean isFragment(Bundle current) {
373 		BundleRevision revision = current.adapt(BundleRevision.class);
374 		return (revision != null) && ((revision.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0);
375 	}
376 
refreshPackages(Bundle[] bundles, BundleContext context)377 	private void refreshPackages(Bundle[] bundles, BundleContext context) {
378 		if (bundles.length == 0 || packageAdminService == null)
379 			return;
380 
381 		// Prior to Luna the Equinox framework would refresh all bundles with the same
382 		// BSN automatically.  This is no longer the case for Luna or other framework
383 		// implementations.  Here we want to make sure all existing bundles with the
384 		// same BSN are refreshed also.
385 		Set<Bundle> allSameBSNs = new LinkedHashSet<>(); // maintain order and avoid duplicates
386 		for (Bundle bundle : bundles) {
387 			allSameBSNs.add(bundle);
388 			String bsn = bundle.getSymbolicName();
389 			if (bsn != null) {
390 				// look for others with same BSN
391 				Bundle[] sameBSNs = packageAdminService.getBundles(bsn, null);
392 				if (sameBSNs != null) {
393 					// likely contains the bundle we just added above but a set is used
394 					allSameBSNs.addAll(Arrays.asList(sameBSNs));
395 				}
396 			}
397 		}
398 
399 		CountDownLatch latch = new CountDownLatch(1);
400 		FrameworkListener listener = event -> {
401 			if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) {
402 				latch.countDown();
403 			}
404 		};
405 		context.addFrameworkListener(listener);
406 		packageAdminService.refreshPackages(allSameBSNs.toArray(new Bundle[0]));
407 
408 		try {
409 			latch.await();
410 		} catch (InterruptedException e) {
411 			// ignore
412 		}
413 
414 		//		if (DEBUG) {
415 		//			for (int i = 0; i < bundles.length; i++) {
416 		//				System.out.println(SimpleConfiguratorUtils.getBundleStateString(bundles[i]));
417 		//			}
418 		//		}
419 		context.removeFrameworkListener(listener);
420 	}
421 
startBundles(Bundle[] bundles)422 	private void startBundles(Bundle[] bundles) {
423 		for (Bundle bundle : bundles) {
424 			if (bundle.getState() == Bundle.UNINSTALLED) {
425 				System.err.println("Could not start: " + bundle.getSymbolicName() + '(' + bundle.getLocation() + ':' + bundle.getBundleId() + ')' + ". It's state is uninstalled.");
426 				continue;
427 			}
428 			if (bundle.getState() == Bundle.STARTING && (bundle == callingBundle || bundle == manipulatingContext.getBundle()))
429 				continue;
430 			if (isFragment(bundle))
431 				continue;
432 			if (bundle.getBundleId() == 0)
433 				continue;
434 
435 			try {
436 				bundle.start();
437 				if (Activator.DEBUG)
438 					System.out.println("started Bundle:" + bundle.getSymbolicName() + '(' + bundle.getLocation() + ':' + bundle.getBundleId() + ')'); //$NON-NLS-1$
439 			} catch (BundleException e) {
440 				e.printStackTrace();
441 				//				FrameworkLogEntry entry = new FrameworkLogEntry(FrameworkAdaptor.FRAMEWORK_SYMBOLICNAME, NLS.bind(EclipseAdaptorMsg.ECLIPSE_STARTUP_FAILED_START, bundle.getLocation()), 0, e, null);
442 				//				log.log(entry);
443 			}
444 		}
445 	}
446 
447 	/**
448 	 * Uninstall bundles which are not listed on finalList.
449 	 *
450 	 * @param finalList bundles list not to be uninstalled.
451 	 * @param packageAdmin package admin service.
452 	 * @return Collection HashSet of bundles finally installed.
453 	 */
uninstallBundles(BundleInfo[] finalList, PackageAdmin packageAdmin)454 	private Collection<Bundle> uninstallBundles(BundleInfo[] finalList, PackageAdmin packageAdmin) {
455 		Bundle[] allBundles = manipulatingContext.getBundles();
456 
457 		//Build a set with all the bundles from the system
458 		Set<Bundle> removedBundles = new HashSet<>(allBundles.length);
459 		//		configurator.setPrerequisiteBundles(allBundles);
460 		for (Bundle allBundle : allBundles) {
461 			if (allBundle.getBundleId() == 0) {
462 				continue;
463 			}
464 			removedBundles.add(allBundle);
465 		}
466 
467 		//Remove all the bundles appearing in the final list from the set of installed bundles
468 		for (BundleInfo element : finalList) {
469 			if (element == null)
470 				continue;
471 			Bundle[] toAdd = packageAdmin.getBundles(element.getSymbolicName(), getVersionRange(element.getVersion()));
472 			for (int j = 0; toAdd != null && j < toAdd.length; j++) {
473 				removedBundles.remove(toAdd[j]);
474 			}
475 		}
476 
477 		for (Iterator<Bundle> iter = removedBundles.iterator(); iter.hasNext();) {
478 			try {
479 				Bundle bundle = iter.next();
480 				if (bundle.getLocation().startsWith("initial@")) {
481 					if (Activator.DEBUG)
482 						System.out.println("Simple configurator thinks a bundle installed by the boot strap should be uninstalled:" + bundle.getSymbolicName() + '(' + bundle.getLocation() + ':' + bundle.getBundleId() + ')'); //$NON-NLS-1$
483 					// Avoid uninstalling bundles that the boot strap code thinks should be installed (bug 232191)
484 					iter.remove();
485 					continue;
486 				}
487 				bundle.uninstall();
488 				if (Activator.DEBUG)
489 					System.out.println("uninstalled Bundle:" + bundle.getSymbolicName() + '(' + bundle.getLocation() + ':' + bundle.getBundleId() + ')'); //$NON-NLS-1$
490 			} catch (BundleException e) {
491 				// TODO Auto-generated catch block
492 				e.printStackTrace();
493 			}
494 		}
495 
496 		return removedBundles;
497 	}
498 
getVersionRange(String version)499 	private String getVersionRange(String version) {
500 		return version == null ? null : new StringBuilder().append('[').append(version).append(',').append(version).append(']').toString();
501 	}
502 }
503