diff --git a/robocode.host/src/main/java/net/sf/robocode/host/security/ThreadManager.java b/robocode.host/src/main/java/net/sf/robocode/host/security/ThreadManager.java index f3f2e9fe0..63cbd7f21 100644 --- a/robocode.host/src/main/java/net/sf/robocode/host/security/ThreadManager.java +++ b/robocode.host/src/main/java/net/sf/robocode/host/security/ThreadManager.java @@ -153,8 +153,12 @@ public FileOutputStream createRobotFileStream(String fileName, boolean append) t AccessController.doPrivileged(new PrivilegedAction() { public Object run() { outputStreamThreads.add(c); - if (!dir.exists() && !dir.mkdirs()) { - syserr.println("Can't create dir " + dir); + try { + if (!dir.exists() && !dir.mkdirs()) { + syserr.println("Can't create dir " + dir); + } + } finally { + outputStreamThreads.remove(c); } return null; } @@ -176,7 +180,12 @@ public Object run() { } outputStreamThreads.add(c); - return new RobotFileOutputStream(fileName, append, fileSystemManager); + try { + return new RobotFileOutputStream(fileName, append, fileSystemManager); + } finally { + // The SecurityManager removes the thread when the file is opened, but Java 24+ has no SecurityManager + outputStreamThreads.remove(c); + } } public void checkRobotFileWrite(File file) { diff --git a/robocode.host/src/test/java/net/sf/robocode/host/security/ThreadManagerTest.java b/robocode.host/src/test/java/net/sf/robocode/host/security/ThreadManagerTest.java new file mode 100644 index 000000000..1b41ebc86 --- /dev/null +++ b/robocode.host/src/test/java/net/sf/robocode/host/security/ThreadManagerTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2001-2026 Mathew A. Nelson and Robocode contributors + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://robocode.sourceforge.io/license/epl-v10.html + */ +package net.sf.robocode.host.security; + + +import net.sf.robocode.host.IHostedThread; +import net.sf.robocode.host.RobotStatics; +import net.sf.robocode.host.io.RobotFileSystemManager; +import net.sf.robocode.repository.IRobotItem; +import net.sf.robocode.security.HiddenAccess; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import robocode.control.RobotSpecification; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + + +/** + * @author Robert Maupin (original) + */ +public class ThreadManagerTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + // The thread would keep the whole robot in memory, and Java 24+ has no SecurityManager to remove it + @Test + public void aThreadIsNotKeptAfterItOpensAFile() throws IOException { + File root = folder.newFolder("data"); + IHostedThread robot = createAdvancedRobot(root); + ThreadManager manager = new ThreadManager(); + + manager.addThreadGroup(Thread.currentThread().getThreadGroup(), robot); + + FileOutputStream stream = manager.createRobotFileStream(new File(root, "Robot.data/file").getPath(), false); + + stream.close(); + + Assert.assertFalse(manager.checkRobotFileStream()); + } + + private static IHostedThread createAdvancedRobot(final File root) { + HiddenAccess.init(); + + IRobotItem item = stub(IRobotItem.class, new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) { + switch (method.getName()) { + case "isAdvancedRobot": + return true; + + case "getFullClassName": + return "test.Robot"; + + case "getShortClassName": + return "Robot"; + + default: + return defaultValue(method.getReturnType()); + } + } + }); + RobotSpecification specification = HiddenAccess.createSpecification(item, "test.Robot 1.0", null, null, "1.0", null, null, + "test.Robot", null); + final RobotStatics statics = new RobotStatics(specification, "test.Robot 1.0", "", false, null, null, null, 0, 0); + + return stub(IHostedThread.class, new InvocationHandler() { + private RobotFileSystemManager manager; + + public Object invoke(Object proxy, Method method, Object[] args) { + switch (method.getName()) { + case "getStatics": + return statics; + + case "getRobotFileSystemManager": + if (manager == null) { + manager = new RobotFileSystemManager((IHostedThread) proxy, 200000, root.getPath(), root.getPath(), null); + } + return manager; + + default: + return defaultValue(method.getReturnType()); + } + } + }); + } + + @SuppressWarnings("unchecked") + private static T stub(Class type, InvocationHandler handler) { + return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type}, handler); + } + + private static Object defaultValue(Class type) { + if (type == boolean.class) { + return false; + } + return type.isPrimitive() && type != void.class ? 0 : null; + } +} diff --git a/robocode.roborumble/src/main/java/net/sf/robocode/roborumble/battlesengine/BattlesRunner.java b/robocode.roborumble/src/main/java/net/sf/robocode/roborumble/battlesengine/BattlesRunner.java index c3451b74f..96ceb2194 100644 --- a/robocode.roborumble/src/main/java/net/sf/robocode/roborumble/battlesengine/BattlesRunner.java +++ b/robocode.roborumble/src/main/java/net/sf/robocode/roborumble/battlesengine/BattlesRunner.java @@ -32,6 +32,9 @@ * @author Robert D. Maupin (contributor) */ public class BattlesRunner { + // How long to wait in milliseconds before going on when the robot data could not be deleted + private static final long CLEARDATA_RETRY_WAIT = 60000; + private final BattlesFile inputfile; private final int numrounds; private final int fieldlen; @@ -117,7 +120,7 @@ public void runBattlesImpl(boolean melee) { battle.getSentryBorderSize(), hidenames, robotsList); lastResults = null; - if (cleardata && !deleteContents(FileUtil.getRobotsDataDir())) { + if (cleardata && !clearRobotData(FileUtil.getRobotsDataDir(), CLEARDATA_RETRY_WAIT)) { // The results would claim that the data was cleared Logger.realErr.println("Skipping battle because the robot data could not be deleted: " + enemies); } else { @@ -137,6 +140,34 @@ public void runBattlesImpl(boolean melee) { outtxt.close(); } + /** + * Deletes the data robots saved. A robot may have left a file open for reading, which keeps it from being deleted + * on Windows until the stream is garbage collected, so this collects the garbage before trying again. If that does + * not help either, it waits, so that a client that cannot fight does not keep asking the server for the ratings. + * + * @return true if nothing is left of the data; false otherwise. + */ + static boolean clearRobotData(File data, long waitAfterFailure) { + if (deleteContents(data)) { + return true; + } + System.gc(); + pause(100); // A separate thread closes the stream after the collection + if (deleteContents(data)) { + return true; + } + pause(waitAfterFailure); + return false; + } + + private static void pause(long milliseconds) { + try { + Thread.sleep(milliseconds); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + /** * Deletes everything in a directory. For the robot data directory this is safe between battles, as * a robot gets back the data in its jar when a battle starts. diff --git a/robocode.roborumble/src/test/java/net/sf/robocode/roborumble/battlesengine/BattlesRunnerTest.java b/robocode.roborumble/src/test/java/net/sf/robocode/roborumble/battlesengine/BattlesRunnerTest.java index 812217c7e..76d8505f8 100644 --- a/robocode.roborumble/src/test/java/net/sf/robocode/roborumble/battlesengine/BattlesRunnerTest.java +++ b/robocode.roborumble/src/test/java/net/sf/robocode/roborumble/battlesengine/BattlesRunnerTest.java @@ -14,6 +14,7 @@ import org.junit.rules.TemporaryFolder; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; @@ -49,4 +50,24 @@ public void aMissingDirectoryIsFine() { public void aPathThatCannotBeListedIsAFailure() throws IOException { Assert.assertFalse(BattlesRunner.deleteContents(folder.newFile("not-a-directory"))); } + + @Test + public void aFileLeftOpenForReadingIsDeletedAfterACollection() throws IOException { + File data = folder.newFolder("data"); + + leaveOpenForReading(new File(data, "kept")); + + Assert.assertTrue(BattlesRunner.clearRobotData(data, 0)); + Assert.assertEquals(0, data.list().length); + } + + @Test + public void dataThatCannotBeDeletedIsAFailure() throws IOException { + Assert.assertFalse(BattlesRunner.clearRobotData(folder.newFile("not-a-directory"), 0)); + } + + private static void leaveOpenForReading(File file) throws IOException { + Assert.assertTrue(file.createNewFile()); + new FileInputStream(file).read(); + } } diff --git a/versions.md b/versions.md index 04d058ab4..44d157fca 100644 --- a/versions.md +++ b/versions.md @@ -9,6 +9,9 @@ `BulletHitBulletEvent`, if the robot that fired was on a team. - Files that a robot left open were not closed when the battle ended. They are now closed, with a warning in the console naming the robot and the file. On Windows, an open file cannot be deleted. +- With Java 24 and newer, a robot that wrote a file stayed in memory after the battle, as its thread was kept in a list + that only the SecurityManager removed threads from. Long runs, e.g. of the RoboRumble client, ran out of memory and + hung. ### Changes @@ -16,8 +19,8 @@ configuration, robots only see their enemies by anonymous names like `#1`, and uploads say `hidenames=YES`. - #118: New `CLEARDATA` setting for the RoboRumble client, by Chase-san. With `CLEARDATA=YES`, the data robots saved is deleted before each battle, so they only keep it between the rounds of one battle, and uploads say `cleardata=YES`. - Data in a robot's jar is not lost. If the data cannot be deleted, the client logs which file it was and skips the - battle. + Data in a robot's jar is not lost. If the data cannot be deleted, even after a garbage collection in case a robot left + a file open for reading, the client logs which file it was, waits a minute, and skips the battle. - Both settings default to `NOT` and must match the rumble on the server. The official LiteRumble rumbles need `NOT`. ## Version 1.11.2 (31-Oct-2026)