Skip to content

Commit 19fbf83

Browse files
committed
MCP-197 Terminate the JVM properly
1 parent f583f01 commit 19fbf83

File tree

2 files changed

+76
-8
lines changed

2 files changed

+76
-8
lines changed

src/main/java/org/sonarsource/sonarqube/mcp/transport/StdioServerTransportProvider.java

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,29 +72,53 @@ public class StdioServerTransportProvider implements McpServerTransportProvider
7272
private final Sinks.One<Void> inboundReady = Sinks.one();
7373

7474
/**
75-
* Creates a new StdioServerTransportProvider with the specified ObjectMapper and
76-
* System streams. Will call System.exit(0) when stdin closes (for Docker containers).
75+
* Flag to indicate if we should call System.exit(0) on stdin EOF.
76+
* False during tests.
77+
*/
78+
private final boolean shouldExitOnEof;
79+
80+
/**
81+
* Creates a new StdioServerTransportProvider with the specified ObjectMapper and system streams.
82+
* Will call System.exit(0) when stdin closes, unless running in test mode.
7783
*/
7884
public StdioServerTransportProvider(ObjectMapper objectMapper) {
79-
this(new JacksonMcpJsonMapper(objectMapper), System.in, System.out);
85+
this(new JacksonMcpJsonMapper(objectMapper), System.in, System.out, shouldExitOnStdinEof());
86+
}
87+
88+
private static boolean shouldExitOnStdinEof() {
89+
return shouldExitOnStdinEof(Thread.currentThread().getStackTrace());
90+
}
91+
92+
static boolean shouldExitOnStdinEof(StackTraceElement[] stackTrace) {
93+
// Check if we're running in test mode
94+
for (var element : stackTrace) {
95+
if (element.getClassName().startsWith("org.junit.")) {
96+
return false;
97+
}
98+
}
99+
return true;
80100
}
81101

82102
/**
83103
* Creates a new StdioServerTransportProvider with the specified ObjectMapper and
84-
* streams. Automatically detects if custom streams are used (tests) to disable System.exit().
85-
*
86-
* @param jsonMapper The JsonMapper to use for JSON serialization/deserialization
87-
* @param inputStream The input stream to read from
88-
* @param outputStream The output stream to write to
104+
* streams. Custom streams disable System.exit() behavior (used for testing).
89105
*/
90106
public StdioServerTransportProvider(McpJsonMapper jsonMapper, InputStream inputStream, OutputStream outputStream) {
107+
this(jsonMapper, inputStream, outputStream, false);
108+
}
109+
110+
/**
111+
* Private constructor with explicit control over System.exit behavior.
112+
*/
113+
private StdioServerTransportProvider(McpJsonMapper jsonMapper, InputStream inputStream, OutputStream outputStream, boolean shouldExitOnEof) {
91114
Assert.notNull(jsonMapper, "The JsonMapper can not be null");
92115
Assert.notNull(inputStream, "The InputStream can not be null");
93116
Assert.notNull(outputStream, "The OutputStream can not be null");
94117

95118
this.jsonMapper = jsonMapper;
96119
this.inputStream = inputStream;
97120
this.outputStream = outputStream;
121+
this.shouldExitOnEof = shouldExitOnEof;
98122
}
99123

100124
@Override
@@ -267,6 +291,13 @@ private void startInboundProcessing() {
267291
session.close();
268292
}
269293
inboundSink.tryEmitComplete();
294+
295+
// When stdin closes (EOF detected) in production, terminate the JVM to ensure we exit properly.
296+
// In test mode, this is disabled to avoid terminating the test runner.
297+
if (shouldExitOnEof) {
298+
logger.info("stdin closed (EOF detected) - terminating process");
299+
System.exit(0);
300+
}
270301
}
271302
});
272303
}

src/test/java/org/sonarsource/sonarqube/mcp/transport/StdioServerTransportProviderTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.mockito.Mockito;
2424
import reactor.core.publisher.Mono;
2525

26+
import static org.assertj.core.api.Assertions.assertThat;
2627
import static org.assertj.core.api.Assertions.assertThatCode;
2728
import static org.mockito.Mockito.mock;
2829
import static org.mockito.Mockito.never;
@@ -123,5 +124,41 @@ void closeGracefully_should_complete_immediately_if_session_closes_in_5_seconds(
123124
verify(mockSession, never()).close();
124125
}
125126

127+
@Test
128+
void shouldExitOnStdinEof_should_return_true_for_production_stack_trace() {
129+
// Simulate a production stack trace with no test frameworks
130+
var stackTrace = new StackTraceElement[] {
131+
new StackTraceElement("com.example.MyApp", "main", "MyApp.java", 10),
132+
new StackTraceElement("com.example.MyService", "start", "MyService.java", 20),
133+
new StackTraceElement("java.lang.Thread", "run", "Thread.java", 100)
134+
};
135+
136+
boolean result = StdioServerTransportProvider.shouldExitOnStdinEof(stackTrace);
137+
138+
assertThat(result).isTrue();
139+
}
140+
141+
@Test
142+
void shouldExitOnStdinEof_should_return_false_when_junit_in_stack_trace() {
143+
// Simulate a test stack trace with JUnit
144+
var stackTrace = new StackTraceElement[] {
145+
new StackTraceElement("org.junit.jupiter.engine.execution.ExecutableInvoker", "invoke", "ExecutableInvoker.java", 20)
146+
};
147+
148+
boolean result = StdioServerTransportProvider.shouldExitOnStdinEof(stackTrace);
149+
150+
assertThat(result).isFalse();
151+
}
152+
153+
@Test
154+
void shouldExitOnStdinEof_should_return_true_for_empty_stack_trace() {
155+
// Edge case: empty stack trace
156+
var stackTrace = new StackTraceElement[] {};
157+
158+
boolean result = StdioServerTransportProvider.shouldExitOnStdinEof(stackTrace);
159+
160+
assertThat(result).isTrue();
161+
}
162+
126163
}
127164

0 commit comments

Comments
 (0)