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)