Adriano Machado created CAMEL-23835:
---------------------------------------

             Summary: camel-launcher: 'camel tui' fails with "No 
BackendProvider found" because embedded plugins are loaded without a classloader
                 Key: CAMEL-23835
                 URL: https://issues.apache.org/jira/browse/CAMEL-23835
             Project: Camel
          Issue Type: Bug
          Components: camel-jbang
    Affects Versions: 4.20.0
            Reporter: Adriano Machado
            Assignee: Adriano Machado


h2. Summary

Running the TUI from the {{camel-launcher}} fat-jar (e.g. {{./camel.sh tui}} or 
{{podman run camel-launcher tui}}) fails immediately with:

{code}
dev.tamboui.terminal.BackendException: No BackendProvider found on classpath.
Add a backend dependency such as tamboui-jline3-backend or 
tamboui-panama-backend.
    at dev.tamboui.terminal.BackendFactory.tryProviders(BackendFactory.java:139)
    at dev.tamboui.terminal.BackendFactory.create(BackendFactory.java:91)
    at dev.tamboui.tui.TuiRunner.create(TuiRunner.java:186)
    at 
org.apache.camel.dsl.jbang.core.commands.tui.TuiBackendHelper.createTuiRunner(TuiBackendHelper.java:36)
    at 
org.apache.camel.dsl.jbang.core.commands.tui.CamelMonitor.doCall(CamelMonitor.java:323)
    ...
{code}

The same command works when the CLI is installed via JBang. This is *not* a 
packaging problem: the backend jar ({{tamboui-jline3-backend}}) and its 
{{META-INF/services/dev.tamboui.terminal.BackendProvider}} SPI
entry are correctly bundled in the fat-jar under {{BOOT-INF/lib/}}.

h2. Affects

* Module: {{dsl/camel-jbang/camel-launcher}} (and any embedded/fat-jar plugin 
scenario)
* Version: 4.21.0-SNAPSHOT

h2. Steps to reproduce

# Build the launcher: {{mvn -o install -Dquickly -pl 
dsl/camel-jbang/camel-launcher -am}}
# Unpack {{camel-launcher-*-bin.zip}} and run {{./camel.sh tui}} (or {{podman 
run -it --rm camel-launcher tui}}).
# Observe the {{No BackendProvider found}} failure.

h2. Root cause

TamboUI discovers its terminal backend via 
{{ServiceLoader.load(BackendProvider.class)}}, which keys off the 
*thread-context classloader* (TCCL). To make this work, the TUI deliberately 
installs its own
classloader as the TCCL before starting:

{code:java}
// CamelMonitor.java:266-267
// to make ServiceLoader work with tamboui for downloaded JARs
Thread.currentThread().setContextClassLoader(classLoader);
{code}

That {{classLoader}} arrives as {{null}} for *embedded* (fat-jar) plugins, 
because the embedded-plugin loader never sets it. 
{{PluginHelper.loadPluginFromService(...)}} instantiates the plugin and calls
{{customize(...)}} but, unlike the two downloaded-plugin paths 
({{PluginHelper.java:344}} and {{:520}}, which both call 
{{instance.setClassLoader(...)}}), it omits the {{setClassLoader}} call. As a 
result
{{TuiPlugin.classLoader}} stays {{null}} → {{CamelMonitor.classLoader}} is 
{{null}} → the TCCL is set to {{null}}.

With a {{null}} TCCL, {{ServiceLoader}} falls back to the *system class 
loader*. In a Spring Boot fat-jar the system loader is {{AppClassLoader}}, 
which cannot see the nested {{BOOT-INF/lib/*.jar}} (only
{{LaunchedClassLoader}} can). The provider is never found. TamboUI's 
{{SafeServiceLoader.load(Class)}} then swallows the resulting 
{{ServiceConfigurationError}}/{{LinkageError}} (it passes a {{null}} error
handler) and reports the misleading "No BackendProvider found".

Proven directly inside the actual fat-jar:

{code}
probe loaded by: LaunchedClassLoader
system CL:       AppClassLoader
[TCCL=null]     SPI class not visible: ClassNotFoundException: 
dev.tamboui.terminal.BackendProvider  -> providers=-1
[TCCL=launched] providers=1
{code}

h2. Why JBang works

Under JBang the TUI plugin is a *downloaded* plugin, so it goes through a code 
path that calls {{setClassLoader(...)}} with a non-null classloader that chains 
to the TamboUI jars. The TCCL is therefore valid and
discovery succeeds.

h2. Fix

In {{PluginHelper.loadPluginFromService(...)}}, hand the loading classloader to 
the plugin before customizing it, mirroring the downloaded-plugin paths:

{code:java}
Plugin plugin = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
plugin.setClassLoader(classLoader);   // <-- added
{code}

{{Plugin.setClassLoader}} is a {{default}} no-op, so this is harmless for every 
other bundled plugin.

h2. Tests

Added {{PluginHelperTest#testEmbeddedPluginReceivesClassLoader}}, which drives 
{{loadPluginFromService}} with a capturing {{Plugin}} and asserts it receives 
the exact classloader used to load it. Verified the test
fails without the fix ({{expected: <1> but was: <0>}}) and passes with it.

h2. Notes

* The misleading error message originates upstream in TamboUI 
({{SafeServiceLoader.load(Class)}} discards the real 
{{LinkageError}}/{{ServiceConfigurationError}}). Worth reporting to TamboUI 
separately; not in
scope for this fix.
* No upgrade-guide entry is needed: this is a pure bug fix with nothing to 
migrate.

_Reported by Claude Code on behalf of Adriano Machado._



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to