1 package com.atlassian.plugin.osgi.factory;
2
3 import com.atlassian.plugin.IllegalPluginStateException;
4 import com.atlassian.plugin.InstallationMode;
5 import com.atlassian.plugin.ModuleDescriptor;
6 import com.atlassian.plugin.Plugin;
7 import com.atlassian.plugin.PluginArtifact;
8 import com.atlassian.plugin.PluginDependencies;
9 import com.atlassian.plugin.PluginState;
10 import com.atlassian.plugin.event.PluginEventListener;
11 import com.atlassian.plugin.event.PluginEventManager;
12 import com.atlassian.plugin.event.events.PluginContainerFailedEvent;
13 import com.atlassian.plugin.event.events.PluginContainerRefreshedEvent;
14 import com.atlassian.plugin.event.events.PluginFrameworkShutdownEvent;
15 import com.atlassian.plugin.event.events.PluginFrameworkShuttingDownEvent;
16 import com.atlassian.plugin.event.events.PluginFrameworkStartedEvent;
17 import com.atlassian.plugin.event.events.PluginRefreshedEvent;
18 import com.atlassian.plugin.impl.AbstractPlugin;
19 import com.atlassian.plugin.module.ContainerAccessor;
20 import com.atlassian.plugin.module.ContainerManagedPlugin;
21 import com.atlassian.plugin.osgi.container.OsgiContainerException;
22 import com.atlassian.plugin.osgi.container.OsgiContainerManager;
23 import com.atlassian.plugin.osgi.event.PluginServiceDependencyWaitEndedEvent;
24 import com.atlassian.plugin.osgi.event.PluginServiceDependencyWaitStartingEvent;
25 import com.atlassian.plugin.osgi.event.PluginServiceDependencyWaitTimedOutEvent;
26 import com.atlassian.plugin.osgi.external.ListableModuleDescriptorFactory;
27 import com.atlassian.plugin.util.PluginUtils;
28 import com.google.common.annotations.VisibleForTesting;
29 import org.dom4j.Element;
30 import org.osgi.framework.Bundle;
31 import org.osgi.framework.BundleContext;
32 import org.osgi.framework.BundleEvent;
33 import org.osgi.framework.BundleException;
34 import org.osgi.framework.BundleListener;
35 import org.osgi.framework.ServiceReference;
36 import org.osgi.framework.SynchronousBundleListener;
37 import org.osgi.framework.namespace.PackageNamespace;
38 import org.osgi.service.packageadmin.PackageAdmin;
39 import org.osgi.util.tracker.ServiceTracker;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 import javax.annotation.Nonnull;
44 import java.io.InputStream;
45 import java.net.URL;
46 import java.util.Date;
47 import java.util.HashMap;
48 import java.util.Map;
49 import java.util.Set;
50 import java.util.concurrent.CopyOnWriteArraySet;
51
52 import static com.google.common.base.Preconditions.checkNotNull;
53
54
55
56
57
58
59
60
61
62
63
64
65 public class OsgiPlugin extends AbstractPlugin implements OsgiBackedPlugin, ContainerManagedPlugin {
66 private static final Logger log = LoggerFactory.getLogger(OsgiPlugin.class);
67
68
69
70
71 public static final String ATLASSIAN_PLUGIN_KEY = "Atlassian-Plugin-Key";
72
73
74
75
76 public static final String ATLASSIAN_SCAN_FOLDERS = "Atlassian-Scan-Folders";
77
78
79
80
81 public static final String REMOTE_PLUGIN_KEY = "Remote-Plugin";
82
83 private final Map<String, Element> moduleElements = new HashMap<>();
84 private final PluginEventManager pluginEventManager;
85 private final PackageAdmin packageAdmin;
86 private final Set<OutstandingDependency> outstandingDependencies = new CopyOnWriteArraySet<>();
87 private final BundleListener bundleStartStopListener;
88
89 private volatile boolean treatPluginContainerCreationAsRefresh = false;
90 private volatile OsgiPluginHelper helper;
91
92
93
94
95
96
97 private volatile boolean frameworkStarted = false;
98
99 private volatile boolean frameworkShuttingDown = false;
100
101 public OsgiPlugin(final String key, final OsgiContainerManager mgr, final PluginArtifact artifact, final PluginArtifact originalPluginArtifact, final PluginEventManager pluginEventManager) {
102 super(checkNotNull(originalPluginArtifact));
103 this.pluginEventManager = checkNotNull(pluginEventManager);
104
105 this.helper = new OsgiPluginUninstalledHelper(
106 checkNotNull(key, "The plugin key is required"),
107 checkNotNull(mgr, "The osgi container is required"),
108 checkNotNull(artifact, "The plugin artifact is required"));
109 this.packageAdmin = extractPackageAdminFromOsgi(mgr);
110 super.setKey(key);
111
112 this.bundleStartStopListener = (SynchronousBundleListener) bundleEvent -> {
113 if (bundleEvent.getBundle() == getBundle()) {
114 if (bundleEvent.getType() == BundleEvent.STOPPING) {
115 helper.onDisable();
116 setPluginState(PluginState.DISABLED);
117 } else if (bundleEvent.getType() == BundleEvent.STARTED) {
118 final BundleContext ctx = getBundle().getBundleContext();
119 helper.onEnable(createServiceTrackers(ctx));
120 setPluginState(PluginState.ENABLED);
121 }
122 }
123 };
124 }
125
126
127
128
129
130
131
132
133 @VisibleForTesting
134 OsgiPlugin(final String key, final PluginEventManager pluginEventManager, final OsgiPluginHelper helper,
135 final PackageAdmin packageAdmin) {
136 super(null);
137 this.helper = helper;
138 this.pluginEventManager = pluginEventManager;
139 this.packageAdmin = packageAdmin;
140 this.bundleStartStopListener = null;
141 super.setKey(checkNotNull(key, "The plugin key is required"));
142 }
143
144 @Override
145 public Bundle getBundle() throws IllegalPluginStateException {
146 return helper.getBundle();
147 }
148
149 @Override
150 public InstallationMode getInstallationMode() {
151 return helper.isRemotePlugin() ? InstallationMode.REMOTE : InstallationMode.LOCAL;
152 }
153
154
155
156
157 public boolean isUninstallable() {
158 return true;
159 }
160
161
162
163
164 public boolean isDynamicallyLoaded() {
165 return true;
166 }
167
168
169
170
171 public boolean isDeleteable() {
172 return true;
173 }
174
175
176 @Override
177 public Date getDateInstalled() {
178 long date = getPluginArtifact().toFile().lastModified();
179 if (date == 0) {
180 date = getDateLoaded().getTime();
181 }
182 return new Date(date);
183 }
184
185
186
187
188
189 @Override
190 public void setKey(final String key){
191
192 if(! getKey().equals(key))
193 throw new IllegalArgumentException("setKey() should not be called after initialization.");
194 }
195
196
197
198
199
200
201
202
203
204 public <T> Class<T> loadClass(final String clazz, final Class<?> callingClass) throws ClassNotFoundException, IllegalPluginStateException {
205 return helper.loadClass(clazz, callingClass);
206 }
207
208
209
210
211
212
213 public URL getResource(final String name) throws IllegalPluginStateException {
214 return helper.getResource(name);
215 }
216
217
218
219
220
221
222 public InputStream getResourceAsStream(final String name) throws IllegalPluginStateException {
223 return helper.getResourceAsStream(name);
224 }
225
226
227
228
229
230 public ClassLoader getClassLoader() throws IllegalPluginStateException {
231 return helper.getClassLoader();
232 }
233
234
235
236
237
238
239
240
241 @PluginEventListener
242 public void onPluginContainerFailed(final PluginContainerFailedEvent event) throws IllegalPluginStateException {
243 if (getKey() == null) {
244 throw new IllegalPluginStateException("Plugin key must be set");
245 }
246 if (getKey().equals(event.getPluginKey())) {
247 logAndClearOustandingDependencies();
248 log.error("Unable to start the plugin container for plugin '{}'", getKey(), event.getCause());
249 disable();
250 }
251 }
252
253 @PluginEventListener
254 public void onPluginFrameworkStartedEvent(final PluginFrameworkStartedEvent event) {
255 frameworkStarted = true;
256 }
257
258 @PluginEventListener
259 public void onPluginFrameworkShutdownEvent(final PluginFrameworkShutdownEvent event) {
260 frameworkStarted = false;
261 }
262
263 @PluginEventListener
264 public void onPluginFrameworkShuttingDownEvent(final PluginFrameworkShuttingDownEvent event) {
265 frameworkShuttingDown = true;
266 }
267
268
269
270
271 boolean isFrameworkShuttingDown() {
272 return frameworkShuttingDown;
273 }
274
275 @PluginEventListener
276 public void onServiceDependencyWaitStarting(final PluginServiceDependencyWaitStartingEvent event) {
277 if (event.getPluginKey() != null && event.getPluginKey().equals(getKey())) {
278 final OutstandingDependency dep = new OutstandingDependency(event.getBeanName(), String.valueOf(event.getFilter()));
279 outstandingDependencies.add(dep);
280 log.info("Plugin '{}' waiting for {}", getKey(), dep);
281 }
282 }
283
284 @PluginEventListener
285 public void onServiceDependencyWaitEnded(final PluginServiceDependencyWaitEndedEvent event) {
286 if (event.getPluginKey() != null && event.getPluginKey().equals(getKey())) {
287 final OutstandingDependency dep = new OutstandingDependency(event.getBeanName(), String.valueOf(event.getFilter()));
288 outstandingDependencies.remove(dep);
289 log.info("Plugin '{}' found {}", getKey(), dep);
290 }
291 }
292
293 @PluginEventListener
294 public void onServiceDependencyWaitEnded(final PluginServiceDependencyWaitTimedOutEvent event) {
295 if (event.getPluginKey() != null && event.getPluginKey().equals(getKey())) {
296 final OutstandingDependency dep = new OutstandingDependency(event.getBeanName(), String.valueOf(event.getFilter()));
297 outstandingDependencies.remove(dep);
298 log.error("Plugin '{}' timeout waiting for {}", getKey(), dep);
299 }
300 }
301
302
303
304
305
306
307
308
309
310 @PluginEventListener
311 public void onPluginContainerRefresh(final PluginContainerRefreshedEvent event) throws IllegalPluginStateException {
312 if (getKey() == null) {
313 throw new IllegalPluginStateException("Plugin key must be set");
314 }
315 if (getKey().equals(event.getPluginKey())) {
316 outstandingDependencies.clear();
317 helper.setPluginContainer(event.getContainer());
318 if (!compareAndSetPluginState(PluginState.ENABLING, PluginState.ENABLED) && getPluginState() != PluginState.ENABLED) {
319 log.warn("Ignoring the bean container that was just created for plugin " + getKey() + ". The plugin " +
320 "is in an invalid state, " + getPluginState() + ", that doesn't support a transition to " +
321 "enabled. Most likely, it was disabled due to a timeout.");
322 helper.setPluginContainer(null);
323 return;
324 }
325
326
327 if (treatPluginContainerCreationAsRefresh) {
328 pluginEventManager.broadcast(new PluginRefreshedEvent(this));
329 } else {
330 treatPluginContainerCreationAsRefresh = true;
331 }
332 }
333 }
334
335
336
337
338
339
340
341
342
343
344
345
346 @Nonnull
347 @Override
348 public PluginDependencies getDependencies() {
349 return helper.getDependencies();
350 }
351
352 @Override
353 public String toString() {
354 return getKey();
355 }
356
357
358
359
360
361
362 @Override
363 protected void installInternal() throws IllegalPluginStateException {
364 log.debug("Installing OSGi plugin '{}'", getKey());
365 final Bundle bundle = helper.install();
366 helper = new OsgiPluginInstalledHelper(bundle, packageAdmin);
367 }
368
369
370
371
372
373
374
375
376
377
378 @Override
379 protected synchronized PluginState enableInternal() throws OsgiContainerException, IllegalPluginStateException {
380 log.debug("Enabling OSGi plugin '{}'", getKey());
381 try {
382 final PluginState stateResult;
383 if (getBundle().getState() == Bundle.ACTIVE) {
384 log.debug("Plugin '{}' bundle is already active, not doing anything", getKey());
385 stateResult = PluginState.ENABLED;
386 } else if ((getBundle().getState() == Bundle.RESOLVED) || (getBundle().getState() == Bundle.INSTALLED)) {
387 pluginEventManager.register(this);
388 if (!treatPluginContainerCreationAsRefresh) {
389
390 setPluginState(PluginState.ENABLING);
391
392 stateResult = PluginState.PENDING;
393 } else {
394 stateResult = PluginState.ENABLED;
395 }
396 log.debug("Plugin '{}' bundle is resolved or installed, starting.", getKey());
397 getBundle().start();
398 final BundleContext ctx = getBundle().getBundleContext();
399 helper.onEnable(createServiceTrackers(ctx));
400
401
402 ctx.addBundleListener(bundleStartStopListener);
403 } else {
404 throw new OsgiContainerException("Cannot enable the plugin '" + getKey() + "' when the bundle is not in the resolved or installed state: "
405 + getBundle().getState() + "(" + getBundle().getBundleId() + ")");
406 }
407
408
409 return stateResult;
410 } catch (final BundleException e) {
411 log.error("Detected an error (BundleException) enabling the plugin '" + getKey() + "' : " + e.getMessage() + ". " +
412 " This error usually occurs when your plugin imports a package from another bundle with a specific version constraint " +
413 "and either the bundle providing that package doesn't meet those version constraints, or there is no bundle " +
414 "available that provides the specified package. For more details on how to fix this, see " +
415 "https://developer.atlassian.com/x/mQAN");
416 throw new OsgiContainerException("Cannot start plugin: " + getKey(), e);
417 }
418 }
419
420 private ServiceTracker[] createServiceTrackers(final BundleContext ctx) {
421 return new ServiceTracker[]{
422 new ServiceTracker(ctx, ModuleDescriptor.class.getName(),
423 new ModuleDescriptorServiceTrackerCustomizer(this, pluginEventManager)),
424 new ServiceTracker(ctx, ListableModuleDescriptorFactory.class.getName(),
425 new UnrecognizedModuleDescriptorServiceTrackerCustomizer(this, pluginEventManager))
426 };
427 }
428
429
430
431
432
433
434
435 @Override
436 protected synchronized void disableInternal() throws OsgiContainerException, IllegalPluginStateException {
437
438 if (!requiresRestart()) {
439 try {
440 if (getPluginState() == PluginState.DISABLING) {
441 logAndClearOustandingDependencies();
442 }
443 helper.onDisable();
444 pluginEventManager.unregister(this);
445 getBundle().stop();
446 treatPluginContainerCreationAsRefresh = false;
447 } catch (final BundleException e) {
448 log.error("Detected an error (BundleException) disabling the plugin '" + getKey() + "' : " + e.getMessage() + ".");
449 throw new OsgiContainerException("Cannot stop plugin: " + getKey(), e);
450 }
451 }
452 }
453
454 private boolean requiresRestart() {
455 return frameworkStarted && PluginUtils.doesPluginRequireRestart(this);
456 }
457
458 private void logAndClearOustandingDependencies() {
459 for (final OutstandingDependency dep : outstandingDependencies) {
460 log.error("Plugin '{}' never resolved {}", getKey(), dep);
461 }
462 outstandingDependencies.clear();
463 }
464
465
466
467
468
469
470
471 @Override
472 protected void uninstallInternal() throws OsgiContainerException, IllegalPluginStateException {
473 final String key = getKey();
474
475 int retryCount = 0;
476 Exception rootCause = null;
477 final long sleepTime = 500L;
478
479 while (true) {
480 try {
481
482
483 pluginEventManager.unregister(this);
484 final Bundle bundle = getBundleIfInstalled();
485 if (null != bundle) {
486
487
488 final boolean remotePlugin = helper.isRemotePlugin();
489
490
491 if (bundle.getState() != Bundle.UNINSTALLED) {
492 bundle.uninstall();
493 } else {
494 log.warn("Bundle for '{}' already UNINSTALLED, but still held by helper '{}'", key, helper);
495 }
496
497 final OsgiPluginHelper oldHelper = helper;
498 helper = new OsgiPluginDeinstalledHelper(key, remotePlugin);
499 setPluginState(PluginState.UNINSTALLED);
500 oldHelper.onUninstall();
501 } else {
502 log.debug("Trying to uninstall '{}', but it is not installed (helper '{}')", key, helper);
503 }
504 break;
505
506 } catch (final BundleException e) {
507 rootCause = retryCount == 0 ? e : rootCause;
508 retryCount++;
509
510 if (retryCount < 3) {
511 log.debug("Possible transient fail on try {} to uninstall '{}', retrying in {} mSecs",
512 retryCount, key, sleepTime);
513 log.debug(e.getMessage(), e);
514 try {
515 Thread.sleep(sleepTime);
516 } catch (final InterruptedException ei) {
517 throw new OsgiContainerException(
518 "Cannot uninstall '" + key + "', retry sleep was interrupted: " + ei.getMessage());
519 }
520 } else {
521 log.error("Detected an error (BundleException) disabling the plugin '{}'.", key);
522 log.error(rootCause.getMessage(), rootCause);
523 throw new OsgiContainerException("Cannot uninstall '" + key + "'");
524 }
525 }
526 }
527 }
528
529
530
531
532
533
534
535
536
537 private Bundle getBundleIfInstalled() {
538
539
540 try {
541 return getBundle();
542 } catch (final IllegalPluginStateException eips) {
543 return null;
544 }
545 }
546
547
548
549
550
551
552
553 void addModuleDescriptorElement(final String key, final Element element) {
554 moduleElements.put(key, element);
555 }
556
557
558
559
560
561
562 void clearModuleDescriptor(final String key) {
563 removeModuleDescriptor(key);
564 }
565
566
567
568
569
570
571
572 Map<String, Element> getModuleElements() {
573 return moduleElements;
574 }
575
576
577
578
579
580
581
582 private PackageAdmin extractPackageAdminFromOsgi(final OsgiContainerManager mgr) {
583
584 final Bundle bundle = mgr.getBundles()[0];
585
586
587 final ServiceReference ref = bundle.getBundleContext()
588 .getServiceReference(PackageAdmin.class.getName());
589 return (PackageAdmin) bundle.getBundleContext()
590 .getService(ref);
591 }
592
593 public ContainerAccessor getContainerAccessor() {
594 return helper.getContainerAccessor();
595 }
596
597 @Override
598 public void resolve() {
599
600
601
602
603 packageAdmin.resolveBundles(new Bundle[]{getBundle()});
604 }
605
606 private static class OutstandingDependency {
607 private final String beanName;
608 private final String filter;
609
610 public OutstandingDependency(final String beanName, final String filter) {
611 this.beanName = beanName;
612 this.filter = filter;
613 }
614
615 @Override
616 public boolean equals(final Object o) {
617 if (this == o) {
618 return true;
619 }
620 if (o == null || getClass() != o.getClass()) {
621 return false;
622 }
623
624 final OutstandingDependency that = (OutstandingDependency) o;
625
626 if (beanName != null ? !beanName.equals(that.beanName) : that.beanName != null) {
627 return false;
628 }
629 return filter.equals(that.filter);
630 }
631
632 @Override
633 public int hashCode() {
634 int result = beanName != null ? beanName.hashCode() : 0;
635 result = 31 * result + filter.hashCode();
636 return result;
637 }
638
639 public String toString() {
640 return "service '" + beanName + "' with filter '" + filter + "'";
641 }
642 }
643 }