|
1 | 1 | #lang racket/base |
2 | 2 |
|
3 | 3 | (require rivet/backend |
| 4 | + json |
| 5 | + racket/date |
| 6 | + racket/file |
| 7 | + racket/format |
4 | 8 | racket/string |
5 | 9 | "config.rkt" |
6 | 10 | "date-parser.rkt" |
|
14 | 18 | (provide start |
15 | 19 | start-stdio) |
16 | 20 |
|
| 21 | +;; Rivet's diagnostic sink defaults to current-error-port. A Windows |
| 22 | +;; GUI-subsystem process has no console, and the embedded Chez runtime |
| 23 | +;; lazily allocates one on the first stderr write — the black console |
| 24 | +;; window users saw open before the app window, streaming RVT1 protocol |
| 25 | +;; records (on mac/Linux stderr lands in a terminal or /dev/null, so it |
| 26 | +;; never showed there). Diagnostics keep their value as JSONL appended to |
| 27 | +;; ~/.taskly/diagnostics.log instead; stderr is never touched, so no |
| 28 | +;; console is ever allocated. Rivet already swallows sink exceptions, so |
| 29 | +;; an unwritable path drops records rather than failing the backend. |
| 30 | +(define diagnostics-port (box #f)) |
| 31 | +(define diagnostics-lock (make-semaphore 1)) |
| 32 | + |
| 33 | +(define (diagnostics-log-path) |
| 34 | + (build-path (taskly-directory) "diagnostics.log")) |
| 35 | + |
| 36 | +(define (diagnostic-timestamp) |
| 37 | + ;; Local ISO-8601 (no zone suffix): machine-sortable, human-readable. |
| 38 | + (define d (seconds->date (current-seconds) #f)) |
| 39 | + (format "~a-~a-~aT~a:~a:~a" |
| 40 | + (date-year d) |
| 41 | + (~r (date-month d) #:min-width 2 #:pad-string "0") |
| 42 | + (~r (date-day d) #:min-width 2 #:pad-string "0") |
| 43 | + (~r (date-hour d) #:min-width 2 #:pad-string "0") |
| 44 | + (~r (date-minute d) #:min-width 2 #:pad-string "0") |
| 45 | + (~r (date-second d) #:min-width 2 #:pad-string "0"))) |
| 46 | + |
| 47 | +(define (taskly-diagnostic-sink record) |
| 48 | + (call-with-semaphore |
| 49 | + diagnostics-lock |
| 50 | + (lambda () |
| 51 | + (define out |
| 52 | + (or (unbox diagnostics-port) |
| 53 | + (let ([port |
| 54 | + (begin |
| 55 | + ;; The directory may not exist yet during the very first |
| 56 | + ;; handshake (open_database creates it right after). |
| 57 | + (with-handlers ([exn:fail? void]) |
| 58 | + (make-directory* (taskly-directory))) |
| 59 | + (open-output-file (diagnostics-log-path) |
| 60 | + #:exists 'append))]) |
| 61 | + (set-box! diagnostics-port port) |
| 62 | + port))) |
| 63 | + (write-json (hash-set record 'ts (diagnostic-timestamp)) out) |
| 64 | + (newline out) |
| 65 | + (flush-output out)))) |
| 66 | + |
| 67 | +;; Module top level: runs on the boot thread that later invokes `start`, |
| 68 | +;; so serve-fds picks this sink up as its default. |
| 69 | +(current-rivet-diagnostic-sink taskly-diagnostic-sink) |
| 70 | + |
17 | 71 | (define current-service (box #f)) |
18 | 72 | (define-event changed) |
19 | 73 |
|
|
235 | 289 | ;; stdin/stdout. This is deliberately only a transport alternative: the |
236 | 290 | ;; Taskly service and RPC surface remain identical to the embedded host. |
237 | 291 | (define (start-stdio) |
238 | | - (serve (current-input-port) (current-output-port))) |
| 292 | + (serve (current-input-port) (current-output-port) |
| 293 | + ;; serve's own default sink is `void`; pass the file sink so the |
| 294 | + ;; managed dev host records diagnostics like the packaged one. |
| 295 | + #:diagnostic-sink (current-rivet-diagnostic-sink))) |
239 | 296 |
|
240 | 297 | (module+ main |
241 | 298 | (start-stdio)) |
0 commit comments