Skip to content

Commit 6980427

Browse files
committed
LDEV-6221 use java.lang.reflect for inject call to bypass ClazzDynamic ASM bytecode load on stale bundle classloader
1 parent 7638a4c commit 6980427

2 files changed

Lines changed: 65 additions & 68 deletions

File tree

.github/workflows/main.yml

Lines changed: 53 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -486,68 +486,58 @@ jobs:
486486
echo "CONFIG_OVERRIDE_FAILED=true" >> $GITHUB_OUTPUT
487487
fi
488488
489-
# ------------------------------------------------------------------
490-
# Reflection / Lucee-restart scenario — TEMPORARILY DISABLED.
491-
#
492-
# The post-restart path hits a real bug: after cfadmin action="restart",
493-
# the new CFMLEngine's extension classloader can't load
494-
# JakartaWebSocketEndpoint.class — `WebSocketEndpointFactory.scanWebContexts`
495-
# and `registerEndpoint` both fail with "unable to load class path".
496-
# The reflection fallback never gets a chance to patch the old static slot
497-
# because the new classes themselves aren't loadable.
498-
#
499-
# The test CFMs (trigger-lucee-restart.cfm, test-reflection-restart.cfm)
500-
# and this workflow block are kept in place for when the underlying bug
501-
# is fixed — just re-enable the steps below.
502-
#
503-
# See LDEV-6221 and the companion catalina.out output in the earlier CI
504-
# run logs for the stack trace.
505-
# ------------------------------------------------------------------
506-
# - name: Trigger Lucee engine restart (LDEV-6221 scenario)
507-
# id: restart-trigger
508-
# continue-on-error: true
509-
# run: |
510-
# RESPONSE=$(curl -s -w "\n%{http_code}" http://localhost:8888/tests/integration/trigger-lucee-restart.cfm)
511-
# HTTP_CODE=$(echo "$RESPONSE" | tail -1)
512-
# BODY=$(echo "$RESPONSE" | sed '$d')
513-
# echo "$BODY"
514-
# if [ "$HTTP_CODE" != "200" ] || echo "$BODY" | grep -q "FAILED"; then
515-
# echo "RESTART_TRIGGER_FAILED=true" >> $GITHUB_OUTPUT
516-
# fi
517-
#
518-
# - name: Wait for Lucee to come back online
519-
# run: |
520-
# for i in {1..60}; do
521-
# if curl -s -o /dev/null -w "%{http_code}" http://localhost:8888/tests/test-websocket-info.cfm | grep -q "200"; then
522-
# echo "Lucee responding after $i attempts"
523-
# break
524-
# fi
525-
# sleep 1
526-
# done
527-
#
528-
# - name: Run post-restart reflection round-trip
529-
# id: reflection-test
530-
# continue-on-error: true
531-
# run: |
532-
# RESPONSE=$(curl -s -w "\n%{http_code}" http://localhost:8888/tests/integration/test-reflection-restart.cfm)
533-
# HTTP_CODE=$(echo "$RESPONSE" | tail -1)
534-
# BODY=$(echo "$RESPONSE" | sed '$d')
535-
# echo "$BODY"
536-
# if [ "$HTTP_CODE" != "200" ] || echo "$BODY" | grep -q "FAILED"; then
537-
# echo "REFLECTION_FAILED=true" >> $GITHUB_OUTPUT
538-
# fi
539-
#
540-
# - name: Verify reflection warning in catalina.out
541-
# id: reflection-log-check
542-
# continue-on-error: true
543-
# run: |
544-
# if grep -q "calling \[.*\] via reflection" lucee-express/logs/catalina.out; then
545-
# echo "Reflection warning found — hot re-install exercised the fallback path"
546-
# grep "calling \[.*\] via reflection" lucee-express/logs/catalina.out
547-
# else
548-
# echo "REFLECTION_LOG_MISSING=true" >> $GITHUB_OUTPUT
549-
# echo "Reflection warning NOT found in catalina.out — hot re-install did not hit the fallback path"
550-
# fi
489+
- name: Trigger Lucee engine restart (LDEV-6221 scenario)
490+
id: restart-trigger
491+
continue-on-error: true
492+
run: |
493+
# Calls cfadmin action="restart" — reloads the CFMLEngine while Tomcat
494+
# stays up. Forces the fresh extension to hit its reflection fallback
495+
# because Tomcat's endpoint registry still has the pre-restart class.
496+
RESPONSE=$(curl -s -w "\n%{http_code}" http://localhost:8888/tests/integration/trigger-lucee-restart.cfm)
497+
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
498+
BODY=$(echo "$RESPONSE" | sed '$d')
499+
echo "$BODY"
500+
if [ "$HTTP_CODE" != "200" ] || echo "$BODY" | grep -q "FAILED"; then
501+
echo "RESTART_TRIGGER_FAILED=true" >> $GITHUB_OUTPUT
502+
fi
503+
504+
- name: Wait for Lucee to come back online
505+
run: |
506+
# Engine restart takes a few seconds — poll a trivial endpoint until 200
507+
for i in {1..60}; do
508+
if curl -s -o /dev/null -w "%{http_code}" http://localhost:8888/tests/test-websocket-info.cfm | grep -q "200"; then
509+
echo "Lucee responding after $i attempts"
510+
break
511+
fi
512+
sleep 1
513+
done
514+
515+
- name: Run post-restart reflection round-trip
516+
id: reflection-test
517+
continue-on-error: true
518+
run: |
519+
RESPONSE=$(curl -s -w "\n%{http_code}" http://localhost:8888/tests/integration/test-reflection-restart.cfm)
520+
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
521+
BODY=$(echo "$RESPONSE" | sed '$d')
522+
echo "$BODY"
523+
if [ "$HTTP_CODE" != "200" ] || echo "$BODY" | grep -q "FAILED"; then
524+
echo "REFLECTION_FAILED=true" >> $GITHUB_OUTPUT
525+
fi
526+
527+
- name: Verify reflection warning in catalina.out
528+
id: reflection-log-check
529+
continue-on-error: true
530+
run: |
531+
# The reflection fallback path logs "calling [onOpen] via reflection, servlet engine restart needed"
532+
# (see BaseWebSocketEndpoint and LDEV-6221). If the hot re-install actually exercised this path,
533+
# the warning will appear in catalina.out. If not, the round-trip test is lying.
534+
if grep -q "calling \[.*\] via reflection" lucee-express/logs/catalina.out; then
535+
echo "Reflection warning found — hot re-install exercised the fallback path"
536+
grep "calling \[.*\] via reflection" lucee-express/logs/catalina.out
537+
else
538+
echo "REFLECTION_LOG_MISSING=true" >> $GITHUB_OUTPUT
539+
echo "Reflection warning NOT found in catalina.out — hot re-install did not hit the fallback path"
540+
fi
551541
552542
- name: Dump WebSocket event logs
553543
if: always()
@@ -575,7 +565,7 @@ jobs:
575565
lucee-express/lucee-server/context/logs/
576566
577567
- name: Fail if any sub-test failed
578-
if: steps.config-override-test.outputs.CONFIG_OVERRIDE_FAILED == 'true'
568+
if: steps.config-override-test.outputs.CONFIG_OVERRIDE_FAILED == 'true' || steps.reflection-test.outputs.REFLECTION_FAILED == 'true' || steps.reflection-log-check.outputs.REFLECTION_LOG_MISSING == 'true'
579569
run: exit 1
580570

581571
deploy:

source/java/src/org/lucee/extension/websocket/WebSocketEndpointFactory.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,16 +128,23 @@ private void registerEndpoint(ConfigWeb cw) throws PageException {
128128
WSUtil.info(cs, msg);
129129
WSUtil.info(cw, msg);
130130

131-
CFMLEngine eng = CFMLEngineFactory.getInstance();
131+
// Use plain java.lang.reflect instead of Lucee's ClassUtil.callStaticMethod
132+
// The old endpoint Class lives on (Tomcat holds the ref) but its owning
133+
// bundle's classloader may be stopped after cfadmin restart / extension
134+
// hot-reinstall — Lucee's dynamic reflector reads bytecode via ASM which
135+
// fails with "unable to load class path". Direct reflection works off the
136+
// Class's own metadata, no bytecode re-read needed. LDEV-6221.
132137
try {
138+
Class<?> oldEndpointClass = (Class<?>) endpoint;
139+
java.lang.reflect.Method inject = oldEndpointClass.getMethod("inject", Object.class);
133140
if (WSUtil.getContainerType(cw) == WSUtil.TYPE_JAKARTA)
134-
eng.getClassUtil().callStaticMethod((Class) endpoint, "inject", new Object[] { new JakartaWebSocketEndpoint() });
141+
inject.invoke(null, new JakartaWebSocketEndpoint());
135142
else if (WSUtil.getContainerType(cw) == WSUtil.TYPE_JAVAX)
136-
eng.getClassUtil().callStaticMethod((Class) endpoint, "inject", new Object[] { new JavaxWebSocketEndpoint() });
143+
inject.invoke(null, new JavaxWebSocketEndpoint());
137144
}
138-
catch (PageException e) {
145+
catch (ReflectiveOperationException e) {
139146
print.e(e);
140-
throw e;
147+
throw eng.getCastUtil().toPageException(e);
141148
}
142149
}
143150
// add

0 commit comments

Comments
 (0)