1111
1212import java .io .IOException ;
1313import java .lang .reflect .Method ;
14+ import java .lang .reflect .Field ;
1415import java .net .URL ;
1516import java .net .URLClassLoader ;
1617import java .nio .file .Files ;
@@ -95,6 +96,38 @@ void multiReleaseJarUsesOwnedVirtualThreadExecutorOnJdk25() throws Exception {
9596 }
9697 }
9798
99+ @ Test
100+ void clientCloseShutsDownOwnedDefaultExecutorOnJdk25 () throws Exception {
101+ if (Runtime .version ().feature () < 25 ) {
102+ return ;
103+ }
104+
105+ Path classes = Path .of ("target" , "classes" );
106+ Path jar = Files .createTempFile ("copilot-sdk-client-default-executor" , ".jar" );
107+ try {
108+ createClassesJar (jar , classes );
109+
110+ try (var loader = new URLClassLoader (new URL []{jar .toUri ().toURL ()}, null )) {
111+ Class <?> clientClass = Class .forName ("com.github.copilot.CopilotClient" , true , loader );
112+ AutoCloseable client = (AutoCloseable ) clientClass .getConstructor ().newInstance ();
113+ Field ownedExecutorField = clientClass .getDeclaredField ("ownedExecutor" );
114+ ownedExecutorField .setAccessible (true );
115+ ExecutorService ownedExecutor = (ExecutorService ) ownedExecutorField .get (client );
116+
117+ assertNotNull (ownedExecutor );
118+ assertFalse (ownedExecutor .isShutdown ());
119+
120+ client .close ();
121+
122+ assertTrue (ownedExecutor .isShutdown ());
123+ assertTrue (ownedExecutor .awaitTermination (5 , TimeUnit .SECONDS ));
124+ assertTrue (ownedExecutor .isTerminated ());
125+ }
126+ } finally {
127+ Files .deleteIfExists (jar );
128+ }
129+ }
130+
98131 private static boolean isCurrentThreadVirtual () {
99132 try {
100133 Method isVirtual = Thread .class .getMethod ("isVirtual" );
@@ -116,6 +149,31 @@ private static void createProviderJar(Path jar, Path baseClass, Path java25Class
116149 }
117150 }
118151
152+ private static void createClassesJar (Path jar , Path classes ) throws IOException {
153+ Manifest manifest = new Manifest ();
154+ Attributes attributes = manifest .getMainAttributes ();
155+ attributes .put (Attributes .Name .MANIFEST_VERSION , "1.0" );
156+ attributes .putValue ("Multi-Release" , "true" );
157+
158+ try (JarOutputStream output = new JarOutputStream (Files .newOutputStream (jar ), manifest );
159+ var files = Files .walk (classes )) {
160+ var iterator = files .iterator ();
161+ while (iterator .hasNext ()) {
162+ Path file = iterator .next ();
163+ if (!Files .isRegularFile (file )) {
164+ continue ;
165+ }
166+
167+ String entryName = classes .relativize (file ).toString ().replace ('\\' , '/' );
168+ if ("META-INF/MANIFEST.MF" .equals (entryName )) {
169+ continue ;
170+ }
171+
172+ addClass (output , entryName , file );
173+ }
174+ }
175+ }
176+
119177 private static void addClass (JarOutputStream output , String entryName , Path classFile ) throws IOException {
120178 output .putNextEntry (new JarEntry (entryName ));
121179 Files .copy (classFile , output );
0 commit comments