@@ -58,6 +58,23 @@ public static String getName() {
5858 private static int bpMaxLine = Integer .MIN_VALUE ;
5959 private static int bpMaxPathLen = 0 ;
6060
61+ /**
62+ * Function breakpoint storage - parallel arrays for fast lookup.
63+ * Writers synchronize on funcBpLock, readers just access the arrays.
64+ */
65+ private static final Object funcBpLock = new Object ();
66+ private static String [] funcBpNames = new String [0 ]; // lowercase function names
67+ private static String [] funcBpComponents = new String [0 ]; // lowercase component names (null = any)
68+ private static String [] funcBpConditions = new String [0 ]; // CFML conditions (null = unconditional)
69+ private static boolean [] funcBpIsWildcard = new boolean [0 ]; // true if name ends with *
70+
71+ /**
72+ * Pre-computed bounds for fast rejection in onFunctionEntry().
73+ */
74+ private static int funcBpMinLen = Integer .MAX_VALUE ;
75+ private static int funcBpMaxLen = Integer .MIN_VALUE ;
76+ private static volatile boolean hasFuncBps = false ;
77+
6178 /**
6279 * Map of Java thread ID -> WeakReference<PageContext> for natively suspended threads.
6380 * Used to call debuggerResume() when DAP continue is received.
@@ -190,7 +207,7 @@ private static class CachedExecutableLines {
190207 */
191208 private static void updateHasSuspendConditions () {
192209 hasSuspendConditions = dapClientConnected &&
193- (bpLines .length > 0 || breakOnUncaughtExceptions || !steppingThreads .isEmpty () || !threadsToPause .isEmpty ());
210+ (bpLines .length > 0 || hasFuncBps || breakOnUncaughtExceptions || !steppingThreads .isEmpty () || !threadsToPause .isEmpty ());
194211 }
195212
196213 /**
@@ -875,19 +892,23 @@ public static boolean shouldSuspend(PageContext pc, String file, int line) {
875892 /**
876893 * Evaluate a CFML condition expression and return its boolean result.
877894 * Returns false if evaluation fails (exception, timeout, etc.).
895+ * Uses reflection to call Lucee's Evaluate function to avoid classloader issues.
878896 */
879897 private static boolean evaluateCondition (PageContext pc , String condition ) {
880898 try {
881- // Use Evaluate.call() directly on PageContext - same approach as DebugManager
882- Object result = lucee .runtime .functions .dynamicEvaluation .Evaluate .call (
883- pc ,
884- new String []{ condition }
885- );
886- return lucee .runtime .op .Caster .toBoolean (result );
899+ // Use reflection to call Evaluate.call() through Lucee's classloader
900+ ClassLoader luceeLoader = pc .getClass ().getClassLoader ();
901+ Class <?> evaluateClass = luceeLoader .loadClass ("lucee.runtime.functions.dynamicEvaluation.Evaluate" );
902+ java .lang .reflect .Method callMethod = evaluateClass .getMethod ("call" , PageContext .class , Object [].class );
903+ Object result = callMethod .invoke (null , pc , new Object []{ condition });
904+
905+ // Cast result to boolean using Lucee's Caster
906+ Class <?> casterClass = luceeLoader .loadClass ("lucee.runtime.op.Caster" );
907+ java .lang .reflect .Method toBooleanMethod = casterClass .getMethod ("toBoolean" , Object .class );
908+ return (Boolean ) toBooleanMethod .invoke (null , result );
887909 } catch (Exception e ) {
888910 // Condition evaluation failed - don't suspend
889- // Log but don't spam - conditions may intentionally reference undefined vars
890- Log .debug ("Condition evaluation failed: " + e .getMessage ());
911+ Log .error ("Condition evaluation failed: " + e .getMessage ());
891912 return false ;
892913 }
893914 }
@@ -1080,4 +1101,154 @@ private static PageContext createTemporaryPageContext() {
10801101 }
10811102 return null ;
10821103 }
1104+
1105+ // ========== Function Breakpoints ==========
1106+
1107+ /**
1108+ * Check if we should suspend on function entry.
1109+ * Called from Lucee's pushDebuggerFrame via DebuggerListener.onFunctionEntry().
1110+ * Must be blazing fast - every UDF call hits this.
1111+ */
1112+ public static boolean onFunctionEntry ( PageContext pc , String functionName ,
1113+ String componentName , String file , int startLine ) {
1114+ // Fast path - no function breakpoints
1115+ if ( !hasFuncBps || !dapClientConnected ) {
1116+ return false ;
1117+ }
1118+
1119+ int len = functionName .length ();
1120+
1121+ // Length bounds check - rejects 99% of calls instantly
1122+ if ( len < funcBpMinLen || len > funcBpMaxLen ) {
1123+ return false ;
1124+ }
1125+
1126+ // Normalize for case-insensitive matching
1127+ String lowerFunc = functionName .toLowerCase ();
1128+ String lowerComp = componentName != null ? componentName .toLowerCase () : null ;
1129+
1130+ // Check each breakpoint
1131+ String [] names = funcBpNames ;
1132+ String [] comps = funcBpComponents ;
1133+ String [] conds = funcBpConditions ;
1134+ boolean [] wilds = funcBpIsWildcard ;
1135+
1136+ for ( int i = 0 ; i < names .length ; i ++ ) {
1137+ // Check component qualifier first (if specified)
1138+ if ( comps [i ] != null && ( lowerComp == null || !lowerComp .equals ( comps [i ] ) ) ) {
1139+ continue ;
1140+ }
1141+
1142+ // Check function name match
1143+ boolean match ;
1144+ if ( wilds [i ] ) {
1145+ // Wildcard: "on*" matches "onRequestStart"
1146+ String prefix = names [i ].substring ( 0 , names [i ].length () - 1 );
1147+ match = lowerFunc .startsWith ( prefix );
1148+ }
1149+ else {
1150+ match = lowerFunc .equals ( names [i ] );
1151+ }
1152+
1153+ if ( match ) {
1154+ // Check condition if present
1155+ if ( conds [i ] != null ) {
1156+ if ( !evaluateCondition ( pc , conds [i ] ) ) {
1157+ continue ;
1158+ }
1159+ }
1160+ Log .info ( "Function breakpoint hit: " + functionName +
1161+ ( componentName != null ? " in " + componentName : "" ) );
1162+ return true ;
1163+ }
1164+ }
1165+
1166+ return false ;
1167+ }
1168+
1169+ /**
1170+ * Set function breakpoints (replaces all existing).
1171+ * Called from DapServer.setFunctionBreakpoints().
1172+ */
1173+ public static void setFunctionBreakpoints ( String [] names , String [] conditions ) {
1174+ synchronized ( funcBpLock ) {
1175+ int count = names .length ;
1176+ String [] newNames = new String [count ];
1177+ String [] newComps = new String [count ];
1178+ String [] newConds = new String [count ];
1179+ boolean [] newWilds = new boolean [count ];
1180+
1181+ int minLen = Integer .MAX_VALUE ;
1182+ int maxLen = Integer .MIN_VALUE ;
1183+
1184+ for ( int i = 0 ; i < count ; i ++ ) {
1185+ String name = names [i ].trim ();
1186+ String condition = ( conditions != null && i < conditions .length && conditions [i ] != null && !conditions [i ].isEmpty () )
1187+ ? conditions [i ] : null ;
1188+
1189+ // Parse qualified name: "Component.method" or just "method"
1190+ int dot = name .lastIndexOf ( '.' );
1191+ String compName = null ;
1192+ String funcName = name ;
1193+ if ( dot > 0 ) {
1194+ compName = name .substring ( 0 , dot ).toLowerCase ();
1195+ funcName = name .substring ( dot + 1 );
1196+ }
1197+
1198+ // Check for wildcard
1199+ boolean isWild = funcName .endsWith ( "*" );
1200+
1201+ // Store lowercase for case-insensitive matching
1202+ newNames [i ] = funcName .toLowerCase ();
1203+ newComps [i ] = compName ;
1204+ newConds [i ] = condition ;
1205+ newWilds [i ] = isWild ;
1206+
1207+ // Update bounds (for wildcards, use prefix length)
1208+ int effectiveLen = isWild ? funcName .length () - 1 : funcName .length ();
1209+ if ( !isWild ) {
1210+ // Exact match: bounds are exact length
1211+ if ( effectiveLen < minLen ) minLen = effectiveLen ;
1212+ if ( effectiveLen > maxLen ) maxLen = effectiveLen ;
1213+ }
1214+ else {
1215+ // Wildcard: any length >= prefix is possible
1216+ if ( effectiveLen < minLen ) minLen = effectiveLen ;
1217+ maxLen = Integer .MAX_VALUE ; // can't bound max for wildcards
1218+ }
1219+
1220+ Log .info ( "Function breakpoint: " + name +
1221+ ( compName != null ? " (component: " + compName + ")" : "" ) +
1222+ ( isWild ? " (wildcard)" : "" ) +
1223+ ( condition != null ? " condition: " + condition : "" ) );
1224+ }
1225+
1226+ funcBpNames = newNames ;
1227+ funcBpComponents = newComps ;
1228+ funcBpConditions = newConds ;
1229+ funcBpIsWildcard = newWilds ;
1230+ funcBpMinLen = count > 0 ? minLen : Integer .MAX_VALUE ;
1231+ funcBpMaxLen = count > 0 ? maxLen : Integer .MIN_VALUE ;
1232+ }
1233+ hasFuncBps = funcBpNames .length > 0 ;
1234+ updateHasSuspendConditions ();
1235+ Log .info ( "Function breakpoints set: " + funcBpNames .length );
1236+ }
1237+
1238+ /**
1239+ * Clear all function breakpoints.
1240+ */
1241+ public static void clearFunctionBreakpoints () {
1242+ synchronized ( funcBpLock ) {
1243+ funcBpNames = new String [0 ];
1244+ funcBpComponents = new String [0 ];
1245+ funcBpConditions = new String [0 ];
1246+ funcBpIsWildcard = new boolean [0 ];
1247+ funcBpMinLen = Integer .MAX_VALUE ;
1248+ funcBpMaxLen = Integer .MIN_VALUE ;
1249+ }
1250+ hasFuncBps = false ;
1251+ updateHasSuspendConditions ();
1252+ Log .debug ( "Function breakpoints cleared" );
1253+ }
10831254}
0 commit comments