Skip to content
Open
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 @@ -40,7 +40,7 @@
import java.util.*;
import java.util.stream.Collectors;

public class CompiletimeFunctionRunner {
public class CompiletimeFunctionRunner implements AutoCloseable {

private final ImProg imProg;
private final ILInterpreter interpreter;
Expand Down Expand Up @@ -659,4 +659,17 @@ public void setOutputStream(PrintStream printStream) {
interpreter.getGlobalState().setOutStream(printStream);
}

/**
* Releases any resources held by the interpreter or global state (such as open SQLite connections and statements)
* created during compiletime function execution.
*/
@Override
public void close() {
if (interpreter != null) {
interpreter.close();
} else if (globalState != null) {
globalState.close();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,13 @@ public void runCompiletime(WurstProjectConfigData projectConfigData, boolean isP
// compile & inject object-editor data
// TODO run optimizations later?
gui.sendProgress("Running compiletime functions");
CompiletimeFunctionRunner ctr = new CompiletimeFunctionRunner(imTranslator, getImProg(), getMapFile(), getMapfileMpqEditor(), gui,
CompiletimeFunctions, projectConfigData, isProd, cache);
ctr.setInjectObjects(runArgs.isInjectObjects());
ctr.setOutputStream(new PrintStream(System.err));
ctr.run();
// Use try-with-resources to release open native resources (e.g., SQLite DB handles) after compiletime execution finishes.
try (CompiletimeFunctionRunner ctr = new CompiletimeFunctionRunner(imTranslator, getImProg(), getMapFile(), getMapfileMpqEditor(), gui,
CompiletimeFunctions, projectConfigData, isProd, cache)) {
ctr.setInjectObjects(runArgs.isInjectObjects());
ctr.setOutputStream(new PrintStream(System.err));
ctr.run();
}
}

if (gui.getErrorCount() > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,17 @@
import net.moonlightflower.wc3libs.misc.MetaFieldId;
import net.moonlightflower.wc3libs.misc.ObjId;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

@SuppressWarnings("ucd") // ignore unused code detector warnings, because this class uses reflection
public class CompiletimeNatives extends ReflectionBasedNativeProvider implements NativesProvider {
Expand Down Expand Up @@ -185,4 +194,240 @@ public ILconstString getBuildDate() {
public ILconstBool isProductionBuild() {
return isProd ? ILconstBool.TRUE : ILconstBool.FALSE;
}

private int sqliteHandleCounter = 0;
private final Map<Integer, Connection> sqliteConnections = new HashMap<>();
private final Map<Integer, PreparedStatement> sqliteStatements = new HashMap<>();
private final Map<Integer, ResultSet> sqliteResultSets = new HashMap<>();
private final Map<Integer, Integer> sqliteStatementConnections = new HashMap<>();
private final Set<Integer> sqliteExecutedStatements = new HashSet<>();

private Connection sqliteConnection(int handle) {
Connection connection = sqliteConnections.get(handle);
if (connection == null) {
throw new InterpreterException("Invalid SQLite connection handle: " + handle);
}
return connection;
}

private PreparedStatement sqliteStatement(int handle) {
PreparedStatement statement = sqliteStatements.get(handle);
if (statement == null) {
throw new InterpreterException("Invalid SQLite statement handle: " + handle);
}
return statement;
}

public ILconstInt sqlite_open(ILconstString path) {
try {
Connection conn = DriverManager.getConnection("jdbc:sqlite:" + path.getVal());
int handle = ++sqliteHandleCounter;
sqliteConnections.put(handle, conn);
return new ILconstInt(handle);
} catch (SQLException e) {
throw new InterpreterException("Failed to open SQLite database " + path.getVal() + ": " + e.getMessage());
}
}

public ILconstInt sqlite_prepare(ILconstInt connection, ILconstString query) {
Connection conn = sqliteConnection(connection.getVal());
try {
PreparedStatement stmt = conn.prepareStatement(query.getVal());
int handle = ++sqliteHandleCounter;
sqliteStatements.put(handle, stmt);
sqliteStatementConnections.put(handle, connection.getVal());
return new ILconstInt(handle);
} catch (SQLException e) {
throw new InterpreterException("Failed to prepare SQLite statement: " + e.getMessage());
}
}

public void sqlite_bind_int(ILconstInt statement, ILconstInt index, ILconstInt value) {
PreparedStatement stmt = sqliteStatement(statement.getVal());
try {
stmt.setInt(index.getVal(), value.getVal());
} catch (SQLException e) {
throw new InterpreterException("Failed to bind int: " + e.getMessage());
}
}

public void sqlite_bind_real(ILconstInt statement, ILconstInt index, ILconstReal value) {
PreparedStatement stmt = sqliteStatement(statement.getVal());
try {
stmt.setDouble(index.getVal(), (double) value.getVal());
} catch (SQLException e) {
throw new InterpreterException("Failed to bind real: " + e.getMessage());
}
}

public void sqlite_bind_string(ILconstInt statement, ILconstInt index, ILconstString value) {
PreparedStatement stmt = sqliteStatement(statement.getVal());
try {
stmt.setString(index.getVal(), value.getVal());
} catch (SQLException e) {
throw new InterpreterException("Failed to bind string: " + e.getMessage());
}
}

public ILconstBool sqlite_step(ILconstInt statement) {
PreparedStatement stmt = sqliteStatement(statement.getVal());
try {
ResultSet rs = sqliteResultSets.get(statement.getVal());
if (rs == null) {
if (sqliteExecutedStatements.contains(statement.getVal())) {
return ILconstBool.FALSE;
}
boolean hasResultSet = stmt.execute();
Comment thread
Frotty marked this conversation as resolved.
sqliteExecutedStatements.add(statement.getVal());
if (hasResultSet) {
rs = stmt.getResultSet();
sqliteResultSets.put(statement.getVal(), rs);
boolean hasRow = rs.next();
return hasRow ? ILconstBool.TRUE : ILconstBool.FALSE;
} else {
return ILconstBool.FALSE;
}
} else {
boolean hasRow = rs.next();
return hasRow ? ILconstBool.TRUE : ILconstBool.FALSE;
}
} catch (SQLException e) {
throw new InterpreterException("Failed to step SQLite statement: " + e.getMessage());
}
}

public ILconstInt sqlite_column_count(ILconstInt statement) {
try {
ResultSet rs = sqliteResultSets.get(statement.getVal());
if (rs != null) {
return new ILconstInt(rs.getMetaData().getColumnCount());
}
PreparedStatement stmt = sqliteStatement(statement.getVal());
java.sql.ResultSetMetaData meta = stmt.getMetaData();
return new ILconstInt(meta == null ? 0 : meta.getColumnCount());
} catch (SQLException e) {
throw new InterpreterException("Failed to get column count: " + e.getMessage());
}
}

public ILconstInt sqlite_column_int(ILconstInt statement, ILconstInt index) {
ResultSet rs = sqliteResultSets.get(statement.getVal());
if (rs == null) throw new InterpreterException("No result set for statement handle: " + statement.getVal());
try {
return new ILconstInt(rs.getInt(index.getVal() + 1));
} catch (SQLException e) {
throw new InterpreterException("Failed to get column int: " + e.getMessage());
}
}

public ILconstReal sqlite_column_real(ILconstInt statement, ILconstInt index) {
ResultSet rs = sqliteResultSets.get(statement.getVal());
if (rs == null) throw new InterpreterException("No result set for statement handle: " + statement.getVal());
try {
return new ILconstReal((float) rs.getDouble(index.getVal() + 1));
} catch (SQLException e) {
throw new InterpreterException("Failed to get column real: " + e.getMessage());
}
}

public ILconstString sqlite_column_string(ILconstInt statement, ILconstInt index) {
ResultSet rs = sqliteResultSets.get(statement.getVal());
if (rs == null) throw new InterpreterException("No result set for statement handle: " + statement.getVal());
try {
String val = rs.getString(index.getVal() + 1);
return new ILconstString(val == null ? "" : val);
} catch (SQLException e) {
throw new InterpreterException("Failed to get column string: " + e.getMessage());
}
}

public void sqlite_reset(ILconstInt statement) {
// Mirror SQLite's sqlite3_reset(): reset execution state so the statement can be
// stepped again, but leave bound parameters in place. Clearing bindings here would
// break the common reuse pattern of binding once and re-stepping. Use
// sqlite_clear_bindings to explicitly clear parameters (sqlite3_clear_bindings()).
sqliteStatement(statement.getVal());
try {
ResultSet rs = sqliteResultSets.remove(statement.getVal());
if (rs != null) rs.close();
sqliteExecutedStatements.remove(statement.getVal());
} catch (SQLException e) {
throw new InterpreterException("Failed to reset SQLite statement: " + e.getMessage());
}
}

public void sqlite_clear_bindings(ILconstInt statement) {
PreparedStatement stmt = sqliteStatement(statement.getVal());
try {
stmt.clearParameters();
} catch (SQLException e) {
throw new InterpreterException("Failed to clear SQLite statement bindings: " + e.getMessage());
}
}

public void sqlite_finalize(ILconstInt statement) {
PreparedStatement stmt = sqliteStatements.remove(statement.getVal());
if (stmt == null) throw new InterpreterException("Invalid SQLite statement handle: " + statement.getVal());
sqliteStatementConnections.remove(statement.getVal());
sqliteExecutedStatements.remove(statement.getVal());
try {
ResultSet rs = sqliteResultSets.remove(statement.getVal());
if (rs != null) rs.close();
stmt.close();
} catch (SQLException e) {
throw new InterpreterException("Failed to finalize SQLite statement: " + e.getMessage());
}
}

public void sqlite_close(ILconstInt connection) {
Connection conn = sqliteConnections.remove(connection.getVal());
if (conn == null) throw new InterpreterException("Invalid SQLite connection handle: " + connection.getVal());
for (int statement : sqliteStatementConnections.entrySet().stream()
.filter(entry -> entry.getValue().intValue() == connection.getVal())
.map(Map.Entry::getKey)
.toList()) {
sqlite_finalize(new ILconstInt(statement));
}
try {
conn.close();
} catch (SQLException e) {
throw new InterpreterException("Failed to close SQLite connection: " + e.getMessage());
}
}

public void sqlite_exec(ILconstInt connection, ILconstString query) {
Connection conn = sqliteConnection(connection.getVal());
try (java.sql.Statement stmt = conn.createStatement()) {
stmt.execute(query.getVal());
} catch (SQLException e) {
throw new InterpreterException("Failed to exec SQLite query: " + e.getMessage());
}
}

@Override
public void close() {
closeAllSqliteResources();
}

/**
* Closes all open SQLite resources (result sets, statements, connections).
* Called by the interpreter on shutdown to prevent resource leaks.
*/
public void closeAllSqliteResources() {
Comment thread
Frotty marked this conversation as resolved.
for (ResultSet rs : sqliteResultSets.values()) {
try { rs.close(); } catch (SQLException ignored) {}
}
sqliteResultSets.clear();
for (PreparedStatement stmt : sqliteStatements.values()) {
try { stmt.close(); } catch (SQLException ignored) {}
}
sqliteStatements.clear();
sqliteStatementConnections.clear();
sqliteExecutedStatements.clear();
for (Connection conn : sqliteConnections.values()) {
try { conn.close(); } catch (SQLException ignored) {}
}
sqliteConnections.clear();
sqliteHandleCounter = 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,19 @@ public int getTotalTests() {
public TestResult runTests(ImTranslator translator, ImProg imProg, Optional<FuncDef> funcToTest, Optional<CompilationUnit> cu) {
WurstGui gui = new TestGui();

CompiletimeFunctionRunner cfr = new CompiletimeFunctionRunner(translator, imProg, Optional.empty(), null, gui,
CompiletimeFunctions, WurstProjectConfigData.empty(), false, false);
ILInterpreter interpreter = cfr.getInterpreter();
ProgramState globalState = cfr.getGlobalState();
if (globalState == null) {
globalState = new ProgramState(gui, imProg, true);
}
if (interpreter == null) {
interpreter = new ILInterpreter(imProg, gui, Optional.empty(), globalState);
interpreter.addNativeProvider(new ReflectionNativeProvider(interpreter));
}
// Use try-with-resources to ensure NativeProvider resources (e.g. SQLite database connections and file handles)
// created during test execution are automatically closed when the language server finishes processing the request.
try (CompiletimeFunctionRunner cfr = new CompiletimeFunctionRunner(translator, imProg, Optional.empty(), null, gui,
CompiletimeFunctions, WurstProjectConfigData.empty(), false, false)) {
ILInterpreter interpreter = cfr.getInterpreter();
ProgramState globalState = cfr.getGlobalState();
if (globalState == null) {
globalState = new ProgramState(gui, imProg, true);
}
if (interpreter == null) {
interpreter = new ILInterpreter(imProg, gui, Optional.empty(), globalState);
interpreter.addNativeProvider(new ReflectionNativeProvider(interpreter));
}

redirectInterpreterOutput(globalState);

Expand Down Expand Up @@ -355,6 +357,7 @@ public TestResult runTests(ImTranslator translator, ImProg imProg, Optional<Func

WLogger.info("finished tests");
return new TestResult(successTests.size(), successTests.size() + failTests.size());
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import static de.peeeq.wurstscript.validation.GlobalCaches.LOCAL_STATE_CACHE;
import static de.peeeq.wurstscript.validation.GlobalCaches.LOCAL_STATE_NOARG_CACHE;

public class ILInterpreter implements AbstractInterpreter {
public class ILInterpreter implements AbstractInterpreter, AutoCloseable {
private ImProg prog;
private final ProgramState globalState;
private final TimerMockHandler timerMockHandler = new TimerMockHandler();
Expand Down Expand Up @@ -570,4 +570,14 @@ public int getMaxInstanceCount(int val) {
}
return (int) count;
}

/**
* Closes the interpreter by shutting down its global ProgramState and releasing registered native provider resources.
*/
@Override
public void close() {
if (globalState != null) {
globalState.close();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@

import java.io.PrintStream;

public interface NativesProvider {
public interface NativesProvider extends AutoCloseable {

ILconst invoke(String funcname, ILconst[] args) throws NoSuchNativeException;

void setOutStream(PrintStream outStream);

/**
* Optional lifecycle hook to release any resources (e.g. SQLite database connections, JDBC statements)
* held by this provider when interpreter execution completes.
*/
@Override
default void close() {
// Default implementation does nothing
}

}
Loading