Skip to content

Commit ed21022

Browse files
committed
Qute debugger support for standalone application
Signed-off-by: azerr <azerr@redhat.com>
1 parent 6251fb3 commit ed21022

File tree

12 files changed

+198
-82
lines changed

12 files changed

+198
-82
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package io.quarkus.qute;
2+
3+
import io.quarkus.qute.EngineBuilder.EngineListener;
4+
5+
/**
6+
* Utility class responsible for enabling Qute debugging support when running
7+
* Qute in standalone mode (i.e., outside a Quarkus application runtime).
8+
* <p>
9+
* This class checks configuration flags defined as **system properties** (using `-D`)
10+
* to determine whether the Qute debugger should be started.
11+
* Environment variables are still supported as a fallback, but using `-D` is preferred.
12+
* <p>
13+
* The debugger will be enabled if:
14+
* <ul>
15+
* <li>{@code -DquteDebugPort=<port>} is specified, or</li>
16+
* <li>{@code -DquteDebugEnabled=true} is specified</li>
17+
* </ul>
18+
* and only when Qute is not running inside a Quarkus runtime. If running inside Quarkus,
19+
* debugging is handled automatically via {@code DebugQuteEngineObserver}.
20+
*
21+
* <p>
22+
* To enable standalone debugging, the dependency {@code quarkus-qute-debug} must be present.
23+
* Otherwise, a message will be printed and debugging will be skipped.
24+
*
25+
* <h3>Supported system properties</h3>
26+
* <ul>
27+
* <li>{@code quteDebugPort} — Debug server port (optional)</li>
28+
* <li>{@code quteDebugEnabled} — Enables debugging even without a port (optional)</li>
29+
* <li>{@code quteDebugSuspend} — If true, pauses template rendering until a debugger attaches</li>
30+
* </ul>
31+
*
32+
* <p>
33+
* Example of launching a standalone Qute application with debugging enabled:
34+
*
35+
* <pre>
36+
* java -DquteDebugPort=5005 -DquteDebugSuspend=true -DquteDebugEnabled=true -jar my-qute-app.jar
37+
* </pre>
38+
*/
39+
public class DebuggerConfigurationUtils {
40+
41+
private static final String QUTE_DEBUG_PORT = "quteDebugPort";
42+
private static final String QUTE_DEBUG_SUSPEND = "quteDebugSuspend";
43+
private static final String QUTE_DEBUG_ENABLED = "quteDebugEnabled";
44+
45+
/**
46+
* Creates the debugger adapter listener if debugging is requested and Qute
47+
* is running in standalone mode.
48+
*
49+
* @return EngineListener for the debug server, or null if debugging is disabled or running inside Quarkus
50+
*/
51+
public static EngineListener createDebuggerIfNeeded() {
52+
Integer port = getDebugPort();
53+
boolean enabled = isDebugEnabled();
54+
55+
// Debug not explicitly enabled and no port defined → do nothing.
56+
if (port == null && !enabled) {
57+
return null;
58+
}
59+
60+
// If we are inside Quarkus runtime, the debug is handled automatically by Quarkus.
61+
if (!isStandalone()) {
62+
return null;
63+
}
64+
65+
// Try to load the standalone debug server implementation.
66+
try {
67+
Class<?> cls = Class.forName("io.quarkus.qute.debug.adapter.RegisterDebugServerAdapter");
68+
// Use the no-argument constructor
69+
return (EngineListener) cls.getDeclaredConstructor().newInstance();
70+
} catch (Exception e) {
71+
System.err.println("You need to install the `quarkus-qute-debug` dependency to enable debugging.");
72+
return null;
73+
}
74+
}
75+
76+
/**
77+
* Reads the debug port from system property or environment variable.
78+
*
79+
* @return the port number, or null if unset or invalid
80+
*/
81+
public static Integer getDebugPort() {
82+
String port = getPropertyValue(QUTE_DEBUG_PORT);
83+
if (port == null || port.isBlank()) {
84+
return null;
85+
}
86+
try {
87+
return Integer.parseInt(port);
88+
} catch (NumberFormatException e) {
89+
return null;
90+
}
91+
}
92+
93+
/**
94+
* Returns whether debugging is enabled.
95+
*
96+
* @return true if debugging is enabled via system property or environment variable
97+
*/
98+
public static boolean isDebugEnabled() {
99+
return Boolean.parseBoolean(getPropertyValue(QUTE_DEBUG_ENABLED));
100+
}
101+
102+
/**
103+
* Returns whether rendering should block until a debugger attaches.
104+
*
105+
* @return true if the suspend flag is set
106+
*/
107+
public static boolean isDebugSuspend() {
108+
return Boolean.parseBoolean(getPropertyValue(QUTE_DEBUG_SUSPEND));
109+
}
110+
111+
/**
112+
* Determines whether Qute is running in standalone mode (i.e., outside Quarkus runtime).
113+
*
114+
* @return true if standalone, false if running inside Quarkus
115+
*/
116+
private static boolean isStandalone() {
117+
try {
118+
// If this class exists, we are inside Quarkus → not standalone
119+
Class.forName("io.quarkus.qute.runtime.debug.DebugQuteEngineObserver");
120+
return false;
121+
} catch (ClassNotFoundException e) {
122+
// Class missing → standalone environment
123+
return true;
124+
}
125+
}
126+
127+
/**
128+
* Reads a configuration value from environment variable first, then system property.
129+
* <p>
130+
* For standalone applications, it is recommended to use system properties with `-D`.
131+
*
132+
* @param name the name of the property or environment variable
133+
* @return the property value, or null if unset
134+
*/
135+
private static String getPropertyValue(String name) {
136+
String value = System.getenv(name);
137+
if (value == null || value.isBlank()) {
138+
value = System.getProperty(name);
139+
}
140+
return value;
141+
}
142+
}

independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public final class EngineBuilder {
2929

3030
private static final Logger LOG = Logger.getLogger(EngineBuilder.class);
3131

32+
private static final EngineListener /* RegisterDebugServerAdapter */ DEBUGGER = DebuggerConfigurationUtils
33+
.createDebuggerIfNeeded();
34+
3235
final Map<String, SectionHelperFactory<?>> sectionHelperFactories;
3336
final List<ValueResolver> valueResolvers;
3437
final List<NamespaceResolver> namespaceResolvers;
@@ -59,6 +62,10 @@ public final class EngineBuilder {
5962
this.timeout = 10_000;
6063
this.useAsyncTimeout = true;
6164
this.listeners = new ArrayList<>();
65+
if (DEBUGGER != null) {
66+
enableTracing(true);
67+
addEngineListener(DEBUGGER);
68+
}
6269
}
6370

6471
/**

independent-projects/qute/debug/src/main/java/io/quarkus/qute/debug/adapter/RegisterDebugServerAdapter.java

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.eclipse.lsp4j.debug.services.IDebugProtocolClient;
1515
import org.eclipse.lsp4j.jsonrpc.Launcher;
1616

17+
import io.quarkus.qute.DebuggerConfigurationUtils;
1718
import io.quarkus.qute.Engine;
1819
import io.quarkus.qute.EngineBuilder.EngineListener;
1920
import io.quarkus.qute.debug.agent.DebuggeeAgent;
@@ -101,60 +102,36 @@ private DebuggeeAgent createAgentIfNeeded() {
101102
}
102103

103104
/**
104-
* Returns the Qute debugger port defines with "-DquteDebugPort" environment variable and false otherwise.
105+
* Returns the Qute debugger port defines with "-DquteDebugPort" environment
106+
* variable and false otherwise.
105107
*
106-
* @return the Qute debugger port defines with "-DquteDebugPort" environment variable and false otherwise.
108+
* @return the Qute debugger port defines with "-DquteDebugPort" environment
109+
* variable and false otherwise.
107110
*/
108111
public Integer getPort() {
109112
if (port != null) {
110113
return port;
111114
}
112-
port = doGetPort();
113-
return port;
114-
}
115-
116-
private static Integer doGetPort() {
117-
// Read the debug port from the environment variable
118-
String portStr = getPropertyValue("quteDebugPort");
119-
if (portStr == null || portStr.isBlank()) {
120-
return null;
121-
}
122-
try {
123-
return Integer.parseInt(portStr);
124-
} catch (Exception e) {
125-
return null;
115+
port = DebuggerConfigurationUtils.getDebugPort();
116+
if (port == null && DebuggerConfigurationUtils.isDebugEnabled()) {
117+
// free port
118+
try {
119+
port = findAvailableSocketPort();
120+
} catch (Exception e) {
121+
// Ignore error: handle exception
122+
}
126123
}
124+
return port;
127125
}
128126

129127
private boolean isSuspend() {
130128
if (suspend != null) {
131129
return suspend;
132130
}
133-
suspend = doIsSuspend();
131+
suspend = DebuggerConfigurationUtils.isDebugSuspend();
134132
return suspend;
135133
}
136134

137-
private boolean doIsSuspend() {
138-
// Read the suspend flag from the environment variable
139-
String suspend = getPropertyValue("quteDebugSuspend");
140-
if (suspend == null || suspend.isBlank()) {
141-
return false;
142-
}
143-
try {
144-
return Boolean.parseBoolean(suspend);
145-
} catch (Exception e) {
146-
return false;
147-
}
148-
}
149-
150-
private static String getPropertyValue(String name) {
151-
String value = System.getenv(name);
152-
if (value == null || value.isBlank()) {
153-
value = System.getProperty(name);
154-
}
155-
return value;
156-
}
157-
158135
/**
159136
* Initializes the debug agent. In suspend mode, this call blocks until a DAP
160137
* client connects. Otherwise, initialization is done asynchronously in a
@@ -265,6 +242,20 @@ public void reset() {
265242
}
266243
}
267244

245+
public static int findAvailableSocketPort() throws IOException {
246+
try (ServerSocket serverSocket = new ServerSocket(0)) {
247+
int port = serverSocket.getLocalPort();
248+
synchronized (serverSocket) {
249+
try {
250+
serverSocket.wait(1L);
251+
} catch (InterruptedException e) {
252+
Thread.currentThread().interrupt();
253+
}
254+
}
255+
return port;
256+
}
257+
}
258+
268259
/**
269260
* Creates and starts the DAP launcher for the connected client socket. Cancels
270261
* any previous launcher.

independent-projects/qute/debug/src/test/java/io/quarkus/qute/debug/breakpoints/BreakpointTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.qute.debug.breakpoints;
22

3+
import static io.quarkus.qute.debug.adapter.RegisterDebugServerAdapter.findAvailableSocketPort;
34
import static org.junit.jupiter.api.Assertions.assertEquals;
45
import static org.junit.jupiter.api.Assertions.assertFalse;
56

@@ -17,15 +18,14 @@
1718
import io.quarkus.qute.debug.RenderTemplateInThread;
1819
import io.quarkus.qute.debug.adapter.RegisterDebugServerAdapter;
1920
import io.quarkus.qute.debug.client.DAPClient;
20-
import io.quarkus.qute.debug.client.DebuggerUtils;
2121

2222
public class BreakpointTest {
2323

2424
private static final String TEMPLATE_ID = "hello.qute";
2525

2626
@Test
2727
public void debuggingTemplate() throws Exception {
28-
int port = DebuggerUtils.findAvailableSocketPort();
28+
int port = findAvailableSocketPort();
2929

3030
// Server side :
3131
// - create a Qute engine and set the debugging port as 1234
@@ -156,4 +156,4 @@ public void debuggingTemplate() throws Exception {
156156

157157
}
158158

159-
}
159+
}

independent-projects/qute/debug/src/test/java/io/quarkus/qute/debug/breakpoints/ConditionalBreakpointTest.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.qute.debug.breakpoints;
22

3+
import static io.quarkus.qute.debug.adapter.RegisterDebugServerAdapter.findAvailableSocketPort;
34
import static org.junit.jupiter.api.Assertions.assertEquals;
45
import static org.junit.jupiter.api.Assertions.assertFalse;
56

@@ -17,15 +18,14 @@
1718
import io.quarkus.qute.debug.RenderTemplateInThread;
1819
import io.quarkus.qute.debug.adapter.RegisterDebugServerAdapter;
1920
import io.quarkus.qute.debug.client.DAPClient;
20-
import io.quarkus.qute.debug.client.DebuggerUtils;
2121

2222
public class ConditionalBreakpointTest {
2323

2424
private static final String TEMPLATE_ID = "hello.qute";
2525

2626
@Test
2727
public void debuggingTemplate() throws Exception {
28-
int port = DebuggerUtils.findAvailableSocketPort();
28+
int port = findAvailableSocketPort();
2929

3030
// Server side :
3131
// - create a Qute engine and set the debugging port as 1234
@@ -114,8 +114,7 @@ public void debuggingTemplate() throws Exception {
114114
// Stack frame on item_count
115115
frameId = currentFrame.getId();
116116
frameName = currentFrame.getName();
117-
assertEquals(
118-
"ExpressionNode [expression=Expression [namespace=null, parts=[item_count], literal=null]]",
117+
assertEquals("ExpressionNode [expression=Expression [namespace=null, parts=[item_count], literal=null]]",
119118
frameName);
120119

121120
// Evaluate item_count
@@ -156,8 +155,7 @@ public void debuggingTemplate() throws Exception {
156155
// Stack frame on item_count
157156
frameId = currentFrame.getId();
158157
frameName = currentFrame.getName();
159-
assertEquals(
160-
"ExpressionNode [expression=Expression [namespace=null, parts=[item_count], literal=null]]",
158+
assertEquals("ExpressionNode [expression=Expression [namespace=null, parts=[item_count], literal=null]]",
161159
frameName);
162160

163161
// Evaluate item_count

independent-projects/qute/debug/src/test/java/io/quarkus/qute/debug/client/DebuggerUtils.java

Lines changed: 0 additions & 22 deletions
This file was deleted.

independent-projects/qute/debug/src/test/java/io/quarkus/qute/debug/completions/CompletionArrayListTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static io.quarkus.qute.debug.QuteAssert.assertCompletion;
44
import static io.quarkus.qute.debug.QuteAssert.c;
5+
import static io.quarkus.qute.debug.adapter.RegisterDebugServerAdapter.findAvailableSocketPort;
56
import static org.junit.jupiter.api.Assertions.assertEquals;
67
import static org.junit.jupiter.api.Assertions.assertNotNull;
78

@@ -19,7 +20,6 @@
1920
import io.quarkus.qute.debug.RenderTemplateInThread;
2021
import io.quarkus.qute.debug.adapter.RegisterDebugServerAdapter;
2122
import io.quarkus.qute.debug.client.DAPClient;
22-
import io.quarkus.qute.debug.client.DebuggerUtils;
2323
import io.quarkus.qute.debug.data.Item;
2424

2525
public class CompletionArrayListTest {
@@ -28,7 +28,7 @@ public class CompletionArrayListTest {
2828

2929
@Test
3030
public void debuggingTemplate() throws Exception {
31-
int port = DebuggerUtils.findAvailableSocketPort();
31+
int port = findAvailableSocketPort();
3232

3333
// Server side :
3434
// - create a Qute engine and set the debugging port as 1234

0 commit comments

Comments
 (0)