Add repl-reader.ss: full-duplex client for the TLS debug REPL
ober
bbe0164c5b8c09070a7bb7b037b985451dc3dfb7
new file mode 100755 --- /dev/null +++ b/repl-reader.ss @@ -0,0 +1,98 @@ +#!/usr/bin/env jerboa +;;; repl-reader.ss — full-duplex talk to a jcode debug REPL over TLS. +;;; +;;; Usage: +;;; ./repl-reader.ss HOST:PORT 'expr1' 'expr2' ... ;; exprs from argv +;;; ./repl-reader.ss HOST:PORT ;; exprs from stdin +;;; +;;; The auth token comes from $JCODE_REPL_TOKEN, else ./.repl-token, and is +;;; sent as the first line (the REPL's `token?` gate). TLS transport is +;;; `openssl s_client`, because the REPL serves a per-process self-signed +;;; cert — the token is the real auth and TLS is just wire privacy, so cert +;;; verification is intentionally skipped (openssl's default). +;;; +;;; Framing: `openssl -quiet` implies -ign_eof (it never closes on our +;;; stdin EOF), so we instead bound its lifetime with `timeout WAIT` inside +;;; the command. We send the token + every expression, flush, and read +;;; stdout until `timeout` kills openssl and the pipe hits EOF. WAIT seconds +;;; (env JCODE_REPL_WAIT, default 3) is the full wall time per call — raise +;;; it to watch a slow eval / a running sub-agent stream its reply. +;;; +;;; Only (chezscheme) is used: the standalone `jerboa` interpreter bundles +;;; just a subset of (std ...), but open-process-ports is a kernel builtin. + +(import (chezscheme)) + +;; ---- helpers ---- + +(define (ws? c) + (or (char=? c #\space) (char=? c #\newline) + (char=? c #\return) (char=? c #\tab))) + +(define (trim s) + (let ((n (string-length s))) + (let scan-l ((i 0)) + (cond + ((and (< i n) (ws? (string-ref s i))) (scan-l (+ i 1))) + (else + (let scan-r ((j n)) + (cond + ((and (> j i) (ws? (string-ref s (- j 1)))) (scan-r (- j 1))) + (else (substring s i j))))))))) + +(define (read-token) + (let ((env (getenv "JCODE_REPL_TOKEN"))) + (cond + ((and env (> (string-length env) 0)) (trim env)) + ((file-exists? ".repl-token") + (trim (call-with-input-file ".repl-token" get-string-all))) + (else #f)))) + +(define (die msg) + (display msg (current-error-port)) + (newline (current-error-port)) + (exit 2)) + +;; ---- main ---- + +(define argv (cdr (command-line-arguments))) ;; drop the script name +(when (null? argv) + (die "usage: repl-reader.ss HOST:PORT [expr ...]")) + +(define target (car argv)) +(define exprs (cdr argv)) +(define token (or (read-token) + (die "no token: set JCODE_REPL_TOKEN or create .repl-token"))) + +(define wait + (let ((w (getenv "JCODE_REPL_WAIT"))) + (or (and w (string->number (trim w))) 3))) + +;; `exec` so the process we spawn IS timeout (clean teardown of openssl); +;; openssl stderr (cert-verify chatter) → /dev/null; we read only stdout. +(define cmd + (string-append + "exec timeout " (number->string wait) + " openssl s_client -quiet -connect " target " 2>/dev/null")) + +(call-with-values + (lambda () + (open-process-ports cmd (buffer-mode block) (make-transcoder (utf-8-codec)))) + (lambda (to-in from-out from-err pid) + ;; send token first, then the expressions + (put-string to-in token) (put-string to-in "\n") + (if (null? exprs) + (let lp () ;; exprs from our stdin, one per line + (let ((line (get-line (current-input-port)))) + (unless (eof-object? line) + (put-string to-in (trim line)) (put-string to-in "\n") + (lp)))) + (for-each (lambda (e) (put-string to-in e) (put-string to-in "\n")) exprs)) + (flush-output-port to-in) ;; leave stdin open; `timeout` ends it + (let ((out (get-string-all from-out))) + (display out) + (when (and (string? out) (> (string-length out) 0) + (not (char=? (string-ref out (- (string-length out) 1)) #\newline))) + (newline))) + (close-port from-out) + (close-port from-err)))