From 327b634c3d37cdb28787292a93b93c1b16b5f55b Mon Sep 17 00:00:00 2001 From: Avinash Kumar Deepak Date: Mon, 13 Apr 2026 15:28:30 +0530 Subject: [PATCH] refactor(java): share runtime core for concore and concoredocker --- .github/workflows/ci.yml | 3 +- ConcoreJavaRuntimeCore.java | 774 +++++++++++++++++++++++++++++++++++ TestConcoreApi.java | 285 +++++++++++++ concore.java | 776 ++---------------------------------- concoredocker.java | 775 ++--------------------------------- 5 files changed, 1111 insertions(+), 1502 deletions(-) create mode 100644 ConcoreJavaRuntimeCore.java create mode 100644 TestConcoreApi.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c61a167..51f238e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,12 +71,13 @@ jobs: - name: Compile Java tests run: | - javac -cp jeromq.jar concoredocker.java TestLiteralEval.java TestConcoredockerApi.java + javac -cp jeromq.jar ConcoreJavaRuntimeCore.java concore.java concoredocker.java TestLiteralEval.java TestConcoredockerApi.java TestConcoreApi.java - name: Run Java tests run: | java -cp .:jeromq.jar TestLiteralEval java -cp .:jeromq.jar TestConcoredockerApi + java -cp .:jeromq.jar TestConcoreApi docker-build: runs-on: ubuntu-latest diff --git a/ConcoreJavaRuntimeCore.java b/ConcoreJavaRuntimeCore.java new file mode 100644 index 0000000..d371080 --- /dev/null +++ b/ConcoreJavaRuntimeCore.java @@ -0,0 +1,774 @@ +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.zeromq.ZMQ; + +class ConcoreJavaRuntimeCore { + private Map iport = new HashMap<>(); + private Map oport = new HashMap<>(); + private String s = ""; + private String olds = ""; + private int delay = 1000; + private int retrycount = 0; + private int maxRetries = 5; + private String inpath; + private String outpath; + private final boolean suffixPortPath; + private Map params = new HashMap<>(); + private Map zmqPorts = new HashMap<>(); + private ZMQ.Context zmqContext = null; + private double simtime = 0; + private double maxtime; + + ConcoreJavaRuntimeCore(String inpath, String outpath, boolean suffixPortPath) { + this.inpath = inpath; + this.outpath = outpath; + this.suffixPortPath = suffixPortPath; + initialize(); + } + + private void initialize() { + try { + iport = parseFile("concore.iport"); + } catch (IOException e) { + } + try { + oport = parseFile("concore.oport"); + } catch (IOException e) { + } + try { + String paramsFile = Paths.get(portPath(inpath, 1), "concore.params").toString(); + String sparams = new String(Files.readAllBytes(Paths.get(paramsFile)), java.nio.charset.StandardCharsets.UTF_8); + if (sparams.length() > 0 && sparams.charAt(0) == '"') { + sparams = sparams.substring(1); + sparams = sparams.substring(0, sparams.indexOf('"')); + } + params = parseParams(sparams); + } catch (IOException e) { + params = new HashMap<>(); + } + defaultMaxTime(100); + } + + private String portPath(String base, int portNum) { + if (suffixPortPath) { + return base + portNum; + } + return Paths.get(base, String.valueOf(portNum)).toString(); + } + + private Path resolvePortFilePath(String base, int portNum, String name) throws IOException { + Path portDir = Paths.get(portPath(base, portNum)).toAbsolutePath().normalize(); + Path filePath = portDir.resolve(name).normalize(); + if (!filePath.startsWith(portDir)) { + throw new IOException("Invalid file name '" + name + "' for port " + portNum); + } + return filePath; + } + + private static Map parseParams(String sparams) { + Map result = new HashMap<>(); + if (sparams == null || sparams.isEmpty()) return result; + String trimmed = sparams.trim(); + if (trimmed.startsWith("{") && trimmed.endsWith("}")) { + try { + Object val = literalEval(trimmed); + if (val instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) val; + return map; + } + } catch (Exception e) { + } + } + for (String item : trimmed.split(";")) { + if (item.contains("=")) { + String[] parts = item.split("=", 2); + String key = parts[0].trim(); + String value = parts[1].trim(); + try { + result.put(key, literalEval(value)); + } catch (Exception e) { + result.put(key, value); + } + } + } + return result; + } + + private static Map parseFile(String filename) throws IOException { + String content = new String(Files.readAllBytes(Paths.get(filename)), java.nio.charset.StandardCharsets.UTF_8); + content = content.trim(); + if (content.isEmpty()) { + return new HashMap<>(); + } + try { + Object result = literalEval(content); + if (result instanceof Map) { + @SuppressWarnings("unchecked") + Map map = (Map) result; + return map; + } + } catch (IllegalArgumentException e) { + System.err.println("Failed to parse file as map: " + filename + " (" + e.getMessage() + ")"); + } + return new HashMap<>(); + } + + void setInPath(String path) { inpath = path; } + void setOutPath(String path) { outpath = path; } + void setDelay(int ms) { delay = ms; } + double getSimtime() { return simtime; } + void resetState() { s = ""; olds = ""; simtime = 0; } + + boolean unchanged() { + if (olds.equals(s)) { + s = ""; + return true; + } + olds = s; + return false; + } + + Object tryParam(String n, Object i) { + if (params.containsKey(n)) { + return params.get(n); + } + return i; + } + + void defaultMaxTime(double defaultValue) { + try { + String maxtimeFile = Paths.get(portPath(inpath, 1), "concore.maxtime").toString(); + String content = new String(Files.readAllBytes(Paths.get(maxtimeFile))); + Object parsed = literalEval(content.trim()); + if (parsed instanceof Number) { + maxtime = ((Number) parsed).doubleValue(); + } else { + maxtime = defaultValue; + } + } catch (IOException | RuntimeException e) { + maxtime = defaultValue; + } + } + + ReadResult readFilePort(int port, String name, String initstr) { + List defaultVal = new ArrayList<>(); + try { + List parsed = (List) literalEval(initstr); + if (parsed.size() > 1) { + defaultVal = new ArrayList<>(parsed.subList(1, parsed.size())); + } + } catch (Exception e) { + } + + Path filePathObj; + try { + filePathObj = resolvePortFilePath(inpath, port, name); + } catch (IOException | RuntimeException e) { + System.out.println("Invalid path for port " + port + " and name '" + name + "': " + e.getMessage()); + return new ReadResult(ReadStatus.PARSE_ERROR, defaultVal); + } + String filePath = filePathObj.toString(); + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + s += initstr; + return new ReadResult(ReadStatus.TIMEOUT, defaultVal); + } + + String ins; + try { + ins = new String(Files.readAllBytes(filePathObj)); + } catch (IOException e) { + System.out.println("File " + filePath + " not found, using default value."); + s += initstr; + return new ReadResult(ReadStatus.FILE_NOT_FOUND, defaultVal); + } + + int attempts = 0; + while (ins.length() == 0 && attempts < maxRetries) { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + s += initstr; + return new ReadResult(ReadStatus.TIMEOUT, defaultVal); + } + try { + ins = new String(Files.readAllBytes(filePathObj)); + } catch (IOException e) { + System.out.println("Retry " + (attempts + 1) + ": Error reading " + filePath); + } + attempts++; + retrycount++; + } + + if (ins.length() == 0) { + System.out.println("Max retries reached for " + filePath + ", using default value."); + return new ReadResult(ReadStatus.RETRIES_EXCEEDED, defaultVal); + } + + s += ins; + try { + List inval = (List) literalEval(ins); + if (!inval.isEmpty()) { + double firstSimtime = ((Number) inval.get(0)).doubleValue(); + simtime = Math.max(simtime, firstSimtime); + return new ReadResult(ReadStatus.SUCCESS, new ArrayList<>(inval.subList(1, inval.size()))); + } + } catch (Exception e) { + System.out.println("Error parsing " + ins + ": " + e.getMessage()); + } + return new ReadResult(ReadStatus.PARSE_ERROR, defaultVal); + } + + void writeFilePort(int port, String name, Object val, int delta) { + try { + Path pathObj = resolvePortFilePath(outpath, port, name); + StringBuilder content = new StringBuilder(); + if (val instanceof String) { + Thread.sleep(2 * delay); + content.append(val); + } else if (val instanceof List) { + List listVal = (List) val; + content.append("["); + content.append(toPythonLiteral(simtime + delta)); + for (int i = 0; i < listVal.size(); i++) { + content.append(", "); + content.append(toPythonLiteral(listVal.get(i))); + } + content.append("]"); + } else if (val instanceof Object[]) { + Object[] arrayVal = (Object[]) val; + content.append("["); + content.append(toPythonLiteral(simtime + delta)); + for (Object o : arrayVal) { + content.append(", "); + content.append(toPythonLiteral(o)); + } + content.append("]"); + } else { + System.out.println("write must have list or str"); + return; + } + Files.write(pathObj, content.toString().getBytes()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + System.out.println("skipping " + outpath + "/" + port + "/" + name); + } catch (IOException e) { + System.out.println("skipping " + outpath + "/" + port + "/" + name); + } + } + + List initVal(String simtimeVal) { + List val = new ArrayList<>(); + try { + List inval = (List) literalEval(simtimeVal); + if (!inval.isEmpty()) { + simtime = ((Number) inval.get(0)).doubleValue(); + val = new ArrayList<>(inval.subList(1, inval.size())); + } + } catch (Exception e) { + System.out.println("Error parsing initVal: " + e.getMessage()); + } + return val; + } + + private ZMQ.Context getZmqContext() { + if (zmqContext == null) { + zmqContext = ZMQ.context(1); + } + return zmqContext; + } + + void initZmqPort(String portName, String portType, String address, String socketTypeStr) { + if (zmqPorts.containsKey(portName)) return; + int sockType = zmqSocketTypeFromString(socketTypeStr); + if (sockType == -1) { + System.err.println("initZmqPort: unknown socket type '" + socketTypeStr + "'"); + return; + } + zmqPorts.put(portName, new ZeroMQPort(portType, address, sockType)); + } + + void terminateZmq() { + for (Map.Entry entry : zmqPorts.entrySet()) { + entry.getValue().socket.close(); + } + zmqPorts.clear(); + if (zmqContext != null) { + zmqContext.term(); + zmqContext = null; + } + } + + private static int zmqSocketTypeFromString(String s) { + switch (s.toUpperCase()) { + case "REQ": return ZMQ.REQ; + case "REP": return ZMQ.REP; + case "PUB": return ZMQ.PUB; + case "SUB": return ZMQ.SUB; + case "PUSH": return ZMQ.PUSH; + case "PULL": return ZMQ.PULL; + case "PAIR": return ZMQ.PAIR; + default: return -1; + } + } + + ReadResult readZmqPort(String portName, String name, String initstr) { + List defaultVal = new ArrayList<>(); + try { + List parsed = (List) literalEval(initstr); + if (parsed.size() > 1) { + defaultVal = new ArrayList<>(parsed.subList(1, parsed.size())); + } + } catch (Exception e) { + } + ZeroMQPort port = zmqPorts.get(portName); + if (port == null) { + System.err.println("read: ZMQ port '" + portName + "' not initialized"); + return new ReadResult(ReadStatus.FILE_NOT_FOUND, defaultVal); + } + String msg = port.recvWithRetry(); + if (msg == null) { + System.err.println("read: ZMQ recv timeout on port '" + portName + "'"); + return new ReadResult(ReadStatus.TIMEOUT, defaultVal); + } + s += msg; + try { + List inval = (List) literalEval(msg); + if (!inval.isEmpty()) { + simtime = Math.max(simtime, ((Number) inval.get(0)).doubleValue()); + return new ReadResult(ReadStatus.SUCCESS, new ArrayList<>(inval.subList(1, inval.size()))); + } + } catch (Exception e) { + System.out.println("Error parsing ZMQ message '" + msg + "': " + e.getMessage()); + } + return new ReadResult(ReadStatus.PARSE_ERROR, defaultVal); + } + + void writeZmqPort(String portName, String name, Object val, int delta) { + ZeroMQPort port = zmqPorts.get(portName); + if (port == null) { + System.err.println("write: ZMQ port '" + portName + "' not initialized"); + return; + } + String payload; + if (val instanceof List) { + List listVal = (List) val; + StringBuilder sb = new StringBuilder("["); + sb.append(toJsonLiteral(simtime + delta)); + for (Object o : listVal) { + sb.append(", "); + sb.append(toJsonLiteral(o)); + } + sb.append("]"); + payload = sb.toString(); + } else if (val instanceof String) { + payload = (String) val; + } else { + System.out.println("write must have list or str"); + return; + } + port.sendWithRetry(payload); + } + + private static String escapePythonString(String s) { + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '\\': sb.append("\\\\"); break; + case '\'': sb.append("\\'"); break; + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + default: sb.append(c); break; + } + } + return sb.toString(); + } + + private static String toPythonLiteral(Object obj) { + if (obj == null) return "None"; + if (obj instanceof Boolean) return ((Boolean) obj) ? "True" : "False"; + if (obj instanceof String) return "'" + escapePythonString((String) obj) + "'"; + if (obj instanceof Number) return obj.toString(); + if (obj instanceof List) { + List list = (List) obj; + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < list.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(toPythonLiteral(list.get(i))); + } + sb.append("]"); + return sb.toString(); + } + if (obj instanceof Map) { + Map map = (Map) obj; + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry entry : map.entrySet()) { + if (!first) sb.append(", "); + sb.append(toPythonLiteral(entry.getKey())).append(": ").append(toPythonLiteral(entry.getValue())); + first = false; + } + sb.append("}"); + return sb.toString(); + } + return obj.toString(); + } + + private static String escapeJsonString(String s) { + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '\\': sb.append("\\\\"); break; + case '"': sb.append("\\\""); break; + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + default: sb.append(c); break; + } + } + return sb.toString(); + } + + private static String toJsonLiteral(Object obj) { + if (obj == null) return "null"; + if (obj instanceof Boolean) return ((Boolean) obj) ? "true" : "false"; + if (obj instanceof String) return "\"" + escapeJsonString((String) obj) + "\""; + if (obj instanceof Number) return obj.toString(); + if (obj instanceof List) { + List list = (List) obj; + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < list.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(toJsonLiteral(list.get(i))); + } + sb.append("]"); + return sb.toString(); + } + if (obj instanceof Map) { + Map map = (Map) obj; + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry entry : map.entrySet()) { + if (!first) sb.append(", "); + sb.append(toJsonLiteral(entry.getKey())).append(": ").append(toJsonLiteral(entry.getValue())); + first = false; + } + sb.append("}"); + return sb.toString(); + } + return obj.toString(); + } + + static Object literalEval(String s) { + if (s == null) throw new IllegalArgumentException("Input cannot be null"); + s = s.trim(); + if (s.isEmpty()) throw new IllegalArgumentException("Input cannot be empty"); + Parser parser = new Parser(s); + Object result = parser.parseExpression(); + parser.skipWhitespace(); + if (parser.pos < parser.input.length()) { + throw new IllegalArgumentException("Unexpected trailing content at position " + parser.pos); + } + return result; + } + + enum ReadStatus { + SUCCESS, FILE_NOT_FOUND, TIMEOUT, PARSE_ERROR, RETRIES_EXCEEDED + } + + static class ReadResult { + final ReadStatus status; + final List data; + ReadResult(ReadStatus status, List data) { + this.status = status; + this.data = data; + } + } + + private class ZeroMQPort { + final ZMQ.Socket socket; + + ZeroMQPort(String portType, String address, int socketType) { + ZMQ.Context ctx = getZmqContext(); + this.socket = ctx.socket(socketType); + this.socket.setReceiveTimeOut(2000); + this.socket.setSendTimeOut(2000); + this.socket.setLinger(0); + if (portType.equals("bind")) { + this.socket.bind(address); + } else { + this.socket.connect(address); + } + } + + String recvWithRetry() { + for (int attempt = 0; attempt < 5; attempt++) { + String msg = socket.recvStr(); + if (msg != null) return msg; + try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } + } + return null; + } + + void sendWithRetry(String message) { + for (int attempt = 0; attempt < 5; attempt++) { + if (socket.send(message)) return; + try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } + } + } + } + + private static class Parser { + final String input; + int pos; + + Parser(String input) { + this.input = input; + this.pos = 0; + } + + void skipWhitespace() { + while (pos < input.length() && Character.isWhitespace(input.charAt(pos))) { + pos++; + } + } + + boolean hasMore() { + skipWhitespace(); + return pos < input.length(); + } + + char advance() { + char c = input.charAt(pos); + pos++; + return c; + } + + Object parseExpression() { + skipWhitespace(); + if (pos >= input.length()) throw new IllegalArgumentException("Unexpected end of input"); + char c = input.charAt(pos); + + if (c == '{') return parseDict(); + if (c == '[') return parseList(); + if (c == '(') return parseTuple(); + if (c == '\'' || c == '"') return parseString(); + if (c == '-' || c == '+' || Character.isDigit(c)) return parseNumber(); + return parseKeyword(); + } + + Map parseDict() { + Map map = new HashMap<>(); + pos++; + skipWhitespace(); + if (hasMore() && input.charAt(pos) == '}') { + pos++; + return map; + } + while (true) { + skipWhitespace(); + Object key = parseExpression(); + skipWhitespace(); + if (pos >= input.length() || input.charAt(pos) != ':') { + throw new IllegalArgumentException("Expected ':' in dict at position " + pos); + } + pos++; + skipWhitespace(); + Object value = parseExpression(); + if (!(key instanceof String)) { + throw new IllegalArgumentException("Dict keys must be non-null strings, but got: " + + (key == null ? "null" : key.getClass().getSimpleName())); + } + map.put((String) key, value); + skipWhitespace(); + if (pos >= input.length()) { + throw new IllegalArgumentException("Unterminated dict: missing '}'"); + } + if (input.charAt(pos) == '}') { + pos++; + break; + } + if (input.charAt(pos) == ',') { + pos++; + skipWhitespace(); + if (hasMore() && input.charAt(pos) == '}') { + pos++; + break; + } + } else { + throw new IllegalArgumentException("Expected ',' or '}' in dict at position " + pos); + } + } + return map; + } + + List parseList() { + List list = new ArrayList<>(); + pos++; + skipWhitespace(); + if (hasMore() && input.charAt(pos) == ']') { + pos++; + return list; + } + while (true) { + skipWhitespace(); + list.add(parseExpression()); + skipWhitespace(); + if (pos >= input.length()) { + throw new IllegalArgumentException("Unterminated list: missing ']'"); + } + if (input.charAt(pos) == ']') { + pos++; + break; + } + if (input.charAt(pos) == ',') { + pos++; + skipWhitespace(); + if (hasMore() && input.charAt(pos) == ']') { + pos++; + break; + } + } else { + throw new IllegalArgumentException("Expected ',' or ']' in list at position " + pos); + } + } + return list; + } + + List parseTuple() { + List list = new ArrayList<>(); + pos++; + skipWhitespace(); + if (hasMore() && input.charAt(pos) == ')') { + pos++; + return list; + } + while (true) { + skipWhitespace(); + list.add(parseExpression()); + skipWhitespace(); + if (pos >= input.length()) { + throw new IllegalArgumentException("Unterminated tuple: missing ')'"); + } + if (input.charAt(pos) == ')') { + pos++; + break; + } + if (input.charAt(pos) == ',') { + pos++; + skipWhitespace(); + if (hasMore() && input.charAt(pos) == ')') { + pos++; + break; + } + } else { + throw new IllegalArgumentException("Expected ',' or ')' in tuple at position " + pos); + } + } + return list; + } + + String parseString() { + char quote = advance(); + StringBuilder sb = new StringBuilder(); + while (pos < input.length()) { + char c = input.charAt(pos); + if (c == '\\' && pos + 1 < input.length()) { + pos++; + char escaped = input.charAt(pos); + switch (escaped) { + case 'n': sb.append('\n'); break; + case 't': sb.append('\t'); break; + case 'r': sb.append('\r'); break; + case '\\': sb.append('\\'); break; + case '\'': sb.append('\''); break; + case '"': sb.append('"'); break; + default: sb.append('\\').append(escaped); break; + } + pos++; + } else if (c == quote) { + pos++; + return sb.toString(); + } else { + sb.append(c); + pos++; + } + } + throw new IllegalArgumentException("Unterminated string starting at position " + (pos - sb.length() - 1)); + } + + Number parseNumber() { + int start = pos; + if (pos < input.length() && (input.charAt(pos) == '-' || input.charAt(pos) == '+')) { + pos++; + } + boolean hasDecimal = false; + boolean hasExponent = false; + while (pos < input.length()) { + char c = input.charAt(pos); + if (Character.isDigit(c)) { + pos++; + } else if (c == '.' && !hasDecimal && !hasExponent) { + hasDecimal = true; + pos++; + } else if ((c == 'e' || c == 'E') && !hasExponent) { + hasExponent = true; + pos++; + if (pos < input.length() && (input.charAt(pos) == '+' || input.charAt(pos) == '-')) { + pos++; + } + } else { + break; + } + } + String numStr = input.substring(start, pos); + try { + if (hasDecimal || hasExponent) { + return Double.parseDouble(numStr); + } + try { + return Integer.parseInt(numStr); + } catch (NumberFormatException e) { + return Long.parseLong(numStr); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid number: '" + numStr + "' at position " + start); + } + } + + Object parseKeyword() { + int start = pos; + while ((pos < input.length() && Character.isLetterOrDigit(input.charAt(pos))) + || (pos < input.length() && input.charAt(pos) == '_')) { + pos++; + } + String word = input.substring(start, pos); + switch (word) { + case "True": + case "true": + return Boolean.TRUE; + case "False": + case "false": + return Boolean.FALSE; + case "None": + case "null": + return null; + default: + throw new IllegalArgumentException("Unknown keyword: '" + word + "' at position " + start); + } + } + } +} \ No newline at end of file diff --git a/TestConcoreApi.java b/TestConcoreApi.java new file mode 100644 index 0000000..fd56dfc --- /dev/null +++ b/TestConcoreApi.java @@ -0,0 +1,285 @@ +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +/** + * Tests for concore read(), write(), unchanged(), initVal() + * using temp directories for file-based IPC. + */ +public class TestConcoreApi { + static int passed = 0; + static int failed = 0; + + public static void main(String[] args) { + concore.setDelay(0); + + testWriteProducesCorrectFormat(); + testReadParsesFileAndStripsSimtime(); + testReadWriteRoundtrip(); + testSimtimeAdvancesWithDelta(); + testUnchangedReturnsFalseAfterRead(); + testUnchangedReturnsTrueOnSameData(); + testInitValExtractsSimtime(); + testInitValReturnsRemainingValues(); + testOutputFileMatchesPythonWireFormat(); + testReadFileNotFound(); + testReadRetriesExceeded(); + testReadParseError(); + testReadTraversalBlocked(); + testWriteTraversalBlocked(); + + System.out.println("\n=== Results: " + passed + " passed, " + failed + " failed out of " + (passed + failed) + " tests ==="); + if (failed > 0) { + System.exit(1); + } + } + + static void check(String testName, Object expected, Object actual) { + if (Objects.equals(expected, actual)) { + System.out.println("PASS: " + testName); + passed++; + } else { + System.out.println("FAIL: " + testName + " | expected: " + expected + " | actual: " + actual); + failed++; + } + } + + static Path makeTempDir() { + try { + return Files.createTempDirectory("concore_local_test_"); + } catch (IOException e) { + throw new RuntimeException("Failed to create temp dir", e); + } + } + + static String basePath(Path tmp) { + return tmp.resolve("in").toString(); + } + + static Path portDir(String base, int port) { + return Paths.get(base + port); + } + + static void writeFile(String base, int port, String name, String content) { + try { + Path dir = portDir(base, port); + Files.createDirectories(dir); + Files.write(dir.resolve(name), content.getBytes()); + } catch (IOException e) { + throw new RuntimeException("Failed to write test file", e); + } + } + + static String readFile(String base, int port, String name) { + try { + return new String(Files.readAllBytes(portDir(base, port).resolve(name))); + } catch (IOException e) { + throw new RuntimeException("Failed to read test file", e); + } + } + + static void testWriteProducesCorrectFormat() { + Path tmp = makeTempDir(); + String base = basePath(tmp); + concore.resetState(); + concore.setOutPath(base); + try { + Files.createDirectories(portDir(base, 1)); + } catch (IOException e) { + throw new RuntimeException(e); + } + + List vals = new ArrayList<>(); + vals.add(10.0); + vals.add(20.0); + concore.write(1, "signal", vals, 1); + + String content = readFile(base, 1, "signal"); + @SuppressWarnings("unchecked") + List parsed = (List) concore.literalEval(content); + check("write: simtime+delta as first element", 1.0, parsed.get(0)); + check("write: val1 correct", 10.0, parsed.get(1)); + check("write: val2 correct", 20.0, parsed.get(2)); + } + + static void testReadParsesFileAndStripsSimtime() { + Path tmp = makeTempDir(); + String base = basePath(tmp); + concore.resetState(); + concore.setInPath(base); + writeFile(base, 1, "sensor", "[0.0, 42.0, 99.0]"); + + concore.ReadResult result = concore.read(1, "sensor", "[0.0, 0.0, 0.0]"); + check("read: status SUCCESS", concore.ReadStatus.SUCCESS, result.status); + check("read: strips simtime, size=2", 2, result.data.size()); + check("read: val1 correct", 42.0, result.data.get(0)); + check("read: val2 correct", 99.0, result.data.get(1)); + } + + static void testReadWriteRoundtrip() { + Path tmp = makeTempDir(); + String base = basePath(tmp); + concore.resetState(); + concore.setInPath(base); + concore.setOutPath(base); + try { + Files.createDirectories(portDir(base, 1)); + } catch (IOException e) { + throw new RuntimeException(e); + } + + List outVals = new ArrayList<>(); + outVals.add(7.0); + outVals.add(8.0); + concore.write(1, "data", outVals, 1); + + concore.ReadResult inVals = concore.read(1, "data", "[0.0, 0.0, 0.0]"); + check("roundtrip: status", concore.ReadStatus.SUCCESS, inVals.status); + check("roundtrip: size", 2, inVals.data.size()); + check("roundtrip: val1", 7.0, inVals.data.get(0)); + check("roundtrip: val2", 8.0, inVals.data.get(1)); + } + + static void testSimtimeAdvancesWithDelta() { + Path tmp = makeTempDir(); + String base = basePath(tmp); + concore.resetState(); + concore.setInPath(base); + concore.setOutPath(base); + try { + Files.createDirectories(portDir(base, 1)); + } catch (IOException e) { + throw new RuntimeException(e); + } + + List v = Collections.singletonList((Object) 1.0); + + concore.write(1, "tick", v, 1); + concore.read(1, "tick", "[0.0, 0.0]"); + check("simtime after iter 1", 1.0, concore.getSimtime()); + + concore.write(1, "tick", v, 1); + concore.read(1, "tick", "[0.0, 0.0]"); + check("simtime after iter 2", 2.0, concore.getSimtime()); + } + + static void testUnchangedReturnsFalseAfterRead() { + Path tmp = makeTempDir(); + String base = basePath(tmp); + concore.resetState(); + concore.setInPath(base); + writeFile(base, 1, "sig", "[0.0, 5.0]"); + + concore.read(1, "sig", "[0.0, 0.0]"); + check("unchanged: false right after read", false, concore.unchanged()); + } + + static void testUnchangedReturnsTrueOnSameData() { + Path tmp = makeTempDir(); + String base = basePath(tmp); + concore.resetState(); + concore.setInPath(base); + writeFile(base, 1, "sig", "[0.0, 5.0]"); + + concore.read(1, "sig", "[0.0, 0.0]"); + concore.unchanged(); + check("unchanged: true on second call with same data", true, concore.unchanged()); + } + + static void testInitValExtractsSimtime() { + concore.resetState(); + concore.initVal("[2.0, 10, 20]"); + check("initVal: simtime extracted", 2.0, concore.getSimtime()); + } + + static void testInitValReturnsRemainingValues() { + concore.resetState(); + List result = concore.initVal("[3.5, 100, 200]"); + check("initVal: size of returned list", 2, result.size()); + check("initVal: first remaining val", 100, result.get(0)); + check("initVal: second remaining val", 200, result.get(1)); + } + + static void testOutputFileMatchesPythonWireFormat() { + Path tmp = makeTempDir(); + String base = basePath(tmp); + concore.resetState(); + concore.setOutPath(base); + try { + Files.createDirectories(portDir(base, 1)); + } catch (IOException e) { + throw new RuntimeException(e); + } + + List vals = new ArrayList<>(); + vals.add(1.0); + vals.add(2.0); + concore.write(1, "out", vals, 0); + + String raw = readFile(base, 1, "out"); + check("wire format: starts with '['", true, raw.startsWith("[")); + check("wire format: ends with ']'", true, raw.endsWith("]")); + Object reparsed = concore.literalEval(raw); + check("wire format: re-parseable as list", true, reparsed instanceof List); + } + + static void testReadFileNotFound() { + Path tmp = makeTempDir(); + String base = basePath(tmp); + concore.resetState(); + concore.setInPath(base); + concore.ReadResult result = concore.read(1, "missing", "[0.0, 0.0]"); + check("read file not found: status", concore.ReadStatus.FILE_NOT_FOUND, result.status); + check("read file not found: data is default", 1, result.data.size()); + } + + static void testReadRetriesExceeded() { + Path tmp = makeTempDir(); + String base = basePath(tmp); + concore.resetState(); + concore.setInPath(base); + writeFile(base, 1, "empty", ""); + concore.ReadResult result = concore.read(1, "empty", "[0.0, 0.0]"); + check("read retries exceeded: status", concore.ReadStatus.RETRIES_EXCEEDED, result.status); + check("read retries exceeded: data is default", 1, result.data.size()); + } + + static void testReadParseError() { + Path tmp = makeTempDir(); + String base = basePath(tmp); + concore.resetState(); + concore.setInPath(base); + writeFile(base, 1, "bad", "not_a_valid_list"); + concore.ReadResult result = concore.read(1, "bad", "[0.0, 0.0]"); + check("read parse error: status", concore.ReadStatus.PARSE_ERROR, result.status); + check("read parse error: data is default", 1, result.data.size()); + } + + static void testReadTraversalBlocked() { + Path tmp = makeTempDir(); + String base = basePath(tmp); + concore.resetState(); + concore.setInPath(base); + + concore.ReadResult result = concore.read(1, "../escape", "[0.0, 7.0]"); + check("read traversal blocked: status", concore.ReadStatus.PARSE_ERROR, result.status); + check("read traversal blocked: returns default", 1, result.data.size()); + } + + static void testWriteTraversalBlocked() { + Path tmp = makeTempDir(); + String base = basePath(tmp); + concore.resetState(); + concore.setOutPath(base); + try { + Files.createDirectories(portDir(base, 1)); + } catch (IOException e) { + throw new RuntimeException(e); + } + + concore.write(1, "../escape", Collections.singletonList((Object) 1.0), 0); + check("write traversal blocked: no escaped file", false, Files.exists(tmp.resolve("escape"))); + } +} diff --git a/concore.java b/concore.java index 52b96f4..c5178fe 100644 --- a/concore.java +++ b/concore.java @@ -6,11 +6,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.util.HashMap; -import java.util.Map; import java.util.ArrayList; import java.util.List; -import org.zeromq.ZMQ; /** * Java implementation of concore local communication. @@ -19,22 +16,7 @@ * mirroring the functionality of concore.py. */ public class concore { - private static Map iport = new HashMap<>(); - private static Map oport = new HashMap<>(); - private static String s = ""; - private static String olds = ""; - // delay in milliseconds (Python uses time.sleep(1) = 1 second) - private static int delay = 1000; - private static int retrycount = 0; - private static int maxRetries = 5; - private static String inpath = "./in"; - private static String outpath = "./out"; - private static Map params = new HashMap<>(); - private static Map zmqPorts = new HashMap<>(); - private static ZMQ.Context zmqContext = null; - // simtime as double to preserve fractional values (e.g. "[0.0, ...]") - private static double simtime = 0; - private static double maxtime; + private static final ConcoreJavaRuntimeCore runtime = new ConcoreJavaRuntimeCore("./in", "./out", true); private static final Path BASE_DIR = Paths.get("").toAbsolutePath().normalize(); private static final Path PID_REGISTRY_FILE = BASE_DIR.resolve("concorekill_pids.txt"); @@ -47,26 +29,6 @@ public class concore { writeKillScript(); Runtime.getRuntime().addShutdownHook(new Thread(concore::cleanupPid)); } - try { - iport = parseFile("concore.iport"); - } catch (IOException e) { - } - try { - oport = parseFile("concore.oport"); - } catch (IOException e) { - } - try { - String paramsFile = Paths.get(portPath(inpath, 1), "concore.params").toString(); - String sparams = new String(Files.readAllBytes(Paths.get(paramsFile)), java.nio.charset.StandardCharsets.UTF_8); - if (sparams.length() > 0 && sparams.charAt(0) == '"') { // windows keeps "" need to remove - sparams = sparams.substring(1); - sparams = sparams.substring(0, sparams.indexOf('"')); - } - params = parseParams(sparams); - } catch (IOException e) { - params = new HashMap<>(); - } - defaultMaxTime(100); Runtime.getRuntime().addShutdownHook(new Thread(concore::terminateZmq)); } @@ -146,117 +108,27 @@ private static void writeKillScript() { } } - /** - * Parses a param string into a map, matching concore_base.parse_params. - * Tries dict literal first, then falls back to semicolon-separated key=value pairs. - */ - private static Map parseParams(String sparams) { - Map result = new HashMap<>(); - if (sparams == null || sparams.isEmpty()) return result; - String trimmed = sparams.trim(); - if (trimmed.startsWith("{") && trimmed.endsWith("}")) { - try { - Object val = literalEval(trimmed); - if (val instanceof Map) { - @SuppressWarnings("unchecked") - Map map = (Map) val; - return map; - } - } catch (Exception e) { - } - } - for (String item : trimmed.split(";")) { - if (item.contains("=")) { - String[] parts = item.split("=", 2); // split on first '=' only - String key = parts[0].trim(); - String value = parts[1].trim(); - try { - result.put(key, literalEval(value)); - } catch (Exception e) { - result.put(key, value); - } - } - } - return result; - } - - /** - * Parses a file containing a Python-style dictionary literal. - * Returns empty map if file is empty or malformed (matches Python safe_literal_eval). - */ - private static Map parseFile(String filename) throws IOException { - String content = new String(Files.readAllBytes(Paths.get(filename)), java.nio.charset.StandardCharsets.UTF_8); - content = content.trim(); - if (content.isEmpty()) { - return new HashMap<>(); - } - try { - Object result = literalEval(content); - if (result instanceof Map) { - @SuppressWarnings("unchecked") - Map map = (Map) result; - return map; - } - } catch (IllegalArgumentException e) { - System.err.println("Failed to parse file as map: " + filename + " (" + e.getMessage() + ")"); - } - return new HashMap<>(); - } - /** * Sets maxtime from concore.maxtime file, or uses defaultValue if file not found. * Catches both IOException and RuntimeException to match Python safe_literal_eval. */ public static void defaultMaxTime(double defaultValue) { - try { - String maxtimeFile = Paths.get(portPath(inpath, 1), "concore.maxtime").toString(); - String content = new String(Files.readAllBytes(Paths.get(maxtimeFile))); - Object parsed = literalEval(content.trim()); - if (parsed instanceof Number) { - maxtime = ((Number) parsed).doubleValue(); - } else { - maxtime = defaultValue; - } - } catch (IOException | RuntimeException e) { - maxtime = defaultValue; - } - } - - private static String portPath(String base, int portNum) { - return base + portNum; - } - - private static Path resolvePortFilePath(String base, int portNum, String name) throws IOException { - Path portDir = Paths.get(portPath(base, portNum)).toAbsolutePath().normalize(); - Path filePath = portDir.resolve(name).normalize(); - if (!filePath.startsWith(portDir)) { - throw new IOException("Invalid file name '" + name + "' for port " + portNum); - } - return filePath; + runtime.defaultMaxTime(defaultValue); } // package-level helpers for testing with temp directories - static void setInPath(String path) { inpath = path; } - static void setOutPath(String path) { outpath = path; } - static void setDelay(int ms) { delay = ms; } - static double getSimtime() { return simtime; } - static void resetState() { s = ""; olds = ""; simtime = 0; } + static void setInPath(String path) { runtime.setInPath(path); } + static void setOutPath(String path) { runtime.setOutPath(path); } + static void setDelay(int ms) { runtime.setDelay(ms); } + static double getSimtime() { return runtime.getSimtime(); } + static void resetState() { runtime.resetState(); } public static boolean unchanged() { - if (olds.equals(s)) { - s = ""; - return true; - } - olds = s; - return false; + return runtime.unchanged(); } public static Object tryParam(String n, Object i) { - if (params.containsKey(n)) { - return params.get(n); - } else { - return i; - } + return runtime.tryParam(n, i); } /** @@ -266,185 +138,7 @@ public static Object tryParam(String n, Object i) { * Includes max retry limit to avoid infinite blocking (matches Python behavior). */ public static ReadResult read(int port, String name, String initstr) { - // Parse default value upfront for consistent return type - List defaultVal = new ArrayList<>(); - try { - List parsed = (List) literalEval(initstr); - if (parsed.size() > 1) { - defaultVal = new ArrayList<>(parsed.subList(1, parsed.size())); - } - } catch (Exception e) { - // initstr not parseable as list; defaultVal stays empty - } - - Path filePathObj; - try { - filePathObj = resolvePortFilePath(inpath, port, name); - } catch (IOException | RuntimeException e) { - System.out.println("Invalid path for port " + port + " and name '" + name + "': " + e.getMessage()); - return new ReadResult(ReadStatus.PARSE_ERROR, defaultVal); - } - String filePath = filePathObj.toString(); - try { - Thread.sleep(delay); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - s += initstr; - return new ReadResult(ReadStatus.TIMEOUT, defaultVal); - } - - String ins; - try { - ins = new String(Files.readAllBytes(filePathObj)); - } catch (IOException e) { - System.out.println("File " + filePath + " not found, using default value."); - s += initstr; - return new ReadResult(ReadStatus.FILE_NOT_FOUND, defaultVal); - } - - int attempts = 0; - while (ins.length() == 0 && attempts < maxRetries) { - try { - Thread.sleep(delay); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - s += initstr; - return new ReadResult(ReadStatus.TIMEOUT, defaultVal); - } - try { - ins = new String(Files.readAllBytes(filePathObj)); - } catch (IOException e) { - System.out.println("Retry " + (attempts + 1) + ": Error reading " + filePath); - } - attempts++; - retrycount++; - } - - if (ins.length() == 0) { - System.out.println("Max retries reached for " + filePath + ", using default value."); - return new ReadResult(ReadStatus.RETRIES_EXCEEDED, defaultVal); - } - - s += ins; - try { - List inval = (List) literalEval(ins); - if (!inval.isEmpty()) { - double firstSimtime = ((Number) inval.get(0)).doubleValue(); - simtime = Math.max(simtime, firstSimtime); - return new ReadResult(ReadStatus.SUCCESS, new ArrayList<>(inval.subList(1, inval.size()))); - } - } catch (Exception e) { - System.out.println("Error parsing " + ins + ": " + e.getMessage()); - } - return new ReadResult(ReadStatus.PARSE_ERROR, defaultVal); - } - - /** - * Escapes a Java string so it can be safely used as a single-quoted Python string literal. - * At minimum, escapes backslash, single quote, newline, carriage return, and tab. - */ - private static String escapePythonString(String s) { - StringBuilder sb = new StringBuilder(s.length()); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - switch (c) { - case '\\': sb.append("\\\\"); break; - case '\'': sb.append("\\'"); break; - case '\n': sb.append("\\n"); break; - case '\r': sb.append("\\r"); break; - case '\t': sb.append("\\t"); break; - default: sb.append(c); break; - } - } - return sb.toString(); - } - - /** - * Converts a Java object to its Python-literal string representation. - * True/False/None instead of true/false/null; strings single-quoted. - */ - private static String toPythonLiteral(Object obj) { - if (obj == null) return "None"; - if (obj instanceof Boolean) return ((Boolean) obj) ? "True" : "False"; - if (obj instanceof String) return "'" + escapePythonString((String) obj) + "'"; - if (obj instanceof Number) return obj.toString(); - if (obj instanceof List) { - List list = (List) obj; - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < list.size(); i++) { - if (i > 0) sb.append(", "); - sb.append(toPythonLiteral(list.get(i))); - } - sb.append("]"); - return sb.toString(); - } - if (obj instanceof Map) { - Map map = (Map) obj; - StringBuilder sb = new StringBuilder("{"); - boolean first = true; - for (Map.Entry entry : map.entrySet()) { - if (!first) sb.append(", "); - sb.append(toPythonLiteral(entry.getKey())).append(": ").append(toPythonLiteral(entry.getValue())); - first = false; - } - sb.append("}"); - return sb.toString(); - } - return obj.toString(); - } - - /** - * Escapes a Java string so it can be safely embedded in a JSON double-quoted string. - * Escapes backslash, double quote, newline, carriage return, and tab. - */ - private static String escapeJsonString(String s) { - StringBuilder sb = new StringBuilder(s.length()); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - switch (c) { - case '\\': sb.append("\\\\"); break; - case '"': sb.append("\\\""); break; - case '\n': sb.append("\\n"); break; - case '\r': sb.append("\\r"); break; - case '\t': sb.append("\\t"); break; - default: sb.append(c); break; - } - } - return sb.toString(); - } - - /** - * Converts a Java object to its JSON string representation. - * true/false/null instead of True/False/None; strings double-quoted. - */ - private static String toJsonLiteral(Object obj) { - if (obj == null) return "null"; - if (obj instanceof Boolean) return ((Boolean) obj) ? "true" : "false"; - if (obj instanceof String) return "\"" + escapeJsonString((String) obj) + "\""; - if (obj instanceof Number) return obj.toString(); - if (obj instanceof List) { - List list = (List) obj; - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < list.size(); i++) { - if (i > 0) sb.append(", "); - sb.append(toJsonLiteral(list.get(i))); - } - sb.append("]"); - return sb.toString(); - } - if (obj instanceof Map) { - Map map = (Map) obj; - StringBuilder sb = new StringBuilder("{"); - boolean first = true; - for (Map.Entry entry : map.entrySet()) { - if (!first) sb.append(", "); - sb.append(toJsonLiteral(entry.getKey())).append(": ").append(toJsonLiteral(entry.getValue())); - first = false; - } - sb.append("}"); - return sb.toString(); - } - return obj.toString(); + return convertResult(runtime.readFilePort(port, name, initstr)); } /** @@ -453,47 +147,7 @@ private static String toJsonLiteral(Object obj) { * Accepts List or String values (matching Python implementation). */ public static void write(int port, String name, Object val, int delta) { - try { - Path pathObj = resolvePortFilePath(outpath, port, name); - String path = pathObj.toString(); - StringBuilder content = new StringBuilder(); - if (val instanceof String) { - Thread.sleep(2 * delay); - content.append(val); - } else if (val instanceof List) { - List listVal = (List) val; - content.append("["); - content.append(toPythonLiteral(simtime + delta)); - for (int i = 0; i < listVal.size(); i++) { - content.append(", "); - content.append(toPythonLiteral(listVal.get(i))); - } - content.append("]"); - // simtime must not be mutated here. - // Mutation breaks cross-language determinism. - } else if (val instanceof Object[]) { - // Legacy support for Object[] arguments - Object[] arrayVal = (Object[]) val; - content.append("["); - content.append(toPythonLiteral(simtime + delta)); - for (Object o : arrayVal) { - content.append(", "); - content.append(toPythonLiteral(o)); - } - content.append("]"); - // simtime must not be mutated here. - // Mutation breaks cross-language determinism. - } else { - System.out.println("write must have list or str"); - return; - } - Files.write(pathObj, content.toString().getBytes()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - System.out.println("skipping " + outpath + "/" + port + "/" + name); - } catch (IOException e) { - System.out.println("skipping " + outpath + "/" + port + "/" + name); - } + runtime.writeFilePort(port, name, val, delta); } /** @@ -501,58 +155,15 @@ public static void write(int port, String name, Object val, int delta) { * Extracts simtime from position 0 and returns the remaining values as a List. */ public static List initVal(String simtimeVal) { - List val = new ArrayList<>(); - try { - List inval = (List) literalEval(simtimeVal); - if (!inval.isEmpty()) { - simtime = ((Number) inval.get(0)).doubleValue(); - val = new ArrayList<>(inval.subList(1, inval.size())); - } - } catch (Exception e) { - System.out.println("Error parsing initVal: " + e.getMessage()); - } - return val; - } - - private static ZMQ.Context getZmqContext() { - if (zmqContext == null) { - zmqContext = ZMQ.context(1); - } - return zmqContext; + return runtime.initVal(simtimeVal); } public static void initZmqPort(String portName, String portType, String address, String socketTypeStr) { - if (zmqPorts.containsKey(portName)) return; - int sockType = zmqSocketTypeFromString(socketTypeStr); - if (sockType == -1) { - System.err.println("initZmqPort: unknown socket type '" + socketTypeStr + "'"); - return; - } - zmqPorts.put(portName, new ZeroMQPort(portType, address, sockType)); + runtime.initZmqPort(portName, portType, address, socketTypeStr); } public static void terminateZmq() { - for (Map.Entry entry : zmqPorts.entrySet()) { - entry.getValue().socket.close(); - } - zmqPorts.clear(); - if (zmqContext != null) { - zmqContext.term(); - zmqContext = null; - } - } - - private static int zmqSocketTypeFromString(String s) { - switch (s.toUpperCase()) { - case "REQ": return ZMQ.REQ; - case "REP": return ZMQ.REP; - case "PUB": return ZMQ.PUB; - case "SUB": return ZMQ.SUB; - case "PUSH": return ZMQ.PUSH; - case "PULL": return ZMQ.PULL; - case "PAIR": return ZMQ.PAIR; - default: return -1; - } + runtime.terminateZmq(); } /** @@ -560,65 +171,14 @@ private static int zmqSocketTypeFromString(String s) { * expects [simtime, val1, val2, ...], strips simtime, returns the rest. */ public static ReadResult read(String portName, String name, String initstr) { - List defaultVal = new ArrayList<>(); - try { - List parsed = (List) literalEval(initstr); - if (parsed.size() > 1) { - defaultVal = new ArrayList<>(parsed.subList(1, parsed.size())); - } - } catch (Exception e) { - } - ZeroMQPort port = zmqPorts.get(portName); - if (port == null) { - System.err.println("read: ZMQ port '" + portName + "' not initialized"); - return new ReadResult(ReadStatus.FILE_NOT_FOUND, defaultVal); - } - String msg = port.recvWithRetry(); - if (msg == null) { - System.err.println("read: ZMQ recv timeout on port '" + portName + "'"); - return new ReadResult(ReadStatus.TIMEOUT, defaultVal); - } - s += msg; - try { - List inval = (List) literalEval(msg); - if (!inval.isEmpty()) { - simtime = Math.max(simtime, ((Number) inval.get(0)).doubleValue()); - return new ReadResult(ReadStatus.SUCCESS, new ArrayList<>(inval.subList(1, inval.size()))); - } - } catch (Exception e) { - System.out.println("Error parsing ZMQ message '" + msg + "': " + e.getMessage()); - } - return new ReadResult(ReadStatus.PARSE_ERROR, defaultVal); + return convertResult(runtime.readZmqPort(portName, name, initstr)); } /** * Writes data to a ZMQ port. Prepends [simtime+delta] to match file-based write behavior. */ public static void write(String portName, String name, Object val, int delta) { - ZeroMQPort port = zmqPorts.get(portName); - if (port == null) { - System.err.println("write: ZMQ port '" + portName + "' not initialized"); - return; - } - String payload; - if (val instanceof List) { - List listVal = (List) val; - StringBuilder sb = new StringBuilder("["); - sb.append(toJsonLiteral(simtime + delta)); - for (Object o : listVal) { - sb.append(", "); - sb.append(toJsonLiteral(o)); - } - sb.append("]"); - payload = sb.toString(); - // simtime must not be mutated here - } else if (val instanceof String) { - payload = (String) val; - } else { - System.out.println("write must have list or str"); - return; - } - port.sendWithRetry(payload); + runtime.writeZmqPort(portName, name, val, delta); } /** @@ -627,16 +187,15 @@ public static void write(String portName, String name, Object val, int delta) { * This replaces the broken split-based parser that could not handle quoted commas or nesting. */ static Object literalEval(String s) { - if (s == null) throw new IllegalArgumentException("Input cannot be null"); - s = s.trim(); - if (s.isEmpty()) throw new IllegalArgumentException("Input cannot be empty"); - Parser parser = new Parser(s); - Object result = parser.parseExpression(); - parser.skipWhitespace(); - if (parser.pos < parser.input.length()) { - throw new IllegalArgumentException("Unexpected trailing content at position " + parser.pos); - } - return result; + return ConcoreJavaRuntimeCore.literalEval(s); + } + + private static ReadStatus convertStatus(ConcoreJavaRuntimeCore.ReadStatus status) { + return ReadStatus.valueOf(status.name()); + } + + private static ReadResult convertResult(ConcoreJavaRuntimeCore.ReadResult result) { + return new ReadResult(convertStatus(result.status), result.data); } public enum ReadStatus { @@ -652,289 +211,4 @@ public static class ReadResult { } } - /** - * ZMQ socket wrapper with bind/connect, timeouts, and retry. - */ - private static class ZeroMQPort { - final ZMQ.Socket socket; - final String address; - - ZeroMQPort(String portType, String address, int socketType) { - this.address = address; - ZMQ.Context ctx = getZmqContext(); - this.socket = ctx.socket(socketType); - this.socket.setReceiveTimeOut(2000); - this.socket.setSendTimeOut(2000); - this.socket.setLinger(0); - if (portType.equals("bind")) { - this.socket.bind(address); - } else { - this.socket.connect(address); - } - } - - String recvWithRetry() { - for (int attempt = 0; attempt < 5; attempt++) { - String msg = socket.recvStr(); - if (msg != null) return msg; - try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } - } - return null; - } - - void sendWithRetry(String message) { - for (int attempt = 0; attempt < 5; attempt++) { - if (socket.send(message)) return; - try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } - } - } - } - - /** - * Recursive descent parser for Python literal expressions. - * Handles: dicts, lists, tuples, strings, numbers, booleans, None. - */ - private static class Parser { - final String input; - int pos; - - Parser(String input) { - this.input = input; - this.pos = 0; - } - - void skipWhitespace() { - while (pos < input.length() && Character.isWhitespace(input.charAt(pos))) { - pos++; - } - } - - char peek() { - skipWhitespace(); - if (pos >= input.length()) throw new IllegalArgumentException("Unexpected end of input"); - return input.charAt(pos); - } - - char advance() { - char c = input.charAt(pos); - pos++; - return c; - } - - boolean hasMore() { - skipWhitespace(); - return pos < input.length(); - } - - Object parseExpression() { - skipWhitespace(); - if (pos >= input.length()) throw new IllegalArgumentException("Unexpected end of input"); - char c = input.charAt(pos); - - if (c == '{') return parseDict(); - if (c == '[') return parseList(); - if (c == '(') return parseTuple(); - if (c == '\'' || c == '"') return parseString(); - if (c == '-' || c == '+' || Character.isDigit(c)) return parseNumber(); - return parseKeyword(); - } - - Map parseDict() { - Map map = new HashMap<>(); - pos++; // skip '{' - skipWhitespace(); - if (hasMore() && input.charAt(pos) == '}') { - pos++; - return map; - } - while (true) { - skipWhitespace(); - Object key = parseExpression(); - skipWhitespace(); - if (pos >= input.length() || input.charAt(pos) != ':') { - throw new IllegalArgumentException("Expected ':' in dict at position " + pos); - } - pos++; // skip ':' - skipWhitespace(); - Object value = parseExpression(); - if (!(key instanceof String)) { - throw new IllegalArgumentException( - "Dict keys must be non-null strings, but got: " - + (key == null ? "null" : key.getClass().getSimpleName())); - } - map.put((String) key, value); - skipWhitespace(); - if (pos >= input.length()) { - throw new IllegalArgumentException("Unterminated dict: missing '}'"); - } - if (input.charAt(pos) == '}') { - pos++; - break; - } - if (input.charAt(pos) == ',') { - pos++; - skipWhitespace(); - // trailing comma before close - if (hasMore() && input.charAt(pos) == '}') { - pos++; - break; - } - } else { - throw new IllegalArgumentException("Expected ',' or '}' in dict at position " + pos); - } - } - return map; - } - - List parseList() { - List list = new ArrayList<>(); - pos++; // skip '[' - skipWhitespace(); - if (hasMore() && input.charAt(pos) == ']') { - pos++; - return list; - } - while (true) { - skipWhitespace(); - list.add(parseExpression()); - skipWhitespace(); - if (pos >= input.length()) { - throw new IllegalArgumentException("Unterminated list: missing ']'"); - } - if (input.charAt(pos) == ']') { - pos++; - break; - } - if (input.charAt(pos) == ',') { - pos++; - skipWhitespace(); - // trailing comma before close - if (hasMore() && input.charAt(pos) == ']') { - pos++; - break; - } - } else { - throw new IllegalArgumentException("Expected ',' or ']' in list at position " + pos); - } - } - return list; - } - - List parseTuple() { - List list = new ArrayList<>(); - pos++; // skip '(' - skipWhitespace(); - if (hasMore() && input.charAt(pos) == ')') { - pos++; - return list; - } - while (true) { - skipWhitespace(); - list.add(parseExpression()); - skipWhitespace(); - if (pos >= input.length()) { - throw new IllegalArgumentException("Unterminated tuple: missing ')'"); - } - if (input.charAt(pos) == ')') { - pos++; - break; - } - if (input.charAt(pos) == ',') { - pos++; - skipWhitespace(); - // trailing comma before close - if (hasMore() && input.charAt(pos) == ')') { - pos++; - break; - } - } else { - throw new IllegalArgumentException("Expected ',' or ')' in tuple at position " + pos); - } - } - return list; - } - - String parseString() { - char quote = advance(); // opening quote - StringBuilder sb = new StringBuilder(); - while (pos < input.length()) { - char c = input.charAt(pos); - if (c == '\\' && pos + 1 < input.length()) { - pos++; - char escaped = input.charAt(pos); - switch (escaped) { - case 'n': sb.append('\n'); break; - case 't': sb.append('\t'); break; - case 'r': sb.append('\r'); break; - case '\\': sb.append('\\'); break; - case '\'': sb.append('\''); break; - case '"': sb.append('"'); break; - default: sb.append('\\').append(escaped); break; - } - pos++; - } else if (c == quote) { - pos++; - return sb.toString(); - } else { - sb.append(c); - pos++; - } - } - throw new IllegalArgumentException("Unterminated string starting at position " + (pos - sb.length() - 1)); - } - - Number parseNumber() { - int start = pos; - if (pos < input.length() && (input.charAt(pos) == '-' || input.charAt(pos) == '+')) { - pos++; - } - boolean hasDecimal = false; - boolean hasExponent = false; - while (pos < input.length()) { - char c = input.charAt(pos); - if (Character.isDigit(c)) { - pos++; - } else if (c == '.' && !hasDecimal && !hasExponent) { - hasDecimal = true; - pos++; - } else if ((c == 'e' || c == 'E') && !hasExponent) { - hasExponent = true; - pos++; - if (pos < input.length() && (input.charAt(pos) == '+' || input.charAt(pos) == '-')) { - pos++; - } - } else { - break; - } - } - String numStr = input.substring(start, pos); - try { - if (hasDecimal || hasExponent) { - return Double.parseDouble(numStr); - } else { - try { - return Integer.parseInt(numStr); - } catch (NumberFormatException e) { - return Long.parseLong(numStr); - } - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid number: '" + numStr + "' at position " + start); - } - } - - Object parseKeyword() { - int start = pos; - while (pos < input.length() && Character.isLetterOrDigit(input.charAt(pos)) || (pos < input.length() && input.charAt(pos) == '_')) { - pos++; - } - String word = input.substring(start, pos); - switch (word) { - case "True": case "true": return Boolean.TRUE; - case "False": case "false": return Boolean.FALSE; - case "None": case "null": return null; - default: throw new IllegalArgumentException("Unknown keyword: '" + word + "' at position " + start); - } - } - } } diff --git a/concoredocker.java b/concoredocker.java index 65c6170..cc913c2 100644 --- a/concoredocker.java +++ b/concoredocker.java @@ -1,12 +1,4 @@ -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; -import java.util.ArrayList; import java.util.List; -import org.zeromq.ZMQ; /** * Java implementation of concore Docker communication. @@ -15,153 +7,34 @@ * mirroring the functionality of concoredocker.py. */ public class concoredocker { - private static Map iport = new HashMap<>(); - private static Map oport = new HashMap<>(); - private static String s = ""; - private static String olds = ""; - // delay in milliseconds (Python uses time.sleep(1) = 1 second) - private static int delay = 1000; - private static int retrycount = 0; - private static int maxRetries = 5; - private static String inpath = "/in"; - private static String outpath = "/out"; - private static Map params = new HashMap<>(); - private static Map zmqPorts = new HashMap<>(); - private static ZMQ.Context zmqContext = null; - // simtime as double to preserve fractional values (e.g. "[0.0, ...]") - private static double simtime = 0; - private static double maxtime; + private static final ConcoreJavaRuntimeCore runtime = new ConcoreJavaRuntimeCore("/in", "/out", false); // initialize on class load, same as Python module-level init static { - try { - iport = parseFile("concore.iport"); - } catch (IOException e) { - } - try { - oport = parseFile("concore.oport"); - } catch (IOException e) { - } - try { - String sparams = new String(Files.readAllBytes(Paths.get(inpath + "/1/concore.params")), java.nio.charset.StandardCharsets.UTF_8); - if (sparams.length() > 0 && sparams.charAt(0) == '"') { // windows keeps "" need to remove - sparams = sparams.substring(1); - sparams = sparams.substring(0, sparams.indexOf('"')); - } - params = parseParams(sparams); - } catch (IOException e) { - params = new HashMap<>(); - } - defaultMaxTime(100); Runtime.getRuntime().addShutdownHook(new Thread(concoredocker::terminateZmq)); } - /** - * Parses a param string into a map, matching concore_base.parse_params. - * Tries dict literal first, then falls back to semicolon-separated key=value pairs. - */ - private static Map parseParams(String sparams) { - Map result = new HashMap<>(); - if (sparams == null || sparams.isEmpty()) return result; - String trimmed = sparams.trim(); - if (trimmed.startsWith("{") && trimmed.endsWith("}")) { - try { - Object val = literalEval(trimmed); - if (val instanceof Map) { - @SuppressWarnings("unchecked") - Map map = (Map) val; - return map; - } - } catch (Exception e) { - } - } - for (String item : trimmed.split(";")) { - if (item.contains("=")) { - String[] parts = item.split("=", 2); // split on first '=' only - String key = parts[0].trim(); - String value = parts[1].trim(); - try { - result.put(key, literalEval(value)); - } catch (Exception e) { - result.put(key, value); - } - } - } - return result; - } - - /** - * Parses a file containing a Python-style dictionary literal. - * Returns empty map if file is empty or malformed (matches Python safe_literal_eval). - */ - private static Map parseFile(String filename) throws IOException { - String content = new String(Files.readAllBytes(Paths.get(filename)), java.nio.charset.StandardCharsets.UTF_8); - content = content.trim(); - if (content.isEmpty()) { - return new HashMap<>(); - } - try { - Object result = literalEval(content); - if (result instanceof Map) { - @SuppressWarnings("unchecked") - Map map = (Map) result; - return map; - } - } catch (IllegalArgumentException e) { - System.err.println("Failed to parse file as map: " + filename + " (" + e.getMessage() + ")"); - } - return new HashMap<>(); - } - /** * Sets maxtime from concore.maxtime file, or uses defaultValue if file not found. * Catches both IOException and RuntimeException to match Python safe_literal_eval. */ public static void defaultMaxTime(double defaultValue) { - try { - String content = new String(Files.readAllBytes(Paths.get(inpath + "/1/concore.maxtime"))); - Object parsed = literalEval(content.trim()); - if (parsed instanceof Number) { - maxtime = ((Number) parsed).doubleValue(); - } else { - maxtime = defaultValue; - } - } catch (IOException | RuntimeException e) { - maxtime = defaultValue; - } + runtime.defaultMaxTime(defaultValue); } // package-level helpers for testing with temp directories - static void setInPath(String path) { inpath = path; } - static void setOutPath(String path) { outpath = path; } - static void setDelay(int ms) { delay = ms; } - static double getSimtime() { return simtime; } - static void resetState() { s = ""; olds = ""; simtime = 0; } - - private static Path resolvePortFilePath(String base, int portNum, String name) throws IOException { - Path portDir = Paths.get(base, String.valueOf(portNum)).toAbsolutePath().normalize(); - Path filePath = portDir.resolve(name).normalize(); - if (!filePath.startsWith(portDir)) { - throw new IOException("Invalid file name '" + name + "' for port " + portNum); - } - return filePath; - } + static void setInPath(String path) { runtime.setInPath(path); } + static void setOutPath(String path) { runtime.setOutPath(path); } + static void setDelay(int ms) { runtime.setDelay(ms); } + static double getSimtime() { return runtime.getSimtime(); } + static void resetState() { runtime.resetState(); } public static boolean unchanged() { - if (olds.equals(s)) { - s = ""; - return true; - } - olds = s; - return false; + return runtime.unchanged(); } public static Object tryParam(String n, Object i) { - if (params.containsKey(n)) { - return params.get(n); - } else { - return i; - } + return runtime.tryParam(n, i); } /** @@ -171,185 +44,7 @@ public static Object tryParam(String n, Object i) { * Includes max retry limit to avoid infinite blocking (matches Python behavior). */ public static ReadResult read(int port, String name, String initstr) { - // Parse default value upfront for consistent return type - List defaultVal = new ArrayList<>(); - try { - List parsed = (List) literalEval(initstr); - if (parsed.size() > 1) { - defaultVal = new ArrayList<>(parsed.subList(1, parsed.size())); - } - } catch (Exception e) { - // initstr not parseable as list; defaultVal stays empty - } - - Path filePathObj; - try { - filePathObj = resolvePortFilePath(inpath, port, name); - } catch (IOException | RuntimeException e) { - System.out.println("Invalid path for port " + port + " and name '" + name + "': " + e.getMessage()); - return new ReadResult(ReadStatus.PARSE_ERROR, defaultVal); - } - String filePath = filePathObj.toString(); - try { - Thread.sleep(delay); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - s += initstr; - return new ReadResult(ReadStatus.TIMEOUT, defaultVal); - } - - String ins; - try { - ins = new String(Files.readAllBytes(filePathObj)); - } catch (IOException e) { - System.out.println("File " + filePath + " not found, using default value."); - s += initstr; - return new ReadResult(ReadStatus.FILE_NOT_FOUND, defaultVal); - } - - int attempts = 0; - while (ins.length() == 0 && attempts < maxRetries) { - try { - Thread.sleep(delay); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - s += initstr; - return new ReadResult(ReadStatus.TIMEOUT, defaultVal); - } - try { - ins = new String(Files.readAllBytes(filePathObj)); - } catch (IOException e) { - System.out.println("Retry " + (attempts + 1) + ": Error reading " + filePath); - } - attempts++; - retrycount++; - } - - if (ins.length() == 0) { - System.out.println("Max retries reached for " + filePath + ", using default value."); - return new ReadResult(ReadStatus.RETRIES_EXCEEDED, defaultVal); - } - - s += ins; - try { - List inval = (List) literalEval(ins); - if (!inval.isEmpty()) { - double firstSimtime = ((Number) inval.get(0)).doubleValue(); - simtime = Math.max(simtime, firstSimtime); - return new ReadResult(ReadStatus.SUCCESS, new ArrayList<>(inval.subList(1, inval.size()))); - } - } catch (Exception e) { - System.out.println("Error parsing " + ins + ": " + e.getMessage()); - } - return new ReadResult(ReadStatus.PARSE_ERROR, defaultVal); - } - - /** - * Escapes a Java string so it can be safely used as a single-quoted Python string literal. - * At minimum, escapes backslash, single quote, newline, carriage return, and tab. - */ - private static String escapePythonString(String s) { - StringBuilder sb = new StringBuilder(s.length()); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - switch (c) { - case '\\': sb.append("\\\\"); break; - case '\'': sb.append("\\'"); break; - case '\n': sb.append("\\n"); break; - case '\r': sb.append("\\r"); break; - case '\t': sb.append("\\t"); break; - default: sb.append(c); break; - } - } - return sb.toString(); - } - - /** - * Converts a Java object to its Python-literal string representation. - * True/False/None instead of true/false/null; strings single-quoted. - */ - private static String toPythonLiteral(Object obj) { - if (obj == null) return "None"; - if (obj instanceof Boolean) return ((Boolean) obj) ? "True" : "False"; - if (obj instanceof String) return "'" + escapePythonString((String) obj) + "'"; - if (obj instanceof Number) return obj.toString(); - if (obj instanceof List) { - List list = (List) obj; - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < list.size(); i++) { - if (i > 0) sb.append(", "); - sb.append(toPythonLiteral(list.get(i))); - } - sb.append("]"); - return sb.toString(); - } - if (obj instanceof Map) { - Map map = (Map) obj; - StringBuilder sb = new StringBuilder("{"); - boolean first = true; - for (Map.Entry entry : map.entrySet()) { - if (!first) sb.append(", "); - sb.append(toPythonLiteral(entry.getKey())).append(": ").append(toPythonLiteral(entry.getValue())); - first = false; - } - sb.append("}"); - return sb.toString(); - } - return obj.toString(); - } - - /** - * Escapes a Java string so it can be safely embedded in a JSON double-quoted string. - * Escapes backslash, double quote, newline, carriage return, and tab. - */ - private static String escapeJsonString(String s) { - StringBuilder sb = new StringBuilder(s.length()); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - switch (c) { - case '\\': sb.append("\\\\"); break; - case '"': sb.append("\\\""); break; - case '\n': sb.append("\\n"); break; - case '\r': sb.append("\\r"); break; - case '\t': sb.append("\\t"); break; - default: sb.append(c); break; - } - } - return sb.toString(); - } - - /** - * Converts a Java object to its JSON string representation. - * true/false/null instead of True/False/None; strings double-quoted. - */ - private static String toJsonLiteral(Object obj) { - if (obj == null) return "null"; - if (obj instanceof Boolean) return ((Boolean) obj) ? "true" : "false"; - if (obj instanceof String) return "\"" + escapeJsonString((String) obj) + "\""; - if (obj instanceof Number) return obj.toString(); - if (obj instanceof List) { - List list = (List) obj; - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < list.size(); i++) { - if (i > 0) sb.append(", "); - sb.append(toJsonLiteral(list.get(i))); - } - sb.append("]"); - return sb.toString(); - } - if (obj instanceof Map) { - Map map = (Map) obj; - StringBuilder sb = new StringBuilder("{"); - boolean first = true; - for (Map.Entry entry : map.entrySet()) { - if (!first) sb.append(", "); - sb.append(toJsonLiteral(entry.getKey())).append(": ").append(toJsonLiteral(entry.getValue())); - first = false; - } - sb.append("}"); - return sb.toString(); - } - return obj.toString(); + return convertResult(runtime.readFilePort(port, name, initstr)); } /** @@ -358,47 +53,7 @@ private static String toJsonLiteral(Object obj) { * Accepts List or String values (matching Python implementation). */ public static void write(int port, String name, Object val, int delta) { - try { - Path pathObj = resolvePortFilePath(outpath, port, name); - String path = pathObj.toString(); - StringBuilder content = new StringBuilder(); - if (val instanceof String) { - Thread.sleep(2 * delay); - content.append(val); - } else if (val instanceof List) { - List listVal = (List) val; - content.append("["); - content.append(toPythonLiteral(simtime + delta)); - for (int i = 0; i < listVal.size(); i++) { - content.append(", "); - content.append(toPythonLiteral(listVal.get(i))); - } - content.append("]"); - // simtime must not be mutated here. - // Mutation breaks cross-language determinism. - } else if (val instanceof Object[]) { - // Legacy support for Object[] arguments - Object[] arrayVal = (Object[]) val; - content.append("["); - content.append(toPythonLiteral(simtime + delta)); - for (Object o : arrayVal) { - content.append(", "); - content.append(toPythonLiteral(o)); - } - content.append("]"); - // simtime must not be mutated here. - // Mutation breaks cross-language determinism. - } else { - System.out.println("write must have list or str"); - return; - } - Files.write(pathObj, content.toString().getBytes()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - System.out.println("skipping " + outpath + "/" + port + "/" + name); - } catch (IOException e) { - System.out.println("skipping " + outpath + "/" + port + "/" + name); - } + runtime.writeFilePort(port, name, val, delta); } /** @@ -406,58 +61,15 @@ public static void write(int port, String name, Object val, int delta) { * Extracts simtime from position 0 and returns the remaining values as a List. */ public static List initVal(String simtimeVal) { - List val = new ArrayList<>(); - try { - List inval = (List) literalEval(simtimeVal); - if (!inval.isEmpty()) { - simtime = ((Number) inval.get(0)).doubleValue(); - val = new ArrayList<>(inval.subList(1, inval.size())); - } - } catch (Exception e) { - System.out.println("Error parsing initVal: " + e.getMessage()); - } - return val; - } - - private static ZMQ.Context getZmqContext() { - if (zmqContext == null) { - zmqContext = ZMQ.context(1); - } - return zmqContext; + return runtime.initVal(simtimeVal); } public static void initZmqPort(String portName, String portType, String address, String socketTypeStr) { - if (zmqPorts.containsKey(portName)) return; - int sockType = zmqSocketTypeFromString(socketTypeStr); - if (sockType == -1) { - System.err.println("initZmqPort: unknown socket type '" + socketTypeStr + "'"); - return; - } - zmqPorts.put(portName, new ZeroMQPort(portType, address, sockType)); + runtime.initZmqPort(portName, portType, address, socketTypeStr); } public static void terminateZmq() { - for (Map.Entry entry : zmqPorts.entrySet()) { - entry.getValue().socket.close(); - } - zmqPorts.clear(); - if (zmqContext != null) { - zmqContext.term(); - zmqContext = null; - } - } - - private static int zmqSocketTypeFromString(String s) { - switch (s.toUpperCase()) { - case "REQ": return ZMQ.REQ; - case "REP": return ZMQ.REP; - case "PUB": return ZMQ.PUB; - case "SUB": return ZMQ.SUB; - case "PUSH": return ZMQ.PUSH; - case "PULL": return ZMQ.PULL; - case "PAIR": return ZMQ.PAIR; - default: return -1; - } + runtime.terminateZmq(); } /** @@ -465,65 +77,14 @@ private static int zmqSocketTypeFromString(String s) { * expects [simtime, val1, val2, ...], strips simtime, returns the rest. */ public static ReadResult read(String portName, String name, String initstr) { - List defaultVal = new ArrayList<>(); - try { - List parsed = (List) literalEval(initstr); - if (parsed.size() > 1) { - defaultVal = new ArrayList<>(parsed.subList(1, parsed.size())); - } - } catch (Exception e) { - } - ZeroMQPort port = zmqPorts.get(portName); - if (port == null) { - System.err.println("read: ZMQ port '" + portName + "' not initialized"); - return new ReadResult(ReadStatus.FILE_NOT_FOUND, defaultVal); - } - String msg = port.recvWithRetry(); - if (msg == null) { - System.err.println("read: ZMQ recv timeout on port '" + portName + "'"); - return new ReadResult(ReadStatus.TIMEOUT, defaultVal); - } - s += msg; - try { - List inval = (List) literalEval(msg); - if (!inval.isEmpty()) { - simtime = Math.max(simtime, ((Number) inval.get(0)).doubleValue()); - return new ReadResult(ReadStatus.SUCCESS, new ArrayList<>(inval.subList(1, inval.size()))); - } - } catch (Exception e) { - System.out.println("Error parsing ZMQ message '" + msg + "': " + e.getMessage()); - } - return new ReadResult(ReadStatus.PARSE_ERROR, defaultVal); + return convertResult(runtime.readZmqPort(portName, name, initstr)); } /** * Writes data to a ZMQ port. Prepends [simtime+delta] to match file-based write behavior. */ public static void write(String portName, String name, Object val, int delta) { - ZeroMQPort port = zmqPorts.get(portName); - if (port == null) { - System.err.println("write: ZMQ port '" + portName + "' not initialized"); - return; - } - String payload; - if (val instanceof List) { - List listVal = (List) val; - StringBuilder sb = new StringBuilder("["); - sb.append(toJsonLiteral(simtime + delta)); - for (Object o : listVal) { - sb.append(", "); - sb.append(toJsonLiteral(o)); - } - sb.append("]"); - payload = sb.toString(); - // simtime must not be mutated here - } else if (val instanceof String) { - payload = (String) val; - } else { - System.out.println("write must have list or str"); - return; - } - port.sendWithRetry(payload); + runtime.writeZmqPort(portName, name, val, delta); } /** @@ -532,16 +93,15 @@ public static void write(String portName, String name, Object val, int delta) { * This replaces the broken split-based parser that could not handle quoted commas or nesting. */ static Object literalEval(String s) { - if (s == null) throw new IllegalArgumentException("Input cannot be null"); - s = s.trim(); - if (s.isEmpty()) throw new IllegalArgumentException("Input cannot be empty"); - Parser parser = new Parser(s); - Object result = parser.parseExpression(); - parser.skipWhitespace(); - if (parser.pos < parser.input.length()) { - throw new IllegalArgumentException("Unexpected trailing content at position " + parser.pos); - } - return result; + return ConcoreJavaRuntimeCore.literalEval(s); + } + + private static ReadStatus convertStatus(ConcoreJavaRuntimeCore.ReadStatus status) { + return ReadStatus.valueOf(status.name()); + } + + private static ReadResult convertResult(ConcoreJavaRuntimeCore.ReadResult result) { + return new ReadResult(convertStatus(result.status), result.data); } public enum ReadStatus { @@ -557,289 +117,4 @@ public static class ReadResult { } } - /** - * ZMQ socket wrapper with bind/connect, timeouts, and retry. - */ - private static class ZeroMQPort { - final ZMQ.Socket socket; - final String address; - - ZeroMQPort(String portType, String address, int socketType) { - this.address = address; - ZMQ.Context ctx = getZmqContext(); - this.socket = ctx.socket(socketType); - this.socket.setReceiveTimeOut(2000); - this.socket.setSendTimeOut(2000); - this.socket.setLinger(0); - if (portType.equals("bind")) { - this.socket.bind(address); - } else { - this.socket.connect(address); - } - } - - String recvWithRetry() { - for (int attempt = 0; attempt < 5; attempt++) { - String msg = socket.recvStr(); - if (msg != null) return msg; - try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } - } - return null; - } - - void sendWithRetry(String message) { - for (int attempt = 0; attempt < 5; attempt++) { - if (socket.send(message)) return; - try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } - } - } - } - - /** - * Recursive descent parser for Python literal expressions. - * Handles: dicts, lists, tuples, strings, numbers, booleans, None. - */ - private static class Parser { - final String input; - int pos; - - Parser(String input) { - this.input = input; - this.pos = 0; - } - - void skipWhitespace() { - while (pos < input.length() && Character.isWhitespace(input.charAt(pos))) { - pos++; - } - } - - char peek() { - skipWhitespace(); - if (pos >= input.length()) throw new IllegalArgumentException("Unexpected end of input"); - return input.charAt(pos); - } - - char advance() { - char c = input.charAt(pos); - pos++; - return c; - } - - boolean hasMore() { - skipWhitespace(); - return pos < input.length(); - } - - Object parseExpression() { - skipWhitespace(); - if (pos >= input.length()) throw new IllegalArgumentException("Unexpected end of input"); - char c = input.charAt(pos); - - if (c == '{') return parseDict(); - if (c == '[') return parseList(); - if (c == '(') return parseTuple(); - if (c == '\'' || c == '"') return parseString(); - if (c == '-' || c == '+' || Character.isDigit(c)) return parseNumber(); - return parseKeyword(); - } - - Map parseDict() { - Map map = new HashMap<>(); - pos++; // skip '{' - skipWhitespace(); - if (hasMore() && input.charAt(pos) == '}') { - pos++; - return map; - } - while (true) { - skipWhitespace(); - Object key = parseExpression(); - skipWhitespace(); - if (pos >= input.length() || input.charAt(pos) != ':') { - throw new IllegalArgumentException("Expected ':' in dict at position " + pos); - } - pos++; // skip ':' - skipWhitespace(); - Object value = parseExpression(); - if (!(key instanceof String)) { - throw new IllegalArgumentException( - "Dict keys must be non-null strings, but got: " - + (key == null ? "null" : key.getClass().getSimpleName())); - } - map.put((String) key, value); - skipWhitespace(); - if (pos >= input.length()) { - throw new IllegalArgumentException("Unterminated dict: missing '}'"); - } - if (input.charAt(pos) == '}') { - pos++; - break; - } - if (input.charAt(pos) == ',') { - pos++; - skipWhitespace(); - // trailing comma before close - if (hasMore() && input.charAt(pos) == '}') { - pos++; - break; - } - } else { - throw new IllegalArgumentException("Expected ',' or '}' in dict at position " + pos); - } - } - return map; - } - - List parseList() { - List list = new ArrayList<>(); - pos++; // skip '[' - skipWhitespace(); - if (hasMore() && input.charAt(pos) == ']') { - pos++; - return list; - } - while (true) { - skipWhitespace(); - list.add(parseExpression()); - skipWhitespace(); - if (pos >= input.length()) { - throw new IllegalArgumentException("Unterminated list: missing ']'"); - } - if (input.charAt(pos) == ']') { - pos++; - break; - } - if (input.charAt(pos) == ',') { - pos++; - skipWhitespace(); - // trailing comma before close - if (hasMore() && input.charAt(pos) == ']') { - pos++; - break; - } - } else { - throw new IllegalArgumentException("Expected ',' or ']' in list at position " + pos); - } - } - return list; - } - - List parseTuple() { - List list = new ArrayList<>(); - pos++; // skip '(' - skipWhitespace(); - if (hasMore() && input.charAt(pos) == ')') { - pos++; - return list; - } - while (true) { - skipWhitespace(); - list.add(parseExpression()); - skipWhitespace(); - if (pos >= input.length()) { - throw new IllegalArgumentException("Unterminated tuple: missing ')'"); - } - if (input.charAt(pos) == ')') { - pos++; - break; - } - if (input.charAt(pos) == ',') { - pos++; - skipWhitespace(); - // trailing comma before close - if (hasMore() && input.charAt(pos) == ')') { - pos++; - break; - } - } else { - throw new IllegalArgumentException("Expected ',' or ')' in tuple at position " + pos); - } - } - return list; - } - - String parseString() { - char quote = advance(); // opening quote - StringBuilder sb = new StringBuilder(); - while (pos < input.length()) { - char c = input.charAt(pos); - if (c == '\\' && pos + 1 < input.length()) { - pos++; - char escaped = input.charAt(pos); - switch (escaped) { - case 'n': sb.append('\n'); break; - case 't': sb.append('\t'); break; - case 'r': sb.append('\r'); break; - case '\\': sb.append('\\'); break; - case '\'': sb.append('\''); break; - case '"': sb.append('"'); break; - default: sb.append('\\').append(escaped); break; - } - pos++; - } else if (c == quote) { - pos++; - return sb.toString(); - } else { - sb.append(c); - pos++; - } - } - throw new IllegalArgumentException("Unterminated string starting at position " + (pos - sb.length() - 1)); - } - - Number parseNumber() { - int start = pos; - if (pos < input.length() && (input.charAt(pos) == '-' || input.charAt(pos) == '+')) { - pos++; - } - boolean hasDecimal = false; - boolean hasExponent = false; - while (pos < input.length()) { - char c = input.charAt(pos); - if (Character.isDigit(c)) { - pos++; - } else if (c == '.' && !hasDecimal && !hasExponent) { - hasDecimal = true; - pos++; - } else if ((c == 'e' || c == 'E') && !hasExponent) { - hasExponent = true; - pos++; - if (pos < input.length() && (input.charAt(pos) == '+' || input.charAt(pos) == '-')) { - pos++; - } - } else { - break; - } - } - String numStr = input.substring(start, pos); - try { - if (hasDecimal || hasExponent) { - return Double.parseDouble(numStr); - } else { - try { - return Integer.parseInt(numStr); - } catch (NumberFormatException e) { - return Long.parseLong(numStr); - } - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid number: '" + numStr + "' at position " + start); - } - } - - Object parseKeyword() { - int start = pos; - while (pos < input.length() && Character.isLetterOrDigit(input.charAt(pos)) || (pos < input.length() && input.charAt(pos) == '_')) { - pos++; - } - String word = input.substring(start, pos); - switch (word) { - case "True": case "true": return Boolean.TRUE; - case "False": case "false": return Boolean.FALSE; - case "None": case "null": return null; - default: throw new IllegalArgumentException("Unknown keyword: '" + word + "' at position " + start); - } - } - } }