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