Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,12 @@ public FileOutputStream createRobotFileStream(String fileName, boolean append) t
AccessController.doPrivileged(new PrivilegedAction<Object>() {
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;
}
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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> T stub(Class<T> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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();
Comment thread
flemming-n-larsen marked this conversation as resolved.
Resolved
}
}

/**
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.junit.rules.TemporaryFolder;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;


Expand Down Expand Up @@ -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();
}
}
7 changes: 5 additions & 2 deletions versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@
`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

- #117: New `HIDENAMES` setting for the RoboRumble client, by Chase-san. With `HIDENAMES=YES` in a rumble's
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)
Expand Down
Loading