guile-knots/knots.scm

318 lines
10 KiB
Scheme
Raw Normal View History

2026-03-22 15:20:01 +00:00
;;; Guile Knots
;;; Copyright © 2026 Christopher Baines <mail@cbaines.net>
;;;
;;; This file is part of Guile Knots.
;;;
;;; The Guile Knots is free software; you can redistribute it and/or
;;; modify it under the terms of the GNU General Public License as
;;; published by the Free Software Foundation; either version 3 of the
;;; License, or (at your option) any later version.
;;;
;;; The Guile Knots is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;;; General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with the guix-data-service. If not, see
;;; <http://www.gnu.org/licenses/>.
2024-11-19 18:43:43 +00:00
(define-module (knots)
#:use-module (srfi srfi-1)
#:use-module (ice-9 match)
2026-01-24 15:21:15 +00:00
#:use-module (ice-9 threads)
#:use-module (ice-9 binary-ports)
2024-11-19 18:43:43 +00:00
#:use-module (ice-9 suspendable-ports)
#:use-module (rnrs bytevectors)
#:use-module (fibers)
2026-01-24 15:21:15 +00:00
#:use-module (fibers channels)
2024-12-25 20:35:40 +00:00
#:use-module (fibers conditions)
#:use-module (system repl debug)
2024-11-19 18:43:43 +00:00
#:export (call-with-default-io-waiters
2024-12-25 20:35:40 +00:00
wait-when-system-clock-behind
call-with-sigint
display/knots
simple-format/knots
format/knots
&knots-exception
make-knots-exception
knots-exception?
knots-exception-stack
print-backtrace-and-exception/knots
2026-03-18 21:39:55 +00:00
call-with-temporary-thread
spawn-fiber/knots))
2024-11-19 18:43:43 +00:00
(define (call-with-default-io-waiters thunk)
2026-03-18 08:58:41 +00:00
"Run THUNK with Guile's default blocking I/O waiters active.
This is useful when restoring the default Guile I/O waiters from
within a context (like Fibers) where different I/O waiters are used,
for example when creating a new thread from a fiber."
2024-11-19 18:43:43 +00:00
(parameterize
((current-read-waiter (@@ (ice-9 suspendable-ports)
default-read-waiter))
(current-write-waiter (@@ (ice-9 suspendable-ports)
default-write-waiter)))
(thunk)))
(define (wait-when-system-clock-behind)
2026-03-18 08:58:41 +00:00
"Block until the system clock reads at least 2001-01-02.
Useful at startup in environments (virtual machines, embedded systems)
where the clock may start at or near the Unix epoch. Prints a warning
to the current error port every 20 seconds while waiting."
;; Jan 02 2001 02:00:00
(let ((start-of-the-year-2001 978400800))
2024-11-19 18:43:43 +00:00
(while (< (current-time)
start-of-the-year-2001)
2024-11-19 18:43:43 +00:00
(simple-format (current-error-port)
"warning: system clock potentially behind, waiting\n")
(sleep 20))))
2024-12-25 20:35:40 +00:00
;; Copied from (fibers web server)
(define (call-with-sigint thunk cvar)
2026-03-18 08:58:41 +00:00
"Run THUNK with a SIGINT handler that signals the Fibers condition
CVAR. Restores the previous handler when THUNK returns.
Typical usage is to pass a condition variable to this procedure and
wait on CVAR in a fiber to implement clean shutdown on Ctrl-C:
@example
(let ((quit-cvar (make-condition)))
(call-with-sigint
(lambda () (wait quit-cvar))
quit-cvar))
@end example"
2024-12-25 20:35:40 +00:00
(let ((handler #f))
(dynamic-wind
(lambda ()
(set! handler
(sigaction SIGINT (lambda (sig) (signal-condition! cvar)))))
thunk
(lambda ()
(if handler
;; restore Scheme handler, SIG_IGN or SIG_DFL.
(sigaction SIGINT (car handler) (cdr handler))
;; restore original C handler.
(sigaction SIGINT #f))))))
2026-01-24 15:21:15 +00:00
(define (call-with-temporary-thread thunk)
(let ((channel (make-channel)))
(call-with-new-thread
(lambda ()
(call-with-default-io-waiters
(lambda ()
(with-exception-handler
(lambda (exn)
(put-message channel `(exception . ,exn)))
(lambda ()
(with-exception-handler
(lambda (exn)
(let ((stack
(match (fluid-ref %stacks)
((stack-tag . prompt-tag)
(make-stack #t
0 prompt-tag
0 (and prompt-tag 1)))
(_
(make-stack #t)))))
(raise-exception
(make-exception
exn
(make-knots-exception stack)))))
(lambda ()
(call-with-values thunk
(lambda values
(put-message channel `(values ,@values)))))))
#:unwind? #t)))))
(match (get-message channel)
(('values . results)
(apply values results))
(('exception . exn)
(raise-exception exn)))))
(define* (display/knots obj #:optional (port (current-output-port)))
2026-03-18 08:58:41 +00:00
"Write OBJ to PORT (default: current output port) as a UTF-8 byte
sequence via @code{put-bytevector}.
When used with ports without buffering, this should be safer than
display."
(put-bytevector
port
(string->utf8
(call-with-output-string
(lambda (port)
(display obj port))))))
(define (simple-format/knots port s . args)
2026-03-18 08:58:41 +00:00
"Like @code{simple-format} but should be safer when used with a port
without buffering."
(let ((str (apply simple-format #f s args)))
(if (eq? #f port)
str
(display/knots
str
(if (eq? #t port)
(current-output-port)
port)))))
(define (format/knots port s . args)
2026-03-18 08:58:41 +00:00
"Like @code{format} but should be safer when used with a port
without buffering."
(let ((str (apply format #f s args)))
(if (eq? #f port)
str
(display/knots
str
(if (eq? #t port)
(current-output-port)
port)))))
(define &knots-exception
(make-exception-type '&knots-exception
&exception
'(stack)))
(define make-knots-exception
(record-constructor &knots-exception))
(define knots-exception?
2025-02-27 12:18:17 +00:00
(exception-predicate &knots-exception))
(define knots-exception-stack
(exception-accessor
&knots-exception
(record-accessor &knots-exception 'stack)))
(define* (print-backtrace-and-exception/knots
exn
#:key (port (current-error-port)))
(define (get-string port stack)
(define stack-len
(stack-length stack))
(let ((knots-stacks
(map knots-exception-stack
(filter knots-exception?
(simple-exceptions exn)))))
(let* ((stack-vec
(stack->vector stack))
(stack-vec-length
(vector-length stack-vec)))
(print-frames (list->vector
(drop
(vector->list stack-vec)
(if (< stack-vec-length 5)
0
4)))
port
#:count (stack-length stack)))
(for-each
(lambda (stack)
(let* ((stack-vec
(stack->vector stack))
(stack-vec-length
(vector-length stack-vec)))
(print-frames (list->vector
(drop
(vector->list stack-vec)
(if (< stack-vec-length 4)
0
3)))
port
#:count (stack-length stack))))
knots-stacks)
(print-exception
port
(if (null? knots-stacks)
(stack-ref stack
(if (< stack-len 4)
stack-len
4))
(let* ((stack (last knots-stacks))
(stack-len (stack-length stack)))
(stack-ref stack
(if (< stack-len 3)
stack-len
3))))
'%exception
(list exn))))
(let* ((stack
(match (fluid-ref %stacks)
((stack-tag . prompt-tag)
(make-stack #t
0 prompt-tag
0 (and prompt-tag 1)))
(_
(make-stack #t))))
(string-port
(open-output-string))
(error-string
(with-exception-handler
(lambda (exn)
(display/knots (get-output-string string-port)
port)
(close-output-port string-port)
(display/knots "\n\n" port)
(let* ((stack (make-stack #t))
(backtrace
(call-with-output-string
(lambda (port)
(display-backtrace stack port)
(newline port)))))
(display/knots backtrace))
(simple-format/knots
port
"\nexception in print-backtrace-and-exception/knots: ~A\n"
exn)
(raise-exception exn))
(lambda ()
(get-string string-port stack)
(let ((str (get-output-string string-port)))
(close-output-port string-port)
str)))))
(display/knots error-string port)))
(define* (spawn-fiber/knots thunk #:optional scheduler #:key parallel?)
2026-03-18 08:58:41 +00:00
"Spawn a fiber to run THUNK, with knots exception handling.
Accepts the same optional SCHEDULER and @code{#:parallel?} arguments
as @code{spawn-fiber}."
(spawn-fiber
(lambda ()
(with-exception-handler
(lambda (exn)
(display/knots "Uncaught exception in task:\n"
(current-error-port))
(print-backtrace-and-exception/knots exn))
(lambda ()
(with-exception-handler
(lambda (exn)
(let ((stack
(match (fluid-ref %stacks)
((stack-tag . prompt-tag)
(make-stack #t
0 prompt-tag
0 (and prompt-tag 1)))
(_
(make-stack #t)))))
(raise-exception
(make-exception
exn
(make-knots-exception stack)))))
thunk))
#:unwind? #t))
scheduler
#:parallel? parallel?))