diff --git a/fedac/native/Makefile b/fedac/native/Makefile --- a/fedac/native/Makefile +++ b/fedac/native/Makefile @@ -84,6 +84,7 @@ $(SRCDIR)/udp-client.c \ $(SRCDIR)/camera.c \ $(SRCDIR)/pty.c \ $(SRCDIR)/machines.c \ + $(SRCDIR)/swank-bridge.c \ $(SRCDIR)/js-bindings.c \ $(SRCDIR)/recorder.c diff --git a/fedac/native/src/machines.c b/fedac/native/src/machines.c --- a/fedac/native/src/machines.c +++ b/fedac/native/src/machines.c @@ -3,6 +3,7 @@ // Manages a WebSocket connection to session-server for device registration, // heartbeats, log upload, and remote command reception. #include "machines.h" +#include "swank-bridge.h" #include #include @@ -280,6 +281,27 @@ static void process_messages(ACMachines *m) { int count = ws_poll(m->ws); for (int i = 0; i < count; i++) { const char *raw = m->ws->messages[i]; + + // Handle swank:eval — evaluate CL via local Swank server + if (strstr(raw, "\"type\":\"swank:eval\"")) { + char expr[2048] = "", eval_id[32] = ""; + json_get_str(raw, "expr", expr, sizeof(expr)); + json_get_str(raw, "evalId", eval_id, sizeof(eval_id)); + if (expr[0]) { + ac_log("[machines] swank:eval id=%s expr=%.80s\n", eval_id, expr); + char result[4096] = ""; + int rc = swank_eval(expr, result, sizeof(result)); + // Send result back + char escaped_result[8192]; + json_escape(result, escaped_result, sizeof(escaped_result)); + char msg[12288]; + snprintf(msg, sizeof(msg), + "{\"type\":\"swank:result\",\"evalId\":\"%s\",\"ok\":%s,\"result\":\"%s\"}", + eval_id, rc == 0 ? "true" : "false", escaped_result); + ws_send(m->ws, msg); + } + continue; + } // Check if it's a command message if (!strstr(raw, "\"type\":\"command\"")) continue; diff --git a/fedac/native/src/swank-bridge.c b/fedac/native/src/swank-bridge.c new file mode 100644 --- /dev/null +++ b/fedac/native/src/swank-bridge.c @@ -0,0 +1,230 @@ +// swank-bridge.c — Evaluate CL expressions via local Swank server +// +// Swank wire protocol: +// Send: 6-digit hex length + S-expression +// Recv: 6-digit hex length + S-expression +// +// We send: (:emacs-rex (swank:interactive-eval "EXPR") "CL-USER" :repl-thread 1) +// We get: (:return (:ok "RESULT") 1) + +#include "swank-bridge.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SWANK_PORT 4005 +#define SWANK_TIMEOUT_MS 10000 + +static int swank_connect(void) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_port = htons(SWANK_PORT), + .sin_addr.s_addr = htonl(INADDR_LOOPBACK), + }; + + // Non-blocking connect with timeout + fcntl(fd, F_SETFL, O_NONBLOCK); + connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + + fd_set wfds; + struct timeval tv = { 2, 0 }; // 2s connect timeout + FD_ZERO(&wfds); + FD_SET(fd, &wfds); + if (select(fd + 1, NULL, &wfds, NULL, &tv) <= 0) { + close(fd); + return -1; + } + + // Check for connect error + int err = 0; + socklen_t len = sizeof(err); + getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len); + if (err) { close(fd); return -1; } + + fcntl(fd, F_SETFL, 0); // restore blocking + return fd; +} + +static int swank_send(int fd, const char *sexp) { + int slen = strlen(sexp); + char header[7]; + snprintf(header, sizeof(header), "%06x", slen); + if (write(fd, header, 6) != 6) return -1; + if (write(fd, sexp, slen) != slen) return -1; + return 0; +} + +static int swank_recv(int fd, char *buf, int buf_len, int timeout_ms) { + // Read 6-byte hex length header + char header[7] = {0}; + int total = 0; + fd_set rfds; + struct timeval tv; + + while (total < 6) { + FD_ZERO(&rfds); + FD_SET(fd, &rfds); + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + if (select(fd + 1, &rfds, NULL, NULL, &tv) <= 0) return -1; + int n = read(fd, header + total, 6 - total); + if (n <= 0) return -1; + total += n; + } + + int plen = (int)strtol(header, NULL, 16); + if (plen <= 0 || plen >= buf_len) return -1; + + // Read payload + total = 0; + while (total < plen) { + FD_ZERO(&rfds); + FD_SET(fd, &rfds); + tv.tv_sec = timeout_ms / 1000; + tv.tv_usec = (timeout_ms % 1000) * 1000; + if (select(fd + 1, &rfds, NULL, NULL, &tv) <= 0) return -1; + int n = read(fd, buf + total, plen - total); + if (n <= 0) return -1; + total += n; + } + buf[plen] = 0; + return plen; +} + +// Extract the result string from (:return (:ok "RESULT") N) +static void extract_result(const char *response, char *result, int result_len) { + // Look for (:ok ".....") + const char *ok = strstr(response, "(:ok "); + if (!ok) { + // Look for (:abort ".....") + const char *ab = strstr(response, "(:abort "); + if (ab) { + const char *q1 = strchr(ab + 8, '"'); + if (q1) { + const char *q2 = strrchr(q1 + 1, '"'); + if (q2) { + int len = (int)(q2 - q1 - 1); + if (len >= result_len) len = result_len - 1; + memcpy(result, q1 + 1, len); + result[len] = 0; + return; + } + } + } + snprintf(result, result_len, "error: %s", response); + return; + } + + const char *q1 = strchr(ok + 5, '"'); + if (!q1) { + // No quoted result — might be NIL or a non-string value + snprintf(result, result_len, "%.200s", ok + 5); + // Trim trailing ) + char *rp = result + strlen(result) - 1; + while (rp > result && (*rp == ')' || *rp == ' ')) *rp-- = 0; + return; + } + + // Find matching closing quote (handle escaped quotes) + const char *p = q1 + 1; + char *dst = result; + int remaining = result_len - 1; + while (*p && *p != '"' && remaining > 0) { + if (*p == '\\' && *(p + 1)) { + p++; // skip escape + *dst++ = *p++; + remaining--; + } else { + *dst++ = *p++; + remaining--; + } + } + *dst = 0; +} + +int swank_eval(const char *expr, char *result, int result_len) { + result[0] = 0; + + int fd = swank_connect(); + if (fd < 0) { + snprintf(result, result_len, "error: cannot connect to Swank (port %d)", SWANK_PORT); + return -1; + } + + // Consume the initial Swank greeting/info messages + char greeting[4096]; + for (int i = 0; i < 5; i++) { + int n = swank_recv(fd, greeting, sizeof(greeting), 500); + if (n <= 0) break; + // Look for :indentation-update which signals ready + if (strstr(greeting, ":indentation-update")) break; + } + + // Build eval request — escape quotes in expr + char escaped[2048]; + int ei = 0; + for (int i = 0; expr[i] && ei < 2040; i++) { + if (expr[i] == '"' || expr[i] == '\\') escaped[ei++] = '\\'; + escaped[ei++] = expr[i]; + } + escaped[ei] = 0; + + char sexp[4096]; + snprintf(sexp, sizeof(sexp), + "(:emacs-rex (swank:interactive-eval \"%s\") \"CL-USER\" :repl-thread 1)", + escaped); + + if (swank_send(fd, sexp) < 0) { + close(fd); + snprintf(result, result_len, "error: send failed"); + return -1; + } + + // Read response(s) — skip :write-string, wait for :return + char response[8192]; + for (int attempt = 0; attempt < 10; attempt++) { + int n = swank_recv(fd, response, sizeof(response), SWANK_TIMEOUT_MS); + if (n <= 0) break; + if (strstr(response, ":return")) { + extract_result(response, result, result_len); + close(fd); + return 0; + } + // :write-string contains stdout output — append to result + if (strstr(response, ":write-string")) { + const char *q1 = strchr(response + 14, '"'); + if (q1) { + const char *q2 = strrchr(q1 + 1, '"'); + if (q2) { + int cur = strlen(result); + int len = (int)(q2 - q1 - 1); + if (cur + len < result_len - 1) { + memcpy(result + cur, q1 + 1, len); + result[cur + len] = 0; + } + } + } + } + } + + close(fd); + if (!result[0]) snprintf(result, result_len, "error: no response from Swank"); + return -1; +} + +int swank_available(void) { + int fd = swank_connect(); + if (fd < 0) return 0; + close(fd); + return 1; +} diff --git a/fedac/native/src/swank-bridge.h b/fedac/native/src/swank-bridge.h new file mode 100644 --- /dev/null +++ b/fedac/native/src/swank-bridge.h @@ -0,0 +1,13 @@ +// swank-bridge.h — Evaluate Common Lisp via local Swank server (port 4005) +#ifndef SWANK_BRIDGE_H +#define SWANK_BRIDGE_H + +// Evaluate a CL expression via the local Swank server. +// Returns 0 on success, -1 on error. +// Result is written to `result` (up to result_len bytes). +int swank_eval(const char *expr, char *result, int result_len); + +// Check if Swank server is running (connect test). +int swank_available(void); + +#endif diff --git a/session-server/session.mjs b/session-server/session.mjs --- a/session-server/session.mjs +++ b/session-server/session.mjs @@ -1811,6 +1811,19 @@ })); log(`Command '${msg.cmd}' → ${msg.machineId} (${commandId})`); } } + // Swank eval: forward CL expression to device for evaluation + if (msg.type === "swank:eval" && msg.machineId && msg.expr) { + const device = machinesDevices.get(msg.machineId); + if (device && device.user === userSub && device.ws.readyState === WebSocket.OPEN) { + const evalId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6); + device.ws.send(JSON.stringify({ + type: "swank:eval", + expr: msg.expr, + evalId, + })); + log(`🔮 Swank eval → ${msg.machineId}: ${msg.expr.slice(0, 60)}`); + } + } } catch (e) { error('🖥️ Machines viewer message error:', e); } @@ -1904,6 +1917,14 @@ case "command-ack": case "command-response": if (userSub) broadcastToMachineViewers(userSub, { type: msg.type, machineId, commandId: msg.commandId, command: msg.command, data: msg.data }); + break; + + case "swank:result": + // Forward Swank eval result from device to viewer + if (userSub) broadcastToMachineViewers(userSub, { + type: "swank:result", machineId, + evalId: msg.evalId, ok: msg.ok, result: msg.result, + }); break; } } catch (e) {