Skip to content

Commit 4eda379

Browse files
authored
Merge pull request #1178 from devoxx/fix/shutdown-threadpool-npe-guard
fix: guard thread-pool shutdown against a null Application during JVM exit
2 parents 3530d97 + 4ea0ace commit 4eda379

2 files changed

Lines changed: 93 additions & 0 deletions

File tree

src/main/java/com/devoxx/genie/service/prompt/threading/ThreadPoolShutdownManager.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.devoxx.genie.service.prompt.threading;
22

3+
import com.intellij.openapi.application.Application;
34
import com.intellij.openapi.application.ApplicationManager;
45
import com.intellij.openapi.project.Project;
56
import com.intellij.openapi.project.ProjectManager;
@@ -31,6 +32,14 @@ public class ThreadPoolShutdownManager implements ProjectManagerListener {
3132
*/
3233
private static void shutdownThreadPools() {
3334
try {
35+
// The JVM shutdown hook can fire after the IntelliJ Application has already been
36+
// torn down, at which point ApplicationManager.getApplication() returns null and
37+
// ThreadPoolManager.getInstance() would NPE inside getApplication().getService(...).
38+
// Skip cleanly in that case — the pools die with the JVM anyway.
39+
Application application = ApplicationManager.getApplication();
40+
if (application == null) {
41+
return;
42+
}
3443
ThreadPoolManager threadPoolManager = ThreadPoolManager.getInstance();
3544
if (threadPoolManager != null) {
3645
log.info("Shutting down thread pools");
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.devoxx.genie.service.prompt.threading;
2+
3+
import com.intellij.openapi.application.Application;
4+
import com.intellij.openapi.application.ApplicationManager;
5+
import com.intellij.util.messages.MessageBus;
6+
import com.intellij.util.messages.MessageBusConnection;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.ExtendWith;
9+
import org.mockito.MockedStatic;
10+
import org.mockito.junit.jupiter.MockitoExtension;
11+
import org.mockito.junit.jupiter.MockitoSettings;
12+
import org.mockito.quality.Strictness;
13+
14+
import java.lang.reflect.Method;
15+
16+
import static org.assertj.core.api.Assertions.assertThatCode;
17+
import static org.mockito.Mockito.*;
18+
19+
@ExtendWith(MockitoExtension.class)
20+
@MockitoSettings(strictness = Strictness.LENIENT)
21+
class ThreadPoolShutdownManagerTest {
22+
23+
private static final String CLASS_NAME =
24+
"com.devoxx.genie.service.prompt.threading.ThreadPoolShutdownManager";
25+
26+
/**
27+
* Forces the class's static initializer to run while a mocked Application is available,
28+
* so referencing it later (to invoke the private shutdown method) cannot NPE inside the
29+
* static block itself. The initializer subscribes to the message bus and registers a JVM
30+
* shutdown hook.
31+
*/
32+
private void forceClassInitialized(@org.jetbrains.annotations.NotNull Application mockApp,
33+
@org.jetbrains.annotations.NotNull MockedStatic<ApplicationManager> appMgr)
34+
throws Exception {
35+
MessageBus mockBus = mock(MessageBus.class);
36+
MessageBusConnection mockConn = mock(MessageBusConnection.class);
37+
when(mockApp.getMessageBus()).thenReturn(mockBus);
38+
when(mockBus.connect()).thenReturn(mockConn);
39+
appMgr.when(ApplicationManager::getApplication).thenReturn(mockApp);
40+
Class.forName(CLASS_NAME, true, getClass().getClassLoader());
41+
}
42+
43+
private Method shutdownMethod() throws Exception {
44+
Method m = Class.forName(CLASS_NAME).getDeclaredMethod("shutdownThreadPools");
45+
m.setAccessible(true);
46+
return m;
47+
}
48+
49+
@Test
50+
void shutdownThreadPools_whenApplicationIsNull_returnsWithoutTouchingThreadPoolManager() throws Exception {
51+
try (MockedStatic<ApplicationManager> appMgr = mockStatic(ApplicationManager.class);
52+
MockedStatic<ThreadPoolManager> tpm = mockStatic(ThreadPoolManager.class)) {
53+
54+
Application mockApp = mock(Application.class);
55+
forceClassInitialized(mockApp, appMgr);
56+
57+
// Simulate the JVM shutdown hook firing after the Application has been torn down.
58+
appMgr.when(ApplicationManager::getApplication).thenReturn(null);
59+
60+
Method shutdown = shutdownMethod();
61+
62+
// The guard must swallow the missing Application cleanly — no NPE from getInstance().
63+
assertThatCode(() -> shutdown.invoke(null)).doesNotThrowAnyException();
64+
tpm.verify(ThreadPoolManager::getInstance, never());
65+
}
66+
}
67+
68+
@Test
69+
void shutdownThreadPools_whenApplicationPresent_shutsDownThreadPools() throws Exception {
70+
try (MockedStatic<ApplicationManager> appMgr = mockStatic(ApplicationManager.class);
71+
MockedStatic<ThreadPoolManager> tpm = mockStatic(ThreadPoolManager.class)) {
72+
73+
Application mockApp = mock(Application.class);
74+
forceClassInitialized(mockApp, appMgr);
75+
76+
ThreadPoolManager mockManager = mock(ThreadPoolManager.class);
77+
tpm.when(ThreadPoolManager::getInstance).thenReturn(mockManager);
78+
79+
shutdownMethod().invoke(null);
80+
81+
verify(mockManager).shutdown();
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)