Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 62 additions & 68 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ on:
pull_request:
workflow_dispatch:
inputs:
deploy:
description: 'Deploy to Maven Central'
type: boolean
default: false
lucee-versions:
description: 'JSON array of Lucee version queries (e.g. ["6.2.5.34/snapshot/jar","6.2.6.0/snapshot/jar"])'
default: '["6.2/snapshot/jar","7.0/snapshot/jar"]'
dry-run:
description: 'Dry run - skip deploy to Maven'
type: boolean
default: false

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
setup:
Expand Down Expand Up @@ -486,68 +490,58 @@ jobs:
echo "CONFIG_OVERRIDE_FAILED=true" >> $GITHUB_OUTPUT
fi

# ------------------------------------------------------------------
# Reflection / Lucee-restart scenario — TEMPORARILY DISABLED.
#
# The post-restart path hits a real bug: after cfadmin action="restart",
# the new CFMLEngine's extension classloader can't load
# JakartaWebSocketEndpoint.class — `WebSocketEndpointFactory.scanWebContexts`
# and `registerEndpoint` both fail with "unable to load class path".
# The reflection fallback never gets a chance to patch the old static slot
# because the new classes themselves aren't loadable.
#
# The test CFMs (trigger-lucee-restart.cfm, test-reflection-restart.cfm)
# and this workflow block are kept in place for when the underlying bug
# is fixed — just re-enable the steps below.
#
# See LDEV-6221 and the companion catalina.out output in the earlier CI
# run logs for the stack trace.
# ------------------------------------------------------------------
# - name: Trigger Lucee engine restart (LDEV-6221 scenario)
# id: restart-trigger
# continue-on-error: true
# run: |
# RESPONSE=$(curl -s -w "\n%{http_code}" http://localhost:8888/tests/integration/trigger-lucee-restart.cfm)
# HTTP_CODE=$(echo "$RESPONSE" | tail -1)
# BODY=$(echo "$RESPONSE" | sed '$d')
# echo "$BODY"
# if [ "$HTTP_CODE" != "200" ] || echo "$BODY" | grep -q "FAILED"; then
# echo "RESTART_TRIGGER_FAILED=true" >> $GITHUB_OUTPUT
# fi
#
# - name: Wait for Lucee to come back online
# run: |
# for i in {1..60}; do
# if curl -s -o /dev/null -w "%{http_code}" http://localhost:8888/tests/test-websocket-info.cfm | grep -q "200"; then
# echo "Lucee responding after $i attempts"
# break
# fi
# sleep 1
# done
#
# - name: Run post-restart reflection round-trip
# id: reflection-test
# continue-on-error: true
# run: |
# RESPONSE=$(curl -s -w "\n%{http_code}" http://localhost:8888/tests/integration/test-reflection-restart.cfm)
# HTTP_CODE=$(echo "$RESPONSE" | tail -1)
# BODY=$(echo "$RESPONSE" | sed '$d')
# echo "$BODY"
# if [ "$HTTP_CODE" != "200" ] || echo "$BODY" | grep -q "FAILED"; then
# echo "REFLECTION_FAILED=true" >> $GITHUB_OUTPUT
# fi
#
# - name: Verify reflection warning in catalina.out
# id: reflection-log-check
# continue-on-error: true
# run: |
# if grep -q "calling \[.*\] via reflection" lucee-express/logs/catalina.out; then
# echo "Reflection warning found — hot re-install exercised the fallback path"
# grep "calling \[.*\] via reflection" lucee-express/logs/catalina.out
# else
# echo "REFLECTION_LOG_MISSING=true" >> $GITHUB_OUTPUT
# echo "Reflection warning NOT found in catalina.out — hot re-install did not hit the fallback path"
# fi
- name: Trigger Lucee engine restart (LDEV-6221 scenario)
id: restart-trigger
continue-on-error: true
run: |
# Calls cfadmin action="restart" — reloads the CFMLEngine while Tomcat
# stays up. Forces the fresh extension to hit its reflection fallback
# because Tomcat's endpoint registry still has the pre-restart class.
RESPONSE=$(curl -s -w "\n%{http_code}" http://localhost:8888/tests/integration/trigger-lucee-restart.cfm)
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "$BODY"
if [ "$HTTP_CODE" != "200" ] || echo "$BODY" | grep -q "FAILED"; then
echo "RESTART_TRIGGER_FAILED=true" >> $GITHUB_OUTPUT
fi

- name: Wait for Lucee to come back online
run: |
# Engine restart takes a few seconds — poll a trivial endpoint until 200
for i in {1..60}; do
if curl -s -o /dev/null -w "%{http_code}" http://localhost:8888/tests/test-websocket-info.cfm | grep -q "200"; then
echo "Lucee responding after $i attempts"
break
fi
sleep 1
done

- name: Run post-restart reflection round-trip
id: reflection-test
continue-on-error: true
run: |
RESPONSE=$(curl -s -w "\n%{http_code}" http://localhost:8888/tests/integration/test-reflection-restart.cfm)
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')
echo "$BODY"
if [ "$HTTP_CODE" != "200" ] || echo "$BODY" | grep -q "FAILED"; then
echo "REFLECTION_FAILED=true" >> $GITHUB_OUTPUT
fi

- name: Verify reflection warning in catalina.out
id: reflection-log-check
continue-on-error: true
run: |
# The reflection fallback path logs "calling [onOpen] via reflection, servlet engine restart needed"
# (see BaseWebSocketEndpoint and LDEV-6221). If the hot re-install actually exercised this path,
# the warning will appear in catalina.out. If not, the round-trip test is lying.
if grep -q "calling \[.*\] via reflection" lucee-express/logs/catalina.out; then
echo "Reflection warning found — hot re-install exercised the fallback path"
grep "calling \[.*\] via reflection" lucee-express/logs/catalina.out
else
echo "REFLECTION_LOG_MISSING=true" >> $GITHUB_OUTPUT
echo "Reflection warning NOT found in catalina.out — hot re-install did not hit the fallback path"
fi

- name: Dump WebSocket event logs
if: always()
Expand Down Expand Up @@ -575,13 +569,13 @@ jobs:
lucee-express/lucee-server/context/logs/

- name: Fail if any sub-test failed
if: steps.config-override-test.outputs.CONFIG_OVERRIDE_FAILED == 'true'
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'
run: exit 1

deploy:
runs-on: ubuntu-latest
needs: [build-extension, test, test-config-override]
if: github.event_name == 'workflow_dispatch' && needs.test.result == 'success' && needs.test-config-override.result == 'success' && !inputs.dry-run
if: github.event_name == 'workflow_dispatch' && inputs.deploy && needs.test.result == 'success' && needs.test-config-override.result == 'success'
steps:
- name: Checkout repository
uses: actions/checkout@v6
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,23 @@ private void registerEndpoint(ConfigWeb cw) throws PageException {
WSUtil.info(cs, msg);
WSUtil.info(cw, msg);

CFMLEngine eng = CFMLEngineFactory.getInstance();
// Use plain java.lang.reflect instead of Lucee's ClassUtil.callStaticMethod
// The old endpoint Class lives on (Tomcat holds the ref) but its owning
// bundle's classloader may be stopped after cfadmin restart / extension
// hot-reinstall — Lucee's dynamic reflector reads bytecode via ASM which
// fails with "unable to load class path". Direct reflection works off the
// Class's own metadata, no bytecode re-read needed. LDEV-6221.
try {
Class<?> oldEndpointClass = (Class<?>) endpoint;
java.lang.reflect.Method inject = oldEndpointClass.getMethod("inject", Object.class);
if (WSUtil.getContainerType(cw) == WSUtil.TYPE_JAKARTA)
eng.getClassUtil().callStaticMethod((Class) endpoint, "inject", new Object[] { new JakartaWebSocketEndpoint() });
inject.invoke(null, new JakartaWebSocketEndpoint());
else if (WSUtil.getContainerType(cw) == WSUtil.TYPE_JAVAX)
eng.getClassUtil().callStaticMethod((Class) endpoint, "inject", new Object[] { new JavaxWebSocketEndpoint() });
inject.invoke(null, new JavaxWebSocketEndpoint());
}
catch (PageException e) {
catch (ReflectiveOperationException e) {
print.e(e);
throw e;
throw eng.getCastUtil().toPageException(e);
}
}
// add
Expand Down
29 changes: 11 additions & 18 deletions source/java/src/org/lucee/extension/websocket/util/WSUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -570,28 +570,26 @@ public static long getMaxIdleTimeout(ConfigWeb cw, Object session, long defaultV
}

/**
* Get ServletContext from ConfigWeb using reflection to avoid javax/jakarta linking issues
* Get ServletContext from ConfigWeb using plain java.lang.reflect to avoid
* javax/jakarta linking issues AND to avoid Lucee's ClassUtil which ASM-reads
* bytecode via the class's classloader — that fails after cfadmin restart /
* bundle reload because the old classloader is stopped (LDEV-6221).
*/
public static Object getServletContext(ConfigWeb cw) {
try {
CFMLEngine eng = CFMLEngineFactory.getInstance();
return eng.getClassUtil().callMethod(cw, eng.getCastUtil().toKey("getServletContext"), new Object[] {});
return cw.getClass().getMethod("getServletContext").invoke(cw);
}
catch (PageException e) {
catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to get ServletContext from ConfigWeb", e);
}
}

/**
* Get real path from ServletContext using reflection
*/
public static String getServletContextRealPath(ConfigWeb cw, String path) {
try {
Object sc = getServletContext(cw);
CFMLEngine eng = CFMLEngineFactory.getInstance();
return (String) eng.getClassUtil().callMethod(sc, eng.getCastUtil().toKey("getRealPath"), new Object[] { path });
return (String) sc.getClass().getMethod("getRealPath", String.class).invoke(sc, path);
}
catch (PageException e) {
catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to get real path from ServletContext", e);
}
}
Expand All @@ -602,20 +600,15 @@ public static String getServletContextRealPath(ConfigWeb cw, String path) {
public static boolean isCliServletContext(ConfigWeb cw) {
Object sc = getServletContext(cw);
if (sc == null) return true;
ClassUtil util = CFMLEngineFactory.getInstance().getClassUtil();
return util.isInstaneOf(sc.getClass(), "lucee.cli.servlet.ServletContextImpl");
return "lucee.cli.servlet.ServletContextImpl".equals(sc.getClass().getName());
}

/**
* Get attribute from ServletContext using reflection
*/
public static Object getServletContextAttribute(ConfigWeb cw, String name) {
try {
Object sc = getServletContext(cw);
CFMLEngine eng = CFMLEngineFactory.getInstance();
return eng.getClassUtil().callMethod(sc, eng.getCastUtil().toKey("getAttribute"), new Object[] { name });
return sc.getClass().getMethod("getAttribute", String.class).invoke(sc, name);
}
catch (PageException e) {
catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to get attribute [" + name + "] from ServletContext", e);
}
}
Expand Down
11 changes: 4 additions & 7 deletions tests/integration/test-lifecycle-callbacks.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,10 @@ try {
arrayAppend( errors, "expected [#eventName#] #wanted# time(s), got #found#" );
}

// NOTE: onFirstOpen runs on an async thread (AsyncInvoker in BaseWebSocketEndpoint),
// so its ordering relative to onOpen is NOT guaranteed. Presence + count is asserted
// above; we don't check relative ordering.

// onLastClose must be the last event (fires only after every client disconnects)
if ( arrayLen( events ) && events[ arrayLen( events ) ] != "onLastClose" )
arrayAppend( errors, "onLastClose should be the final event, got: " & events[ arrayLen( events ) ] );
// NOTE: both onFirstOpen and onLastClose run on async threads (AsyncInvoker in
// BaseWebSocketEndpoint). Their ordering relative to onOpen / onClose is NOT
// guaranteed — onLastClose can race with the final onClose. We assert presence
// + count above; no relative-ordering assertions.

if ( arrayLen( errors ) ) {
writeOutput( chr( 10 ) & "FAILED:" & chr( 10 ) );
Expand Down
Loading