Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Swank bridge — eval CL on any online device from dashboard

Device side: swank-bridge.c connects to localhost:4005 Swank server,
sends eval requests, parses results. Machines WS handles swank:eval
messages and returns swank:result.

Session server: routes swank:eval from viewer → device, and
swank:result from device → viewer.

Dashboard viewers can send {type:"swank:eval", machineId, expr}
and receive {type:"swank:result", machineId, evalId, ok, result}.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+287
+1
fedac/native/Makefile
··· 84 84 $(SRCDIR)/camera.c \ 85 85 $(SRCDIR)/pty.c \ 86 86 $(SRCDIR)/machines.c \ 87 + $(SRCDIR)/swank-bridge.c \ 87 88 $(SRCDIR)/js-bindings.c \ 88 89 $(SRCDIR)/recorder.c 89 90
+22
fedac/native/src/machines.c
··· 3 3 // heartbeats, log upload, and remote command reception. 4 4 5 5 #include "machines.h" 6 + #include "swank-bridge.h" 6 7 7 8 #include <stdio.h> 8 9 #include <stdlib.h> ··· 280 281 int count = ws_poll(m->ws); 281 282 for (int i = 0; i < count; i++) { 282 283 const char *raw = m->ws->messages[i]; 284 + 285 + // Handle swank:eval — evaluate CL via local Swank server 286 + if (strstr(raw, "\"type\":\"swank:eval\"")) { 287 + char expr[2048] = "", eval_id[32] = ""; 288 + json_get_str(raw, "expr", expr, sizeof(expr)); 289 + json_get_str(raw, "evalId", eval_id, sizeof(eval_id)); 290 + if (expr[0]) { 291 + ac_log("[machines] swank:eval id=%s expr=%.80s\n", eval_id, expr); 292 + char result[4096] = ""; 293 + int rc = swank_eval(expr, result, sizeof(result)); 294 + // Send result back 295 + char escaped_result[8192]; 296 + json_escape(result, escaped_result, sizeof(escaped_result)); 297 + char msg[12288]; 298 + snprintf(msg, sizeof(msg), 299 + "{\"type\":\"swank:result\",\"evalId\":\"%s\",\"ok\":%s,\"result\":\"%s\"}", 300 + eval_id, rc == 0 ? "true" : "false", escaped_result); 301 + ws_send(m->ws, msg); 302 + } 303 + continue; 304 + } 283 305 284 306 // Check if it's a command message 285 307 if (!strstr(raw, "\"type\":\"command\"")) continue;
+230
fedac/native/src/swank-bridge.c
··· 1 + // swank-bridge.c — Evaluate CL expressions via local Swank server 2 + // 3 + // Swank wire protocol: 4 + // Send: 6-digit hex length + S-expression 5 + // Recv: 6-digit hex length + S-expression 6 + // 7 + // We send: (:emacs-rex (swank:interactive-eval "EXPR") "CL-USER" :repl-thread 1) 8 + // We get: (:return (:ok "RESULT") 1) 9 + 10 + #include "swank-bridge.h" 11 + #include <stdio.h> 12 + #include <stdlib.h> 13 + #include <string.h> 14 + #include <unistd.h> 15 + #include <sys/socket.h> 16 + #include <netinet/in.h> 17 + #include <arpa/inet.h> 18 + #include <errno.h> 19 + #include <fcntl.h> 20 + #include <sys/select.h> 21 + 22 + #define SWANK_PORT 4005 23 + #define SWANK_TIMEOUT_MS 10000 24 + 25 + static int swank_connect(void) { 26 + int fd = socket(AF_INET, SOCK_STREAM, 0); 27 + if (fd < 0) return -1; 28 + 29 + struct sockaddr_in addr = { 30 + .sin_family = AF_INET, 31 + .sin_port = htons(SWANK_PORT), 32 + .sin_addr.s_addr = htonl(INADDR_LOOPBACK), 33 + }; 34 + 35 + // Non-blocking connect with timeout 36 + fcntl(fd, F_SETFL, O_NONBLOCK); 37 + connect(fd, (struct sockaddr *)&addr, sizeof(addr)); 38 + 39 + fd_set wfds; 40 + struct timeval tv = { 2, 0 }; // 2s connect timeout 41 + FD_ZERO(&wfds); 42 + FD_SET(fd, &wfds); 43 + if (select(fd + 1, NULL, &wfds, NULL, &tv) <= 0) { 44 + close(fd); 45 + return -1; 46 + } 47 + 48 + // Check for connect error 49 + int err = 0; 50 + socklen_t len = sizeof(err); 51 + getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len); 52 + if (err) { close(fd); return -1; } 53 + 54 + fcntl(fd, F_SETFL, 0); // restore blocking 55 + return fd; 56 + } 57 + 58 + static int swank_send(int fd, const char *sexp) { 59 + int slen = strlen(sexp); 60 + char header[7]; 61 + snprintf(header, sizeof(header), "%06x", slen); 62 + if (write(fd, header, 6) != 6) return -1; 63 + if (write(fd, sexp, slen) != slen) return -1; 64 + return 0; 65 + } 66 + 67 + static int swank_recv(int fd, char *buf, int buf_len, int timeout_ms) { 68 + // Read 6-byte hex length header 69 + char header[7] = {0}; 70 + int total = 0; 71 + fd_set rfds; 72 + struct timeval tv; 73 + 74 + while (total < 6) { 75 + FD_ZERO(&rfds); 76 + FD_SET(fd, &rfds); 77 + tv.tv_sec = timeout_ms / 1000; 78 + tv.tv_usec = (timeout_ms % 1000) * 1000; 79 + if (select(fd + 1, &rfds, NULL, NULL, &tv) <= 0) return -1; 80 + int n = read(fd, header + total, 6 - total); 81 + if (n <= 0) return -1; 82 + total += n; 83 + } 84 + 85 + int plen = (int)strtol(header, NULL, 16); 86 + if (plen <= 0 || plen >= buf_len) return -1; 87 + 88 + // Read payload 89 + total = 0; 90 + while (total < plen) { 91 + FD_ZERO(&rfds); 92 + FD_SET(fd, &rfds); 93 + tv.tv_sec = timeout_ms / 1000; 94 + tv.tv_usec = (timeout_ms % 1000) * 1000; 95 + if (select(fd + 1, &rfds, NULL, NULL, &tv) <= 0) return -1; 96 + int n = read(fd, buf + total, plen - total); 97 + if (n <= 0) return -1; 98 + total += n; 99 + } 100 + buf[plen] = 0; 101 + return plen; 102 + } 103 + 104 + // Extract the result string from (:return (:ok "RESULT") N) 105 + static void extract_result(const char *response, char *result, int result_len) { 106 + // Look for (:ok ".....") 107 + const char *ok = strstr(response, "(:ok "); 108 + if (!ok) { 109 + // Look for (:abort ".....") 110 + const char *ab = strstr(response, "(:abort "); 111 + if (ab) { 112 + const char *q1 = strchr(ab + 8, '"'); 113 + if (q1) { 114 + const char *q2 = strrchr(q1 + 1, '"'); 115 + if (q2) { 116 + int len = (int)(q2 - q1 - 1); 117 + if (len >= result_len) len = result_len - 1; 118 + memcpy(result, q1 + 1, len); 119 + result[len] = 0; 120 + return; 121 + } 122 + } 123 + } 124 + snprintf(result, result_len, "error: %s", response); 125 + return; 126 + } 127 + 128 + const char *q1 = strchr(ok + 5, '"'); 129 + if (!q1) { 130 + // No quoted result — might be NIL or a non-string value 131 + snprintf(result, result_len, "%.200s", ok + 5); 132 + // Trim trailing ) 133 + char *rp = result + strlen(result) - 1; 134 + while (rp > result && (*rp == ')' || *rp == ' ')) *rp-- = 0; 135 + return; 136 + } 137 + 138 + // Find matching closing quote (handle escaped quotes) 139 + const char *p = q1 + 1; 140 + char *dst = result; 141 + int remaining = result_len - 1; 142 + while (*p && *p != '"' && remaining > 0) { 143 + if (*p == '\\' && *(p + 1)) { 144 + p++; // skip escape 145 + *dst++ = *p++; 146 + remaining--; 147 + } else { 148 + *dst++ = *p++; 149 + remaining--; 150 + } 151 + } 152 + *dst = 0; 153 + } 154 + 155 + int swank_eval(const char *expr, char *result, int result_len) { 156 + result[0] = 0; 157 + 158 + int fd = swank_connect(); 159 + if (fd < 0) { 160 + snprintf(result, result_len, "error: cannot connect to Swank (port %d)", SWANK_PORT); 161 + return -1; 162 + } 163 + 164 + // Consume the initial Swank greeting/info messages 165 + char greeting[4096]; 166 + for (int i = 0; i < 5; i++) { 167 + int n = swank_recv(fd, greeting, sizeof(greeting), 500); 168 + if (n <= 0) break; 169 + // Look for :indentation-update which signals ready 170 + if (strstr(greeting, ":indentation-update")) break; 171 + } 172 + 173 + // Build eval request — escape quotes in expr 174 + char escaped[2048]; 175 + int ei = 0; 176 + for (int i = 0; expr[i] && ei < 2040; i++) { 177 + if (expr[i] == '"' || expr[i] == '\\') escaped[ei++] = '\\'; 178 + escaped[ei++] = expr[i]; 179 + } 180 + escaped[ei] = 0; 181 + 182 + char sexp[4096]; 183 + snprintf(sexp, sizeof(sexp), 184 + "(:emacs-rex (swank:interactive-eval \"%s\") \"CL-USER\" :repl-thread 1)", 185 + escaped); 186 + 187 + if (swank_send(fd, sexp) < 0) { 188 + close(fd); 189 + snprintf(result, result_len, "error: send failed"); 190 + return -1; 191 + } 192 + 193 + // Read response(s) — skip :write-string, wait for :return 194 + char response[8192]; 195 + for (int attempt = 0; attempt < 10; attempt++) { 196 + int n = swank_recv(fd, response, sizeof(response), SWANK_TIMEOUT_MS); 197 + if (n <= 0) break; 198 + if (strstr(response, ":return")) { 199 + extract_result(response, result, result_len); 200 + close(fd); 201 + return 0; 202 + } 203 + // :write-string contains stdout output — append to result 204 + if (strstr(response, ":write-string")) { 205 + const char *q1 = strchr(response + 14, '"'); 206 + if (q1) { 207 + const char *q2 = strrchr(q1 + 1, '"'); 208 + if (q2) { 209 + int cur = strlen(result); 210 + int len = (int)(q2 - q1 - 1); 211 + if (cur + len < result_len - 1) { 212 + memcpy(result + cur, q1 + 1, len); 213 + result[cur + len] = 0; 214 + } 215 + } 216 + } 217 + } 218 + } 219 + 220 + close(fd); 221 + if (!result[0]) snprintf(result, result_len, "error: no response from Swank"); 222 + return -1; 223 + } 224 + 225 + int swank_available(void) { 226 + int fd = swank_connect(); 227 + if (fd < 0) return 0; 228 + close(fd); 229 + return 1; 230 + }
+13
fedac/native/src/swank-bridge.h
··· 1 + // swank-bridge.h — Evaluate Common Lisp via local Swank server (port 4005) 2 + #ifndef SWANK_BRIDGE_H 3 + #define SWANK_BRIDGE_H 4 + 5 + // Evaluate a CL expression via the local Swank server. 6 + // Returns 0 on success, -1 on error. 7 + // Result is written to `result` (up to result_len bytes). 8 + int swank_eval(const char *expr, char *result, int result_len); 9 + 10 + // Check if Swank server is running (connect test). 11 + int swank_available(void); 12 + 13 + #endif
+21
session-server/session.mjs
··· 1811 1811 log(`Command '${msg.cmd}' → ${msg.machineId} (${commandId})`); 1812 1812 } 1813 1813 } 1814 + // Swank eval: forward CL expression to device for evaluation 1815 + if (msg.type === "swank:eval" && msg.machineId && msg.expr) { 1816 + const device = machinesDevices.get(msg.machineId); 1817 + if (device && device.user === userSub && device.ws.readyState === WebSocket.OPEN) { 1818 + const evalId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6); 1819 + device.ws.send(JSON.stringify({ 1820 + type: "swank:eval", 1821 + expr: msg.expr, 1822 + evalId, 1823 + })); 1824 + log(`🔮 Swank eval → ${msg.machineId}: ${msg.expr.slice(0, 60)}`); 1825 + } 1826 + } 1814 1827 } catch (e) { 1815 1828 error('🖥️ Machines viewer message error:', e); 1816 1829 } ··· 1904 1917 case "command-ack": 1905 1918 case "command-response": 1906 1919 if (userSub) broadcastToMachineViewers(userSub, { type: msg.type, machineId, commandId: msg.commandId, command: msg.command, data: msg.data }); 1920 + break; 1921 + 1922 + case "swank:result": 1923 + // Forward Swank eval result from device to viewer 1924 + if (userSub) broadcastToMachineViewers(userSub, { 1925 + type: "swank:result", machineId, 1926 + evalId: msg.evalId, ok: msg.ok, result: msg.result, 1927 + }); 1907 1928 break; 1908 1929 } 1909 1930 } catch (e) {