;; the surflet handler
;; Copyright Andreas Bernauer, 2002
(define *debug* #t)
;;; session-table: entry for every new request on a surflet page
(define-record-type session :session
(make-session surflet-name memo
continuation-table continuation-table-lock
continuation-counter
surflet-data)
session?
(surflet-name session-surflet-name)
(memo session-memo set-session-memo!)
(continuation-table session-continuation-table)
(continuation-table-lock session-continuation-table-lock)
(continuation-counter session-continuation-counter)
(surflet-data session-surflet-data set-session-surflet-data!))
(define-record-type memo :memo
(make-memo message value new-memo)
memo?
(message memo:message set-memo:message) ;kill, killed, adjust-timeout
(value memo:value set-memo:value)
(new-memo memo:new-memo set-memo:new-memo))
(define (make-default-memo)
(make-memo 'kill #f #f))
;; caller must do locking stuff
(define (memo-killed! memo)
(set-memo:message memo 'killed))
(define-record-type instance :instance
(make-instance session-id return-continuation)
instance?
(session-id really-instance-session-id
set-instance-session-id!)
(return-continuation really-instance-return-continuation
set-instance-return-continuation!))
(define-record-type options :options
(make-options surflet-path surflet-prefix cache-surflets? session-lifetime)
options?
(surflet-path options:surflet-path set-options:surflet-path)
(surflet-prefix options:surflet-prefix set-options:surflet-prefix)
(cache-surflets? options:cache-surflets? set-options:cache-surflets?)
;; session lifetime is in seconds
(session-lifetime options:session-lifetime set-options:session-lifetime))
;; Surflet-prefix is unused now. Formerly, it contained the virtual
;; path prefix for the handler.
(define (make-default-options surflet-path surflet-prefix)
(make-options surflet-path surflet-prefix #t 600))
(define *options* (make-preserved-thread-fluid #f))
;; preserved thread fluid because between different calls to
;; surflet-handler the options shall remain the same.
(define (make-fluid-selector selector)
(lambda () (selector (thread-fluid *options*))))
(define (make-fluid-setter setter)
(lambda (value)
(setter (thread-fluid *options*) value)))
(define options-surflet-path (make-fluid-selector options:surflet-path))
(define options-surflet-prefix (make-fluid-selector options:surflet-prefix))
(define options-cache-surflets? (make-fluid-selector options:cache-surflets?))
(define options-session-lifetime (make-fluid-selector options:session-lifetime))
(define set-options-cache-surflets? (make-fluid-setter set-options:cache-surflets?))
(define set-options-session-lifetime (make-fluid-setter set-options:session-lifetime))
(define *session-table* (make-integer-table)) ; session-id is index
(define *session-table-lock* (make-lock))
(define random
(let* ((source (make-random-source))
(random-integer (begin
(random-source-randomize! source)
(random-source-make-integers source))))
(lambda ()
(random-integer 1073741824)))) ; I hope, 1+ billion is enough....
(define (surflet-handler surflet-path)
(set-thread-fluid! *options* (make-default-options surflet-path #f))
(lambda (path req)
(if (pair? path) ; need at least one element
(let ((request-method (request-method req))
(path-string (uri-path->uri path)))
(if (or (string=? request-method "GET")
(string=? request-method "POST"))
(if (resume-url? path-string)
(resume-url path-string surflet-path req)
(launch-new-session path-string surflet-path req))
(make-error-response (status-code method-not-allowed) req
request-method)))
(make-error-response (status-code bad-request) req
(format #f "Bad path: ~s" path)))))
(define (launch-new-session path-string surflet-path req)
(cond
((file-not-exists? (absolute-file-name path-string surflet-path))
(make-error-response (status-code not-found) req path-string))
((string=? (file-name-extension path-string) ".scm")
(obtain-lock *session-table-lock*)
;; no access to session table until new session-id is saved
(let ((session-id (generate-new-table-id *session-table*))
(memo (make-default-memo)))
(table-set! *session-table* session-id
(make-session path-string ; used to make
; redirections to origin
memo
(make-integer-table) ; continuation table
(make-lock) ; continuation table lock
(make-thread-safe-counter) ; continuation counter
#f)) ; surflet-data
(release-lock *session-table-lock*)
(register-instance! session-id 'no-return)
(with-fatal-handler
;; Catch conditions from get-surflet-rt-structure.
(lambda (condition decline)
(delete-session! session-id)
(bad-gateway-error-response req path-string condition))
(let ((surflet (get-surflet-rt-structure path-string surflet-path)))
(fork-thread
(session-surveillance session-id
(+ (time) (options-session-lifetime))
memo))
(reset
(with-fatal-handler
;; Catch conditions that occur while running the surflet.
(lambda (condition decline)
(delete-session! session-id)
;; Restore correct continuation with shift.
(shift unused
(bad-gateway-error-response req path-string condition)))
(with-cwd surflet-path
(with-names-from-rt-structure
surflet surflet-interface
(main req))))))))) ; Launch serlvet's main procedure.
(else ; We'll serve every non-scm file.
;; We need access to SEND-FILE-RESPONSE of
;; HTTPD-FILE-DIR-HANDLERS. In the official SUnet release, we
;; don't have it, so we disable this feature here.
; (let ((full-file-name (absolute-file-name path-string surflet-path)))
; (send-file-response full-file-name
; (file-info full-file-name)
; req))
(make-error-response (status-code forbidden) req
"Can't serve other than Scheme files."
path-string))
))
(define (session-surveillance session-id time-to-die memo)
(lambda ()
(let loop ((time-to-die time-to-die)
(memo memo))
(debug "session-surveillance[~s]: going to sleep until ~a"
session-id (format-date "~c" (date time-to-die)))
(let ((seconds-to-sleep (- time-to-die (time))))
(if (positive? seconds-to-sleep)
(sleep (* 1000 seconds-to-sleep))))
;; check state of the world
(case (memo:message memo)
((killed) ; too late
(debug "session-surveillance[~s]: session already killed, dieing"
session-id)
)
((adjust-timeout) ; new timeout
(debug "session-surveillance[~s]: adjusting timeout" session-id)
(loop (memo:value memo)
(memo:new-memo memo)))
((kill) ; kill session
(debug "session-surveillance[~s]: killing"
session-id)
(obtain-lock *session-table-lock*)
(table-set! *session-table* session-id #f)
(release-lock *session-table-lock*))
(else
(format (current-error-port)
"session-surveillance[~s]: unknown message ~s; dieing"
session-id (memo:message memo)))))))
;; try to get continuation-table and then the continuation
(define resume-url
(let ((bad-request
(lambda (path-string req)
(make-error-response
(status-code bad-request) req
(format #f
"
There may be several reasons, why your request for a surflet was denied:
In any case, you may try to restart the surflet from the beginning. Your browser may also have cached an old session of this surflet. In this case, try to reload the page.
" (resume-url-surflet-name path-string))))) (lookup-continuation-table (lambda (session continuation-table continuation-id) (let ((continuation-table-lock (session-continuation-table-lock session))) (obtain-lock continuation-table-lock) (let ((result (table-ref continuation-table continuation-id))) (release-lock continuation-table-lock) result))))) (lambda (path-string surflet-path req) (receive (session-id continuation-id) (resume-url-ids path-string) (let ((session (session-lookup session-id))) (if session (let* ((continuation-table (session-continuation-table session)) (resume (lookup-continuation-table session continuation-table continuation-id))) (if resume (with-cwd surflet-path (reset (begin (register-instance! session-id 'no-return) (resume req)))) (bad-request path-string req))) (bad-request path-string req))) )))) (define (send/suspend response-maker) (shift return (let* ((session-id (instance-session-id)) (session (session-lookup session-id))) ;; the instance might be deleted in the meanwhile (if session (begin (session-adjust-timeout! session-id) (let ((continuations-table (session-continuation-table session)) (continuation-table-lock (session-continuation-table-lock session)) (continuation-counter (session-next-continuation-counter session))) (obtain-lock continuation-table-lock) (let ((continuation-id (generate-new-table-id continuations-table))) (table-set! continuations-table continuation-id return) (release-lock continuation-table-lock) (let ((new-url (make-resume-url (session-surflet-name session) session-id continuation-counter continuation-id))) (make-surflet-response (response-maker new-url)))))) (make-error-response (status-code not-found) #f "The URL refers to a surflet, whose session is no longer alive."))))) (define (send/finish response) (delete-session! (instance-session-id)) (shift unused (make-surflet-response response))) (define (send response) (shift unused (make-surflet-response response))) (define (make-surflet-response response) (let ((buffer (open-output-string)) (surflet-in-port #f) ;; FIXME: no input-port available (options #f)) ;; FIXME: No access to httpd-options :-( (if (writer-body? (response-body response)) (begin ;; Error-handler is already installed. ;; Force string-output to resolve all send/... calls. (display-http-body (response-body response) surflet-in-port buffer options) ;; Create write-out-response for webserver. (make-response (response-code response) (response-message response) (response-seconds response) (response-mime response) (response-extras response) (make-writer-body (lambda (out options) (display (get-output-string buffer) out))))) (make-error-response (status-code bad-gateway) #f "The surflet returned an invalid response object (no writer-body).")))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; access to session-table (define (session-lookup session-id) (obtain-lock *session-table-lock*) (let ((result (table-ref *session-table* session-id))) (release-lock *session-table-lock*) result)) (define (session-next-continuation-counter session) (thread-safe-counter-next! (session-continuation-counter session))) (define (delete-session! session-id) (obtain-lock *session-table-lock*) ;; notify surveillance of session being alread killed (prevents ;; surveillance of killing new session that has the same number by ;; accident) (let ((session (table-ref *session-table* session-id))) (memo-killed! (session-memo session))) ;; why can't table entries be deleted correctly? (table-set! *session-table* session-id #f) (release-lock *session-table-lock*)) (define (session-adjust-timeout! session-id) (obtain-lock *session-table-lock*) (let* ((session (table-ref *session-table* session-id)) (memo (session-memo session)) (new-memo (make-default-memo))) ;; Do it this way: new values and then new message (set-memo:value memo (+ (time) (options-session-lifetime))) (set-memo:new-memo memo new-memo) ;; I don't think we need locking here. Do you agree? (set-session-memo! session new-memo) (set-memo:message memo 'adjust-timeout)) (release-lock *session-table-lock*)) ;; adjusts the timeout of the current session (define (adjust-timeout) (session-adjust-timeout! (instance-session-id))) (define (reset-session-table!) (with-fatal-error-handler (lambda (condtion decline) (release-lock *session-table-lock*) (decline)) (lambda () (obtain-lock *session-table-lock*) ;; notify session killing (table-walk (lambda (session-id session) (memo-killed! (session-memo session))) *session-table*) (set! *session-table* (make-integer-table)) (release-lock *session-table*)))) (define (get-sessions) (obtain-lock *session-table-lock*) (let ((sessions '())) (table-walk (lambda (session-id session-entry) (set! sessions (cons (cons session-id session-entry) sessions))) *session-table*) (release-lock *session-table-lock*) sessions)) (define (get-continuations session-id) (let ((session (session-lookup session-id))) (if session (let ((continuation-table-lock (session-continuation-table-lock session)) (continuation-table (session-continuation-table session)) (continuations '())) (obtain-lock continuation-table-lock) (table-walk (lambda (continuation-id continuation-entry) (set! continuations (cons (cons continuation-id continuation-entry) continuations))) continuation-table) (release-lock continuation-table-lock) continuations) '()))) (define (delete-continuation! session-id continuation-id) (let ((session (session-lookup session-id))) (if session (let ((continuation-table-lock (session-continuation-table-lock session)) (continuation-table (session-continuation-table session)) (continuations '())) (obtain-lock continuation-table-lock) (if (table-ref continuation-table continuation-id) (table-set! continuation-table continuation-id #f)) (release-lock continuation-table-lock))))) (define (set-surflet-data! new-data) (let ((session (session-lookup (instance-session-id)))) (if session (begin (set-session-surflet-data! session new-data) #t) #f))) (define (get-surflet-data) (let ((session (session-lookup (instance-session-id)))) (if session (session-surflet-data session) (error "Instance no longer alive.")))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ID generation ;; locking must be done by caller ;; FIXME?: this may loop forever, if the table is full (can this happen?) (define (generate-new-table-id table) (let loop ((id (random))) (if (table-ref table id) (loop (random)) id))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SURFLETs CACHE (define *surflet-table* (make-string-table)) ; path-string is index (define *surflet-table-lock* (make-lock)) ;; SURFLET-NAME is like "news-dir/latest-news.scm" (define (get-surflet-rt-structure surflet-name directory) (let* ((full-surflet-name (absolute-file-name surflet-name directory)) (load-surflet (lambda (cached?) (with-fatal-handler* (lambda (condition decline) (if cached? (release-lock *surflet-table-lock*)) (decline)) (lambda () ;; load-config-file does not care about cwd(?) ;; --> absolute file name needed (load-config-file full-surflet-name) ;; surflet-structure to load must be named "surflet" (let ((surflet-structure (reify-structure 'surflet))) (load-structure surflet-structure) (if cached? (begin (table-set! *surflet-table* full-surflet-name (cons surflet-structure (file-last-mod full-surflet-name))) ;; only now the lock may be released (release-lock *surflet-table-lock*))) surflet-structure)))))) (if (options-cache-surflets?) (begin ;; The lock is only obtained and released, if surflets are ;; cached. LOAD-SURFLET gets the CACHED? parameter, so ;; nothing may happen, if in the meanwhile caching is turned ;; off. (obtain-lock *surflet-table-lock*) (let ((surflet (table-ref *surflet-table* full-surflet-name))) (if surflet (if (equal? (file-last-mod full-surflet-name) (cdr surflet)) (begin (release-lock *surflet-table-lock*) (car surflet)) (load-surflet #t)) (load-surflet #t)))) (load-surflet #f)))) (define (get-loaded-surflets) (obtain-lock *surflet-table-lock*) (let ((loaded-surflets '())) (table-walk (lambda (surflet-path rt-structure) (set! loaded-surflets (cons surflet-path loaded-surflets))) *surflet-table*) (release-lock *surflet-table-lock*) loaded-surflets)) (define (unload-surflet surflet-name) (obtain-lock *surflet-table-lock*) (if (table-ref *surflet-table* surflet-name) (table-set! *surflet-table* surflet-name #f)) (release-lock *surflet-table-lock*)) (define (reset-surflet-cache!) (with-fatal-error-handler* (lambda (condition decline) (release-lock *surflet-table-lock*) (decline)) (lambda () (obtain-lock *surflet-table-lock*) (set! *surflet-table* (make-string-table)) (release-lock *surflet-table-lock*)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; INSTANCE (define *instance* (make-thread-cell #f)) (define (register-instance! session-id return-continuation) (thread-cell-set! *instance* (make-instance session-id return-continuation))) ;(define (save-instance-return-continuation! return-continuation) ; (set-instance-session-id! (thread-cell-ref *instance*) ; return-continuation)) (define (instance-session-id) (really-instance-session-id (thread-cell-ref *instance*))) ;; unused (define (instance-return-continuation) (really-instance-return-continuation (thread-cell-ref *instance*))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; RESUME-URL (define *resume-url-regexp* (rx (submatch (* (- printing ";"))) ";k" (submatch (* digit)) ; Instance-ID ";c" (+ digit) "-" (submatch (* digit)))) ; Continuation-ID (define (make-resume-url path-string session-id continuation-counter continuation-id) (string-append path-string ";k" (number->string (instance-session-id)) ";c" (number->string continuation-counter) "-" (number->string continuation-id))) (define (resume-url-session-id id-url) (receive (session-id continuation-id) (resume-url-ids id-url) session-id)) (define (resume-url-continuation-id id-url) (receive (session-id continuation-id) (resume-url-ids id-url) continuation-id)) (define (resume-url-ids id-url) (let ((match (regexp-search *resume-url-regexp* id-url))) (if match (values (string->number (match:substring match 2)) (string->number (match:substring match 3))) (values #f #f)))) (define (resume-url-surflet-name id-url) (let ((match (regexp-search *resume-url-regexp* id-url))) (if match (match:substring match 1) (values #f #f)))) (define (resume-url? id-url) (regexp-search? *resume-url-regexp* id-url)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; thread-safe counter (define-record-type counter :counter (really-make-counter counter lock) (counter counter-counter set-counter-counter!) (lock counter-lock)) (define (make-thread-safe-counter) (really-make-counter 0 (make-lock))) ;;; read current value (define (thread-safe-counter counter) (obtain-lock (counter-lock counter)) (let ((result (counter-counter counter))) (release-lock (counter-lock counter)) result)) ;;; make next value and return it (define (thread-safe-counter-next! counter) (obtain-lock (counter-lock counter)) (let ((result (+ 1 (counter-counter counter)))) (set-counter-counter! counter result) (release-lock (counter-lock counter)) result)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Error-Handler ;; ;; Adopted from WITH-FATAL-ERROR-HANDLER, but handles everything that ;; is catchable. We must catch everything because we also want ;; exceptions (and warnings) to be catched (e.g. when the surflet is ;; loaded.) (define (with-fatal-handler* handler thunk) (call-with-current-continuation (lambda (accept) ((call-with-current-continuation (lambda (k) (with-handler (lambda (condition more) (call-with-current-continuation (lambda (decline) (k (lambda () (handler condition decline))))) (more)) ; Keep looking for a handler. (lambda () (call-with-values thunk accept))))))))) (define-syntax with-fatal-handler (syntax-rules () ((with-fatal-handler handler body ...) (with-fatal-handler* handler (lambda () body ...))))) (define (bad-gateway-error-response req path-string condition) (make-error-response (status-code bad-gateway) req (format #f "Error in surflet ~s." path-string) condition)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; DEBUGGING (define (debug fmt . args) (if *debug* (format #t "DEBUG: ~?~%" fmt args) (force-output)))