trunk merge

svn: r11691
This commit is contained in:
Stevie Strickland 2008-09-12 17:11:39 +00:00
commit a4c6d310df
18 changed files with 1821 additions and 1750 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,171 +1,170 @@
(module client mzscheme
(require openssl/mzssl "this-collection.ss")
#lang scheme/base
(provide handin-connect
handin-disconnect
retrieve-user-fields
retrieve-active-assignments
submit-assignment
retrieve-assignment
submit-addition
submit-info-change
retrieve-user-info)
(require openssl/mzssl "this-collection.ss")
(define-struct handin (r w))
(provide handin-connect
handin-disconnect
retrieve-user-fields
retrieve-active-assignments
submit-assignment
retrieve-assignment
submit-addition
submit-info-change
retrieve-user-info)
;; errors to the user: no need for a "foo: " prefix
(define (error* fmt . args)
(error (apply format fmt args)))
(define-struct handin (r w))
(define (write+flush port . xs)
(for-each (lambda (x) (write x port) (newline port)) xs)
(flush-output port))
;; errors to the user: no need for a "foo: " prefix
(define (error* fmt . args)
(error (apply format fmt args)))
(define (close-handin-ports h)
(close-input-port (handin-r h))
(close-output-port (handin-w h)))
(define (write+flush port . xs)
(for ([x xs]) (write x port) (newline port))
(flush-output port))
(define (wait-for-ok r who . reader)
(let ([v (if (pair? reader) ((car reader)) (read r))])
(unless (eq? v 'ok) (error* "~a error: ~a" who v))))
(define (close-handin-ports h)
(close-input-port (handin-r h))
(close-output-port (handin-w h)))
;; ssl connection, makes a readable error message if no connection
(define (connect-to server port)
(define pem (in-this-collection "server-cert.pem"))
(define ctx (ssl-make-client-context))
(ssl-set-verify! ctx #t)
(ssl-load-verify-root-certificates! ctx pem)
(with-handlers
([exn:fail:network?
(lambda (e)
(let* ([msg
"handin-connect: could not connect to the server (~a:~a)"]
[msg (format msg server port)]
#; ; un-comment to get the full message too
[msg (string-append msg " (" (exn-message e) ")")])
(raise (make-exn:fail:network msg (exn-continuation-marks e)))))])
(ssl-connect server port ctx)))
(define (wait-for-ok r who . reader)
(let ([v (if (pair? reader) ((car reader)) (read r))])
(unless (eq? v 'ok) (error* "~a error: ~a" who v))))
(define (handin-connect server port)
(let-values ([(r w) (connect-to server port)])
;; Sanity check: server sends "handin", first:
(let ([s (read-bytes 6 r)])
(unless (equal? #"handin" s)
(error 'handin-connect "bad handshake from server: ~e" s)))
;; Tell server protocol = 'ver1:
(write+flush w 'ver1)
;; One more sanity check: server recognizes protocol:
(let ([s (read r)])
(unless (eq? s 'ver1)
(error 'handin-connect "bad protocol from server: ~e" s)))
;; Return connection:
(make-handin r w)))
;; ssl connection, makes a readable error message if no connection
(define (connect-to server port)
(define pem (in-this-collection "server-cert.pem"))
(define ctx (ssl-make-client-context))
(ssl-set-verify! ctx #t)
(ssl-load-verify-root-certificates! ctx pem)
(with-handlers
([exn:fail:network?
(lambda (e)
(let* ([msg
"handin-connect: could not connect to the server (~a:~a)"]
[msg (format msg server port)]
#; ; un-comment to get the full message too
[msg (string-append msg " (" (exn-message e) ")")])
(raise (make-exn:fail:network msg (exn-continuation-marks e)))))])
(ssl-connect server port ctx)))
(define (handin-disconnect h)
(write+flush (handin-w h) 'bye)
(close-handin-ports h))
(define (handin-connect server port)
(let-values ([(r w) (connect-to server port)])
;; Sanity check: server sends "handin", first:
(let ([s (read-bytes 6 r)])
(unless (equal? #"handin" s)
(error 'handin-connect "bad handshake from server: ~e" s)))
;; Tell server protocol = 'ver1:
(write+flush w 'ver1)
;; One more sanity check: server recognizes protocol:
(let ([s (read r)])
(unless (eq? s 'ver1)
(error 'handin-connect "bad protocol from server: ~e" s)))
;; Return connection:
(make-handin r w)))
(define (retrieve-user-fields h)
(let ([r (handin-r h)] [w (handin-w h)])
(write+flush w 'get-user-fields 'bye)
(define (handin-disconnect h)
(write+flush (handin-w h) 'bye)
(close-handin-ports h))
(define (retrieve-user-fields h)
(let ([r (handin-r h)] [w (handin-w h)])
(write+flush w 'get-user-fields 'bye)
(let ([v (read r)])
(unless (and (list? v) (andmap string? v))
(error* "failed to get user-fields list from server"))
(wait-for-ok r "get-user-fields")
(close-handin-ports h)
v)))
(define (retrieve-active-assignments h)
(let ([r (handin-r h)] [w (handin-w h)])
(write+flush w 'get-active-assignments)
(let ([v (read r)])
(unless (and (list? v) (andmap string? v))
(error* "failed to get active-assignment list from server"))
v)))
(define (submit-assignment h username passwd assignment content
on-commit message message-final message-box)
(let ([r (handin-r h)] [w (handin-w h)])
(define (read/message)
(let ([v (read r)])
(unless (and (list? v) (andmap string? v))
(error* "failed to get user-fields list from server"))
(wait-for-ok r "get-user-fields")
(case v
[(message) (message (read r)) (read/message)]
[(message-final) (message-final (read r)) (read/message)]
[(message-box)
(write+flush w (message-box (read r) (read r))) (read/message)]
[else v])))
(write+flush w
'set 'username/s username
'set 'password passwd
'set 'assignment assignment
'save-submission)
(wait-for-ok r "login")
(write+flush w (bytes-length content))
(let ([v (read r)])
(unless (eq? v 'go) (error* "upload error: ~a" v)))
(display "$" w)
(display content w)
(flush-output w)
;; during processing, we're waiting for 'confirm, in the meanwhile, we
;; can get a 'message or 'message-box to show -- after 'message we expect
;; a string to show using the `messenge' argument, and after 'message-box
;; we expect a string and a style-list to be used with `message-box' and
;; the resulting value written back
(let ([v (read/message)])
(unless (eq? 'confirm v) (error* "submit error: ~a" v)))
(on-commit)
(write+flush w 'check)
(wait-for-ok r "commit" read/message)
(close-handin-ports h)))
(define (retrieve-assignment h username passwd assignment)
(let ([r (handin-r h)] [w (handin-w h)])
(write+flush w
'set 'username/s username
'set 'password passwd
'set 'assignment assignment
'get-submission)
(let ([len (read r)])
(unless (and (number? len) (integer? len) (positive? len))
(error* "bad response from server: ~a" len))
(let ([buf (begin (regexp-match #rx"[$]" r) (read-bytes len r))])
(wait-for-ok r "get-submission")
(close-handin-ports h)
v)))
buf))))
(define (retrieve-active-assignments h)
(let ([r (handin-r h)] [w (handin-w h)])
(write+flush w 'get-active-assignments)
(let ([v (read r)])
(unless (and (list? v) (andmap string? v))
(error* "failed to get active-assignment list from server"))
v)))
(define (submit-addition h username passwd user-fields)
(let ([r (handin-r h)] [w (handin-w h)])
(write+flush w
'set 'username/s username
'set 'password passwd
'set 'user-fields user-fields
'create-user)
(wait-for-ok r "create-user")
(close-handin-ports h)))
(define (submit-assignment h username passwd assignment content
on-commit message message-final message-box)
(let ([r (handin-r h)] [w (handin-w h)])
(define (read/message)
(let ([v (read r)])
(case v
[(message) (message (read r)) (read/message)]
[(message-final) (message-final (read r)) (read/message)]
[(message-box)
(write+flush w (message-box (read r) (read r))) (read/message)]
[else v])))
(write+flush w
'set 'username/s username
'set 'password passwd
'set 'assignment assignment
'save-submission)
(wait-for-ok r "login")
(write+flush w (bytes-length content))
(let ([v (read r)])
(unless (eq? v 'go) (error* "upload error: ~a" v)))
(display "$" w)
(display content w)
(flush-output w)
;; during processing, we're waiting for 'confirm, in the meanwhile, we
;; can get a 'message or 'message-box to show -- after 'message we expect
;; a string to show using the `messenge' argument, and after 'message-box
;; we expect a string and a style-list to be used with `message-box' and
;; the resulting value written back
(let ([v (read/message)])
(unless (eq? 'confirm v) (error* "submit error: ~a" v)))
(on-commit)
(write+flush w 'check)
(wait-for-ok r "commit" read/message)
(close-handin-ports h)))
(define (submit-info-change h username old-passwd new-passwd user-fields)
(let ([r (handin-r h)]
[w (handin-w h)])
(write+flush w
'set 'username/s username
'set 'password old-passwd
'set 'new-password new-passwd
'set 'user-fields user-fields
'change-user-info)
(wait-for-ok r "change-user-info")
(close-handin-ports h)))
(define (retrieve-assignment h username passwd assignment)
(let ([r (handin-r h)] [w (handin-w h)])
(write+flush w
'set 'username/s username
'set 'password passwd
'set 'assignment assignment
'get-submission)
(let ([len (read r)])
(unless (and (number? len) (integer? len) (positive? len))
(error* "bad response from server: ~a" len))
(let ([buf (begin (regexp-match #rx"[$]" r) (read-bytes len r))])
(wait-for-ok r "get-submission")
(close-handin-ports h)
buf))))
(define (submit-addition h username passwd user-fields)
(let ([r (handin-r h)] [w (handin-w h)])
(write+flush w
'set 'username/s username
'set 'password passwd
'set 'user-fields user-fields
'create-user)
(wait-for-ok r "create-user")
(close-handin-ports h)))
(define (submit-info-change h username old-passwd new-passwd user-fields)
(let ([r (handin-r h)]
[w (handin-w h)])
(write+flush w
'set 'username/s username
'set 'password old-passwd
'set 'new-password new-passwd
'set 'user-fields user-fields
'change-user-info)
(wait-for-ok r "change-user-info")
(close-handin-ports h)))
(define (retrieve-user-info h username passwd)
(let ([r (handin-r h)] [w (handin-w h)])
(write+flush w
'set 'username/s username
'set 'password passwd
'get-user-info 'bye)
(let ([v (read r)])
(unless (and (list? v) (andmap string? v))
(error* "failed to get user-info list from server"))
(wait-for-ok r "get-user-info")
(close-handin-ports h)
v)))
)
(define (retrieve-user-info h username passwd)
(let ([r (handin-r h)] [w (handin-w h)])
(write+flush w
'set 'username/s username
'set 'password passwd
'get-user-info 'bye)
(let ([v (read r)])
(unless (and (list? v) (andmap string? v))
(error* "failed to get user-info list from server"))
(wait-for-ok r "get-user-info")
(close-handin-ports h)
v)))

View File

@ -1,282 +1,276 @@
(module handin-multi mzscheme
(require mzlib/class mzlib/list mzlib/string mzlib/port
mred framework
browser/external
"info.ss" "client-gui.ss" "this-collection.ss")
#lang scheme/base
(define handin-name (#%info-lookup 'name))
(define web-address (#%info-lookup 'web-address
(lambda () "http://www.plt-scheme.org")))
(define selection-mode (#%info-lookup 'selection-mode (lambda () 'extended)))
(define selection-defaults
(let ([sd (#%info-lookup 'selection-default (lambda () '("*.scm" "*.ss")))])
(if (string? sd) (list sd) sd)))
(define last-dir-key (make-my-key 'multifile:last-dir))
(preferences:set-default last-dir-key "" string?)
(define last-auto-key (make-my-key 'multifile:last-auto))
(preferences:set-default last-auto-key (car selection-defaults) string?)
(define geometry-key (make-my-key 'multifile:geometry))
(preferences:set-default geometry-key #f void)
(require scheme/class scheme/port mred framework browser/external
"info.ss" "client-gui.ss" "this-collection.ss")
(define update
(and (#%info-lookup 'enable-auto-update (lambda () #f))
(dynamic-require `(lib "updater.ss" ,this-collection-name) 'update)))
(define handin-name (#%info-lookup 'name))
(define web-address (#%info-lookup 'web-address
(lambda () "http://www.plt-scheme.org")))
(define selection-mode (#%info-lookup 'selection-mode (lambda () 'extended)))
(define selection-defaults
(let ([sd (#%info-lookup 'selection-default (lambda () '("*.scm" "*.ss")))])
(if (string? sd) (list sd) sd)))
(define last-dir-key (make-my-key 'multifile:last-dir))
(preferences:set-default last-dir-key "" string?)
(define last-auto-key (make-my-key 'multifile:last-auto))
(preferences:set-default last-auto-key (car selection-defaults) string?)
(define geometry-key (make-my-key 'multifile:geometry))
(preferences:set-default geometry-key #f void)
;; ==========================================================================
(define magic #"<<<MULTI-SUBMISSION-FILE>>>")
(define (pack-files files)
(let/ec return
(parameterize ([current-output-port (open-output-bytes)])
(printf "~a\n" magic)
(for-each
(lambda (file)
(let ([size (and (file-exists? file) (file-size file))])
(unless size (return #f))
(let ([buf (with-input-from-file file
(lambda () (read-bytes size)))])
(unless (equal? size (bytes-length buf)) (return #f))
(write (list file buf)) (newline))))
files)
(flush-output)
(get-output-bytes (current-output-port)))))
(define ((unpack-files parent) buf)
(let/ec return
(define (error* msg)
(message-box "Retrieve Error" msg parent)
(return #f))
(parameterize ([current-input-port (open-input-bytes buf)])
(unless (equal? magic (read-bytes (bytes-length magic)))
(define update
(and (#%info-lookup 'enable-auto-update (lambda () #f))
(dynamic-require `(lib "updater.ss" ,this-collection-name) 'update)))
;; ==========================================================================
(define magic #"<<<MULTI-SUBMISSION-FILE>>>")
(define (pack-files files)
(let/ec return
(parameterize ([current-output-port (open-output-bytes)])
(printf "~a\n" magic)
(for ([file files])
(let ([size (and (file-exists? file) (file-size file))])
(unless size (return #f))
(let ([buf (with-input-from-file file
(lambda () (read-bytes size)))])
(unless (equal? size (bytes-length buf)) (return #f))
(write (list file buf)) (newline))))
(flush-output)
(get-output-bytes (current-output-port)))))
(define ((unpack-files parent) buf)
(let/ec return
(define (error* msg)
(message-box "Retrieve Error" msg parent)
(return #f))
(parameterize ([current-input-port (open-input-bytes buf)])
(unless (equal? magic (read-bytes (bytes-length magic)))
(error* "Error in retrieved content: bad format"))
(let ([files
(let loop ([files '()])
(let ([f (with-handlers ([void void]) (read))])
(if (eof-object? f)
(reverse files) (loop (cons f files)))))]
[overwrite-all? #f])
(define (write? file)
(define (del)
;; check if exists: users might rename files during questions
(when (file-exists? file) (delete-file file)))
(cond [(not (file-exists? file)) #t]
[overwrite-all? (del) #t]
[else (case (message-box/custom
"Retrieve"
(format "~s already exists, overwrite?" file)
"&Yes" "&No" "Yes to &All" parent
'(default=2 caution) 4)
[(1) (del) #t]
[(2) #f]
[(3) (set! overwrite-all? #t) (del) #t]
[(4) (error* "Aborting...")])]))
(unless (and (list? files)
(andmap (lambda (x)
(and (list? x) (= 2 (length x))
(string? (car x)) (bytes? (cadr x))))
files))
(error* "Error in retrieved content: bad format"))
(let ([files
(let loop ([files '()])
(let ([f (with-handlers ([void void]) (read))])
(if (eof-object? f)
(reverse files) (loop (cons f files)))))]
[overwrite-all? #f])
(define (write? file)
(define (del)
;; check if exists: users might rename files during questions
(when (file-exists? file) (delete-file file)))
(cond [(not (file-exists? file)) #t]
[overwrite-all? (del) #t]
[else (case (message-box/custom
"Retrieve"
(format "~s already exists, overwrite?" file)
"&Yes" "&No" "Yes to &All" parent
'(default=2 caution) 4)
[(1) (del) #t]
[(2) #f]
[(3) (set! overwrite-all? #t) (del) #t]
[(4) (error* "Aborting...")])]))
(unless (and (list? files)
(andmap (lambda (x)
(and (list? x) (= 2 (length x))
(string? (car x)) (bytes? (cadr x))))
files))
(error* "Error in retrieved content: bad format"))
(for-each (lambda (file)
(let ([file (car file)] [buf (cadr file)])
(when (write? file)
(with-output-to-file file
(lambda () (display buf) (flush-output))))))
files)
(message-box "Retrieve" "Retrieval done" parent)))))
(for ([file files])
(let ([file (car file)] [buf (cadr file)])
(when (write? file)
(with-output-to-file file
(lambda () (display buf) (flush-output))))))
(message-box "Retrieve" "Retrieval done" parent)))))
;; ==========================================================================
(define multifile-dialog%
(class frame%
;; ----------------------------------------------------------------------
(let ([g (preferences:get geometry-key)])
(super-new [label (format "~a Handin" handin-name)]
[stretchable-width #t] [stretchable-height #t]
[width (and g (car g))] [height (and g (cadr g))]
[x (and g (caddr g))] [y (and g (cadddr g))]))
(define main-pane (new horizontal-pane% [parent this]))
(define buttons-pane
(new vertical-pane% [parent main-pane] [stretchable-width #f]))
(define files-pane
(new vertical-pane% [parent main-pane]))
;; ==========================================================================
(define multifile-dialog%
(class frame%
;; ----------------------------------------------------------------------
(let ([g (preferences:get geometry-key)])
(super-new [label (format "~a Handin" handin-name)]
[stretchable-width #t] [stretchable-height #t]
[width (and g (car g))] [height (and g (cadr g))]
[x (and g (caddr g))] [y (and g (cadddr g))]))
(define main-pane (new horizontal-pane% [parent this]))
(define buttons-pane
(new vertical-pane% [parent main-pane] [stretchable-width #f]))
(define files-pane
(new vertical-pane% [parent main-pane]))
;; ----------------------------------------------------------------------
(define (close)
(preferences:set geometry-key
(list (send this get-width) (send this get-height)
(send this get-x) (send this get-y)))
;; (preferences:save)
(send this show #f))
(define/augment (on-close) (close))
;; ----------------------------------------------------------------------
(define (close)
(preferences:set geometry-key
(list (send this get-width) (send this get-height)
(send this get-x) (send this get-y)))
;; (preferences:save)
(send this show #f))
(define/augment (on-close) (close))
;; ----------------------------------------------------------------------
(new button% [parent buttons-pane]
[label (make-object bitmap% (in-this-collection "icon.png"))]
[callback (lambda _ (send-url web-address))])
(new pane% [parent buttons-pane])
(let ([button (lambda (label callback)
(new button% [label label] [parent buttons-pane]
[stretchable-width #t] [callback callback]))])
(button "&Submit" (lambda _ (do-submit)))
(button "&Retrieve" (lambda _ (do-retrieve)))
(button "A&ccount" (lambda _ (new manage-handin-dialog% [parent this])))
(when update (button "&Update" (lambda _ (update this #t))))
(button "C&lose" (lambda _ (close))))
;; ----------------------------------------------------------------------
(new button% [parent buttons-pane]
[label (make-object bitmap% (in-this-collection "icon.png"))]
[callback (lambda _ (send-url web-address))])
(new pane% [parent buttons-pane])
(let ([button (lambda (label callback)
(new button% [label label] [parent buttons-pane]
[stretchable-width #t] [callback callback]))])
(button "&Submit" (lambda _ (do-submit)))
(button "&Retrieve" (lambda _ (do-retrieve)))
(button "A&ccount" (lambda _ (new manage-handin-dialog% [parent this])))
(when update (button "&Update" (lambda _ (update this #t))))
(button "C&lose" (lambda _ (close))))
;; ----------------------------------------------------------------------
(define files-list
(new list-box% [label "&Files:"] [parent files-pane]
[style `(,selection-mode vertical-label)] [enabled #f]
[choices '("Drag something here," "or click below")]
[min-height 100] [stretchable-width #t] [stretchable-height #t]))
(define auto-select
(new combo-field% [label "&Auto:"] [parent files-pane]
[init-value (preferences:get last-auto-key)]
[choices selection-defaults]
[callback (lambda (t e)
(when (eq? (send e get-event-type) 'text-field-enter)
(preferences:set last-auto-key (send t get-value))
(do-selections '() '())))]))
(define directory-pane
(new horizontal-pane% [parent files-pane]
[stretchable-width #t] [stretchable-height #f]))
(define choose-dir-button
(new button% [label "&Directory:"] [parent directory-pane]
[callback (lambda _ (choose-dir))]))
(define current-working-directory
(new text-field% [label #f] [parent directory-pane] [init-value ""]
[callback (lambda (t e)
(when (eq? (send e get-event-type) 'text-field-enter)
(set-dir (send t get-value))
(send t focus)))]))
;; ----------------------------------------------------------------------
(define files-list
(new list-box% [label "&Files:"] [parent files-pane]
[style `(,selection-mode vertical-label)] [enabled #f]
[choices '("Drag something here," "or click below")]
[min-height 100] [stretchable-width #t] [stretchable-height #t]))
(define auto-select
(new combo-field% [label "&Auto:"] [parent files-pane]
[init-value (preferences:get last-auto-key)]
[choices selection-defaults]
[callback (lambda (t e)
(when (eq? (send e get-event-type) 'text-field-enter)
(preferences:set last-auto-key (send t get-value))
(do-selections '() '())))]))
(define directory-pane
(new horizontal-pane% [parent files-pane]
[stretchable-width #t] [stretchable-height #f]))
(define choose-dir-button
(new button% [label "&Directory:"] [parent directory-pane]
[callback (lambda _ (choose-dir))]))
(define current-working-directory
(new text-field% [label #f] [parent directory-pane] [init-value ""]
[callback (lambda (t e)
(when (eq? (send e get-event-type) 'text-field-enter)
(set-dir (send t get-value))
(send t focus)))]))
(let ([ldir (preferences:get last-dir-key)])
;; don't use init-value since it can get very long
(send current-working-directory set-value ldir)
(unless (equal? "" ldir) (current-directory ldir)))
;; ----------------------------------------------------------------------
(define dir-selected? #f)
(define (->string x)
(cond [(string? x) x]
[(path? x) (path->string x)]
[(bytes? x) (bytes->string/utf-8 x)]
[(symbol? x) (symbol->string x)]
[else (error '->string "bad input: ~e" x)]))
(define (get-selected+unselected)
(if (send files-list is-enabled?)
(let ([selected (send files-list get-selections)])
(let loop ([i (sub1 (send files-list get-number))] [s '()] [u '()])
(if (<= 0 i)
(let ([f (send files-list get-string i)])
(if (memq i selected)
(loop (sub1 i) (cons f s) u)
(loop (sub1 i) s (cons f u))))
(list (reverse s) (reverse u)))))
'(() ())))
(define (set-dir dir)
(let* ([dir (and dir (->string dir))]
[dir (and dir (not (equal? "" dir)) (directory-exists? dir)
(->string (simplify-path (path->complete-path
(build-path dir 'same)))))]
[sel+unsel (if (equal? dir (->string (current-directory)))
(get-selected+unselected) '(() ()))])
(when dir
(current-directory dir)
(set! dir-selected? #t)
(let ([t current-working-directory])
(send t set-value dir)
(send (send t get-editor) select-all))
(preferences:set last-dir-key dir)
(send files-list set
(sort (map ->string (filter file-exists? (directory-list)))
string<?))
(if (< 0 (send files-list get-number))
(begin (apply do-selections sel+unsel)
(send files-list enable #t)
(send files-list focus))
(begin (send files-list append "no files")
(send files-list enable #f))))))
(define (choose-dir)
(let ([ldir (preferences:get last-dir-key)])
;; don't use init-value since it can get very long
(send current-working-directory set-value ldir)
(unless (equal? "" ldir) (current-directory ldir)))
(set-dir
(get-directory "Choose a directory with files to submit" this
(and (not (equal? ldir "")) ldir)))))
(define (refresh-dir)
(when dir-selected? (set-dir (current-directory))))
(define auto-glob+regexp '(#f #f))
(define (globs->regexps glob)
(if (equal? (car auto-glob+regexp) glob)
(cdr auto-glob+regexp)
(let* ([regexps
(map (lambda (glob)
(let* ([re (regexp-replace* #rx"[.]" glob "\\\\.")]
[re (regexp-replace* #rx"[?]" re ".")]
[re (regexp-replace* #rx"[*]" re ".*")]
[re (string-append "^" re "$")]
[re (with-handlers ([void (lambda _ #f)])
(regexp re))])
re))
(regexp-split ";" glob))]
[regexps (filter values regexps)]
[regexps (if (pair? regexps)
(lambda (file)
(ormap (lambda (re) (regexp-match re file))
regexps))
(lambda (_) #f))])
(set! auto-glob+regexp (cons glob regexps))
regexps)))
(define (do-selections selected unselected)
(define glob (send auto-select get-value))
(define regexps (globs->regexps glob))
(let loop ([n (sub1 (send files-list get-number))])
(when (<= 0 n)
(let ([file (send files-list get-string n)])
(send files-list select n
(cond [(member file selected) #t]
[(member file unselected) #f]
[else (regexps file)]))
(loop (sub1 n)))))
(send (if (send files-list is-enabled?) files-list choose-dir-button)
focus))
;; ----------------------------------------------------------------------
(define dir-selected? #f)
(define (->string x)
(cond [(string? x) x]
[(path? x) (path->string x)]
[(bytes? x) (bytes->string/utf-8 x)]
[(symbol? x) (symbol->string x)]
[else (error '->string "bad input: ~e" x)]))
(define (get-selected+unselected)
(if (send files-list is-enabled?)
(let ([selected (send files-list get-selections)])
(let loop ([i (sub1 (send files-list get-number))] [s '()] [u '()])
(if (<= 0 i)
(let ([f (send files-list get-string i)])
(if (memq i selected)
(loop (sub1 i) (cons f s) u)
(loop (sub1 i) s (cons f u))))
(list (reverse s) (reverse u)))))
'(() ())))
(define (set-dir dir)
(let* ([dir (and dir (->string dir))]
[dir (and dir (not (equal? "" dir)) (directory-exists? dir)
(->string (simplify-path (path->complete-path
(build-path dir 'same)))))]
[sel+unsel (if (equal? dir (->string (current-directory)))
(get-selected+unselected) '(() ()))])
(when dir
(current-directory dir)
(set! dir-selected? #t)
(let ([t current-working-directory])
(send t set-value dir)
(send (send t get-editor) select-all))
(preferences:set last-dir-key dir)
(send files-list set
(sort (map ->string (filter file-exists? (directory-list)))
string<?))
(if (< 0 (send files-list get-number))
(begin (apply do-selections sel+unsel)
(send files-list enable #t)
(send files-list focus))
(begin (send files-list append "no files")
(send files-list enable #f))))))
(define (choose-dir)
(let ([ldir (preferences:get last-dir-key)])
(set-dir
(get-directory "Choose a directory with files to submit" this
(and (not (equal? ldir "")) ldir)))))
(define (refresh-dir)
(when dir-selected? (set-dir (current-directory))))
(define auto-glob+regexp '(#f #f))
(define (globs->regexps glob)
(if (equal? (car auto-glob+regexp) glob)
(cdr auto-glob+regexp)
(let* ([regexps
(map (lambda (glob)
(let* ([re (regexp-replace* #rx"[.]" glob "\\\\.")]
[re (regexp-replace* #rx"[?]" re ".")]
[re (regexp-replace* #rx"[*]" re ".*")]
[re (string-append "^" re "$")]
[re (with-handlers ([void (lambda _ #f)])
(regexp re))])
re))
(regexp-split ";" glob))]
[regexps (filter values regexps)]
[regexps (if (pair? regexps)
(lambda (file)
(ormap (lambda (re) (regexp-match re file))
regexps))
(lambda (_) #f))])
(set! auto-glob+regexp (cons glob regexps))
regexps)))
(define (do-selections selected unselected)
(define glob (send auto-select get-value))
(define regexps (globs->regexps glob))
(let loop ([n (sub1 (send files-list get-number))])
(when (<= 0 n)
(let ([file (send files-list get-string n)])
(send files-list select n
(cond [(member file selected) #t]
[(member file unselected) #f]
[else (regexps file)]))
(loop (sub1 n)))))
(send (if (send files-list is-enabled?) files-list choose-dir-button)
focus))
;; ----------------------------------------------------------------------
(define/override (on-drop-file path)
(cond [(directory-exists? path) (set-dir path)]
[(file-exists? path)
(let-values ([(dir name dir?) (split-path path)])
(set-dir dir)
(cond [(send files-list find-string (->string name))
=> (lambda (i) (send files-list select i #t))]))]))
(define/override (on-subwindow-char w e)
(define (next) (super on-subwindow-char w e))
(case (send e get-key-code)
[(escape) (close)]
[(f5) (refresh-dir)]
;; [(#\space) (if (eq? w files-list)
;; (printf ">>> ~s\n" (send files-list get-selection))
;; (next))]
[else (next)]))
;; ----------------------------------------------------------------------
(define/override (on-drop-file path)
(cond [(directory-exists? path) (set-dir path)]
[(file-exists? path)
(let-values ([(dir name dir?) (split-path path)])
(set-dir dir)
(cond [(send files-list find-string (->string name))
=> (lambda (i) (send files-list select i #t))]))]))
(define/override (on-subwindow-char w e)
(define (next) (super on-subwindow-char w e))
(case (send e get-key-code)
[(escape) (close)]
[(f5) (refresh-dir)]
;; [(#\space) (if (eq? w files-list)
;; (printf ">>> ~s\n" (send files-list get-selection))
;; (next))]
[else (next)]))
;; ----------------------------------------------------------------------
(define (do-submit)
(let ([files (car (get-selected+unselected))])
(if (pair? files)
(let ([content (pack-files files)])
(if content
(new handin-frame% [parent this] [on-retrieve #f]
[content content])
(message-box "Handin" "Error when packing files" this)))
(message-box "Handin" "No files" this))))
(define (do-retrieve)
(if dir-selected?
(new handin-frame% [parent this] [content #f]
[on-retrieve (unpack-files this)])
(message-box "Handin" "No directory selected" this)))
;; ----------------------------------------------------------------------
(define (do-submit)
(let ([files (car (get-selected+unselected))])
(if (pair? files)
(let ([content (pack-files files)])
(if content
(new handin-frame% [parent this] [on-retrieve #f]
[content content])
(message-box "Handin" "Error when packing files" this)))
(message-box "Handin" "No files" this))))
(define (do-retrieve)
(if dir-selected?
(new handin-frame% [parent this] [content #f]
[on-retrieve (unpack-files this)])
(message-box "Handin" "No directory selected" this)))
;; ----------------------------------------------------------------------
(send this accept-drop-files #t)
(send choose-dir-button focus)
(send this show #t)
(when update (update this))))
;; ----------------------------------------------------------------------
(send this accept-drop-files #t)
(send choose-dir-button focus)
(send this show #t)
(when update (update this))))
(provide multifile-handin)
(define (multifile-handin) (new multifile-dialog%))
)
(provide multifile-handin)
(define (multifile-handin) (new multifile-dialog%))

View File

@ -1,34 +1,34 @@
(module this-collection mzscheme
#lang scheme/base
(define-syntax (this-name-stx stx)
(let* ([p (syntax-source stx)]
[dir (and (path? p) (let-values ([(b _1 _2) (split-path p)]) b))]
[name (and (path? dir)
(bytes->string/locale
(path-element->bytes
(let-values ([(_1 p _2) (split-path dir)]) p))))])
;; check that we are installed as a top-level collection (this is needed
;; because there are some code bits (that depend on bindings from this
;; file) that expect this to be true)
(with-handlers
([void (lambda (e)
(raise
(make-exn:fail
"*** Error: this collection must be a top-level collection"
(exn-continuation-marks e))))])
(collection-path name))
(datum->syntax-object stx name stx)))
(require (for-syntax scheme/base))
(provide this-collection-name)
(define this-collection-name this-name-stx)
(define-syntax (this-name-stx stx)
(let* ([p (syntax-source stx)]
[dir (and (path? p) (let-values ([(b _1 _2) (split-path p)]) b))]
[name (and (path? dir)
(bytes->string/locale
(path-element->bytes
(let-values ([(_1 p _2) (split-path dir)]) p))))])
;; check that we are installed as a top-level collection (this is needed
;; because there are some code bits (that depend on bindings from this
;; file) that expect this to be true)
(with-handlers
([void (lambda (e)
(raise
(make-exn:fail
"*** Error: this collection must be a top-level collection"
(exn-continuation-marks e))))])
(collection-path name))
(datum->syntax stx name stx)))
(define this-collection-path (collection-path this-collection-name))
(provide in-this-collection)
(define (in-this-collection . paths)
(apply build-path this-collection-path paths))
(provide this-collection-name)
(define this-collection-name this-name-stx)
(provide make-my-key)
(define (make-my-key sym)
(string->symbol (format "handin:~a:~a" this-collection-name sym)))
(define this-collection-path (collection-path this-collection-name))
(provide in-this-collection)
(define (in-this-collection . paths)
(apply build-path this-collection-path paths))
)
(provide make-my-key)
(define (make-my-key sym)
(string->symbol (format "handin:~a:~a" this-collection-name sym)))

View File

@ -1,71 +1,72 @@
(module updater mzscheme
(require mzlib/file mzlib/port net/url setup/plt-installer mred framework
"info.ss" "this-collection.ss")
(define name (#%info-lookup 'name))
(define web-address (#%info-lookup 'web-address))
(define version-filename (#%info-lookup 'version-filename))
(define package-filename (#%info-lookup 'package-filename))
(define dialog-title (string-append name " Updater"))
(define (file->inport filename)
(get-pure-port
(string->url
(string-append (regexp-replace #rx"/?$" web-address "/") filename))))
(define update-key (make-my-key 'update-check))
(preferences:set-default update-key #t boolean?)
#lang scheme/base
(require scheme/file scheme/port net/url setup/plt-installer mred framework
"info.ss" "this-collection.ss")
(define (update!)
(let* ([in (file->inport package-filename)]
[outf (make-temporary-file "tmp~a.plt")]
[out (open-output-file outf 'binary 'truncate)])
(dynamic-wind void
(lambda () (copy-port in out))
(lambda () (close-input-port in) (close-output-port out)))
(run-installer outf (lambda () (delete-file outf)))))
(define (maybe-update parent new-version)
(define response
(message-box/custom
dialog-title
(string-append
"A new version of the "name" plugin is available: "
(let ([v (format "~a" new-version)])
(if (= 12 (string-length v))
(apply format "~a~a~a~a/~a~a/~a~a ~a~a:~a~a" (string->list v))
v)))
"&Update now" "Remind Me &Later"
;; may be disabled, but explicitly invoked through menu item
(if (preferences:get update-key)
"&Stop Checking" "Update and &Always Check")
parent '(default=1 caution) 2))
(case response
[(1) (update!)]
[(2) 'ok] ; do nothing
[(3) (preferences:set update-key (not (preferences:get update-key)))
(when (preferences:get update-key) (update!))]
[else (error 'update "internal error in ~a plugin updater" name)]))
(provide update)
(define (update parent . show-ok?)
(let* ([web-version
(with-handlers ([void (lambda _ 0)])
(let ([in (file->inport version-filename)])
(dynamic-wind void
(lambda () (read in))
(lambda () (close-input-port in)))))]
;; if the file was not there, we might have read some junk
[web-version (if (integer? web-version) web-version 0)]
[current-version
(with-input-from-file (in-this-collection "version") read)])
(cond [(> web-version current-version) (maybe-update parent web-version)]
[(and (pair? show-ok?) (car show-ok?))
(message-box dialog-title "Your plugin is up-to-date" parent)])))
(define name (#%info-lookup 'name))
(define web-address (#%info-lookup 'web-address))
(define version-filename (#%info-lookup 'version-filename))
(define package-filename (#%info-lookup 'package-filename))
(define dialog-title (string-append name " Updater"))
(define (file->inport filename)
(get-pure-port
(string->url
(string-append (regexp-replace #rx"/?$" web-address "/") filename))))
(define update-key (make-my-key 'update-check))
(preferences:set-default update-key #t boolean?)
(define (wait-for-top-level-windows)
;; wait until the definitions are instantiated, return top-level window
(let ([ws (get-top-level-windows)])
(if (null? ws) (begin (sleep 1) (wait-for-top-level-windows)) (car ws))))
(provide bg-update)
(define (bg-update)
(thread (lambda ()
(when (preferences:get update-key)
(update (wait-for-top-level-windows))))))
(define (update!)
(let* ([in (file->inport package-filename)]
[outf (make-temporary-file "tmp~a.plt")]
[out (open-output-file outf 'binary 'truncate)])
(dynamic-wind
void
(lambda () (copy-port in out))
(lambda () (close-input-port in) (close-output-port out)))
(run-installer outf (lambda () (delete-file outf)))))
(define (maybe-update parent new-version)
(define response
(message-box/custom
dialog-title
(string-append
"A new version of the "name" plugin is available: "
(let ([v (format "~a" new-version)])
(if (= 12 (string-length v))
(apply format "~a~a~a~a/~a~a/~a~a ~a~a:~a~a" (string->list v))
v)))
"&Update now" "Remind Me &Later"
;; may be disabled, but explicitly invoked through menu item
(if (preferences:get update-key)
"&Stop Checking" "Update and &Always Check")
parent '(default=1 caution) 2))
(case response
[(1) (update!)]
[(2) 'ok] ; do nothing
[(3) (preferences:set update-key (not (preferences:get update-key)))
(when (preferences:get update-key) (update!))]
[else (error 'update "internal error in ~a plugin updater" name)]))
(provide update)
(define (update parent . show-ok?)
(let* ([web-version
(with-handlers ([void (lambda _ 0)])
(let ([in (file->inport version-filename)])
(dynamic-wind
void
(lambda () (read in))
(lambda () (close-input-port in)))))]
;; if the file was not there, we might have read some junk
[web-version (if (integer? web-version) web-version 0)]
[current-version
(with-input-from-file (in-this-collection "version") read)])
(cond [(> web-version current-version) (maybe-update parent web-version)]
[(and (pair? show-ok?) (car show-ok?))
(message-box dialog-title "Your plugin is up-to-date" parent)])))
)
(define (wait-for-top-level-windows)
;; wait until the definitions are instantiated, return top-level window
(let ([ws (get-top-level-windows)])
(if (null? ws) (begin (sleep 1) (wait-for-top-level-windows)) (car ws))))
(provide bg-update)
(define (bg-update)
(thread (lambda ()
(when (preferences:get update-key)
(update (wait-for-top-level-windows))))))

View File

@ -191,10 +191,8 @@
;; This code will hack textualization of text boxes
(define (insert-to-editor editor . xs)
(for-each (lambda (x)
(send editor insert
(if (string? x) x (make-object editor-snip% x))))
xs))
(for ([x xs])
(send editor insert (if (string? x) x (make-object editor-snip% x)))))
;; support for "text-box%"
(define text-box-sc
@ -284,10 +282,9 @@
'(ok-cancel caution)))))
(error* "Aborting...")))
;; This will create copies of the original files
;; (for-each (lambda (file)
;; (with-output-to-file (car file)
;; (lambda () (display (cadr file)) (flush-output))))
;; files)
;; (for ([file files])
;; (with-output-to-file (car file)
;; (lambda () (display (cadr file)) (flush-output))))
(let* ([pfx-len (string-length markup-prefix)]
[line-len (- maxwidth pfx-len)]
[=s (lambda (n) (if (<= 0 n) (make-string n #\=) ""))]
@ -301,14 +298,12 @@
(display ===)
(newline))
(parameterize ([current-output-port (open-output-bytes)])
(for-each (lambda (file)
(sep (car file))
(parameterize ([current-input-port
(open-input-bytes (cadr file))]
[current-processed-file (car file)])
(input->process->output
maxwidth textualize? untabify? prefix-re)))
files)
(for ([file files])
(sep (car file))
(parameterize ([current-input-port (open-input-bytes (cadr file))]
[current-processed-file (car file)])
(input->process->output
maxwidth textualize? untabify? prefix-re)))
(get-output-bytes (current-output-port))))))
;; ============================================================================
@ -394,10 +389,9 @@
[user-post (id 'user-post)]
[(body ...) (syntax-case #'(body ...) ()
[() #'(void)] [_ #'(body ...)])])
(for-each (lambda (x)
(unless (memq (car x) got)
(raise-syntax-error #f "unknown keyword" stx (cadr x))))
keyvals)
(for ([x keyvals])
(unless (memq (car x) got)
(raise-syntax-error #f "unknown keyword" stx (cadr x))))
#'(begin
(provide checker)
(define checker
@ -476,10 +470,8 @@
(set-run-status "creating text file")
(with-output-to-file text-file #:exists 'truncate
(lambda ()
(for-each (lambda (user)
(prefix-line
(user-substs user student-line)))
users)
(for ([user users])
(prefix-line (user-substs user student-line)))
(for-each prefix-line/substs extra-lines)
(for-each prefix-line/substs
(or (thread-cell-ref added-lines) '()))

View File

@ -24,7 +24,7 @@
(error (apply format fmt args)))
(define (write+flush port . xs)
(for-each (lambda (x) (write x port) (newline port)) xs)
(for ([x xs]) (write x port) (newline port))
(flush-output port))
(define-struct alist (name [l #:mutable]))
@ -87,20 +87,18 @@
;; SUCCESS, or things that are newer in the main submission
;; directory are kept (but subdirs in SUCCESS will are copied as
;; is))
(for-each
(lambda (f)
(define dir/f (build-path dir f))
(cond [(not (or (file-exists? f) (directory-exists? f)))
;; f is in dir but not in the working directory
(copy-directory/files dir/f f)]
[(or (<= (file-or-directory-modify-seconds f)
(file-or-directory-modify-seconds dir/f))
(and (file-exists? f) (file-exists? dir/f)
(not (= (file-size f) (file-size dir/f)))))
;; f is newer in dir than in the working directory
(delete-directory/files f)
(copy-directory/files dir/f f)]))
(directory-list dir)))))
(for ([f (directory-list dir)])
(define dir/f (build-path dir f))
(cond [(not (or (file-exists? f) (directory-exists? f)))
;; f is in dir but not in the working directory
(copy-directory/files dir/f f)]
[(or (<= (file-or-directory-modify-seconds f)
(file-or-directory-modify-seconds dir/f))
(and (file-exists? f) (file-exists? dir/f)
(not (= (file-size f) (file-size dir/f)))))
;; f is newer in dir than in the working directory
(delete-directory/files f)
(copy-directory/files dir/f f)])))))
(define cleanup-sema (make-semaphore 1))
(define (cleanup-submission dir)
@ -118,14 +116,12 @@
(define (cleanup-all-submissions)
(log-line "Cleaning up all submission directories")
(for-each (lambda (pset)
(when (directory-exists? pset) ; just in case
(parameterize ([current-directory pset])
(for-each (lambda (sub)
(when (directory-exists? sub) ; filter non-dirs
(cleanup-submission sub)))
(directory-list)))))
(get-conf 'all-dirs)))
(for ([pset (get-conf 'all-dirs)]
#:when (directory-exists? pset)) ; just in case
(parameterize ([current-directory pset])
(for ([sub (directory-list)]
#:when (directory-exists? sub)) ; filter non-dirs
(cleanup-submission sub)))))
;; On startup, we scan all submissions, then repeat at random intervals (only
;; if clients connected in that time), and check often for changes in the
@ -193,15 +189,11 @@
;; we have a submission, need to create a directory if needed, make
;; sure that no users submitted work with someone else
(unless (directory-exists? dirname)
(for-each
(lambda (dir)
(for-each
(lambda (d)
(when (member d users)
(error* "bad submission: ~a has an existing submission (~a)"
d dir)))
(regexp-split #rx" *[+] *" (path->string dir))))
(directory-list))
(for* ([dir (directory-list)]
[d (regexp-split #rx" *[+] *" (path->string dir))])
(when (member d users)
(error* "bad submission: ~a has an existing submission (~a)"
d dir)))
(make-directory dirname))
(parameterize ([current-directory dirname]
[current-messenger
@ -378,9 +370,9 @@
(error* "the username \"checker.ss\" is reserved"))
(when (get-user-data username)
(error* "username already exists: `~a'" username))
(for-each (lambda (str info)
(check-field str (cadr info) (car info) (caddr info)))
extra-fields (get-conf 'extra-fields))
(for ([str extra-fields]
[info (get-conf 'extra-fields)])
(check-field str (cadr info) (car info) (caddr info)))
(wait-for-lock "+newuser+")
(log-line "create user: ~a" username)
(hook 'user-create `([username ,username] [fields ,extra-fields]))
@ -405,9 +397,9 @@
(error* "changing information not allowed: ~a" username))
(when (equal? new-data old-data)
(error* "no fields changed: ~a" username))
(for-each (lambda (str info)
(check-field str (cadr info) (car info) (caddr info)))
(cdr new-data) (get-conf 'extra-fields))
(for ([str (cdr new-data)]
[info (get-conf 'extra-fields)])
(check-field str (cadr info) (car info) (caddr info)))
(log-line "change info for ~a ~s -> ~s" username old-data new-data)
(unless (equal? (cdr new-data) (cdr old-data)) ; not for password change
(hook 'user-change `([username ,username]

View File

@ -1,111 +1,110 @@
(module config mzscheme
(require mzlib/file mzlib/list)
#lang scheme/base
;; This module should be invoked when we're in the server directory
(provide server-dir)
(define server-dir (or (getenv "PLT_HANDINSERVER_DIR") (current-directory)))
(require scheme/file)
(define config-file (path->complete-path "config.ss" server-dir))
;; This module should be invoked when we're in the server directory
(provide server-dir)
(define server-dir (or (getenv "PLT_HANDINSERVER_DIR") (current-directory)))
(define poll-freq 2000.0) ; poll at most once every two seconds
(define config-file (path->complete-path "config.ss" server-dir))
(define last-poll #f)
(define last-filetime #f)
(define raw-config #f)
(define config-cache #f)
(define poll-freq 2000.0) ; poll at most once every two seconds
(provide get-conf)
(define (get-conf key)
(unless (and raw-config
(< (- (current-inexact-milliseconds) last-poll) poll-freq))
(set! last-poll (current-inexact-milliseconds))
(let ([filetime (file-or-directory-modify-seconds config-file)])
(unless (and filetime (equal? filetime last-filetime))
(set! last-filetime filetime)
(set! raw-config
(with-handlers ([void (lambda (_)
(error 'get-conf
"could not read conf (~a)"
config-file))])
(when raw-config
;; can't use log-line from logger, since it makes a cycle
(fprintf (current-error-port)
(format "loading configuration from ~a\n"
config-file)))
(with-input-from-file config-file read)))
(set! config-cache (make-hash-table)))))
(hash-table-get config-cache key
(lambda ()
(let*-values ([(default translate) (config-default+translate key)]
;; translate = #f => this is a computed value
[(v) (if translate
(translate (cond [(assq key raw-config) => cadr]
[else default]))
default)])
(hash-table-put! config-cache key v)
v))))
(define last-poll #f)
(define last-filetime #f)
(define raw-config #f)
(define config-cache #f)
(define (id x) x)
(define (rx s) (if (regexp? s) s (regexp s)))
(define (path p) (path->complete-path p server-dir))
(define (path/false p) (and p (path p)))
(define (path-list l) (map path l))
(provide get-conf)
(define (get-conf key)
(unless (and raw-config
(< (- (current-inexact-milliseconds) last-poll) poll-freq))
(set! last-poll (current-inexact-milliseconds))
(let ([filetime (file-or-directory-modify-seconds config-file)])
(unless (and filetime (equal? filetime last-filetime))
(set! last-filetime filetime)
(set! raw-config
(with-handlers ([void (lambda (_)
(error 'get-conf
"could not read conf (~a)"
config-file))])
(when raw-config
;; can't use log-line from logger, since it makes a cycle
(fprintf (current-error-port)
(format "loading configuration from ~a\n"
config-file)))
(with-input-from-file config-file read)))
(set! config-cache (make-hasheq)))))
(hash-ref config-cache key
(lambda ()
(let*-values ([(default translate) (config-default+translate key)]
;; translate = #f => this is a computed value
[(v) (if translate
(translate (cond [(assq key raw-config) => cadr]
[else default]))
default)])
(hash-set! config-cache key v)
v))))
(define (config-default+translate which)
(case which
[(active-dirs) (values '() path-list )]
[(inactive-dirs) (values '() path-list )]
[(port-number) (values 7979 id )]
[(https-port-number) (values #f id )]
[(hook-file) (values #f path/false )]
[(session-timeout) (values 300 id )]
[(session-memory-limit) (values 40000000 id )]
[(default-file-name) (values "handin.scm" id )]
[(max-upload) (values 500000 id )]
[(max-upload-keep) (values 9 id )]
[(user-regexp) (values #rx"^[a-z][a-z0-9]+$" rx )]
[(user-desc) (values "alphanumeric string" id )]
[(username-case-sensitive) (values #f id )]
[(allow-new-users) (values #f id )]
[(allow-change-info) (values #f id )]
[(master-password) (values #f id )]
[(web-base-dir) (values #f path/false )]
[(log-output) (values #t id )]
[(log-file) (values "log" path/false )]
[(web-log-file) (values #f path/false )]
[(extra-fields)
(values '(("Full Name" #f #f)
("ID#" #f #f)
("Email" #rx"^[^@<>\"`',]+@[a-zA-Z0-9_.-]+[.][a-zA-Z]+$"
"a valid email address"))
id)]
;; computed from the above (mark by translate = #f)
[(all-dirs)
(values (append (get-conf 'active-dirs) (get-conf 'inactive-dirs)) #f)]
[(names-dirs) ; see below
(values (paths->map (get-conf 'all-dirs)) #f)]
[(user-fields)
(values (filter (lambda (f) (not (eq? '- (cadr f))))
(get-conf 'extra-fields))
#f)]
[else (error 'get-conf "unknown configuration entry: ~s" which)]))
(define (id x) x)
(define (rx s) (if (regexp? s) s (regexp s)))
(define (path p) (path->complete-path p server-dir))
(define (path/false p) (and p (path p)))
(define (path-list l) (map path l))
;; This is used below to map names to submission directory paths and back
;; returns a (list-of (either (list name path) (list path name)))
(define (paths->map dirs)
(define (path->name dir)
(unless (directory-exists? dir)
(error 'get-conf
"directory entry for an inexistent directory: ~e" dir))
(let-values ([(_1 name _2) (split-path dir)])
(bytes->string/locale (path-element->bytes name))))
(let ([names (map path->name dirs)])
(append (map list names dirs) (map list dirs names))))
(define (config-default+translate which)
(case which
[(active-dirs) (values '() path-list )]
[(inactive-dirs) (values '() path-list )]
[(port-number) (values 7979 id )]
[(https-port-number) (values #f id )]
[(hook-file) (values #f path/false )]
[(session-timeout) (values 300 id )]
[(session-memory-limit) (values 40000000 id )]
[(default-file-name) (values "handin.scm" id )]
[(max-upload) (values 500000 id )]
[(max-upload-keep) (values 9 id )]
[(user-regexp) (values #rx"^[a-z][a-z0-9]+$" rx )]
[(user-desc) (values "alphanumeric string" id )]
[(username-case-sensitive) (values #f id )]
[(allow-new-users) (values #f id )]
[(allow-change-info) (values #f id )]
[(master-password) (values #f id )]
[(web-base-dir) (values #f path/false )]
[(log-output) (values #t id )]
[(log-file) (values "log" path/false )]
[(web-log-file) (values #f path/false )]
[(extra-fields)
(values '(("Full Name" #f #f)
("ID#" #f #f)
("Email" #rx"^[^@<>\"`',]+@[a-zA-Z0-9_.-]+[.][a-zA-Z]+$"
"a valid email address"))
id)]
;; computed from the above (mark by translate = #f)
[(all-dirs)
(values (append (get-conf 'active-dirs) (get-conf 'inactive-dirs)) #f)]
[(names-dirs) ; see below
(values (paths->map (get-conf 'all-dirs)) #f)]
[(user-fields)
(values (filter (lambda (f) (not (eq? '- (cadr f))))
(get-conf 'extra-fields))
#f)]
[else (error 'get-conf "unknown configuration entry: ~s" which)]))
;; Translates an assignment name to a directory path or back
(provide assignment<->dir)
(define (assignment<->dir a/d)
(cond [(assoc a/d (get-conf 'names-dirs)) => cadr]
[else (error 'assignment<->dir "internal error: ~e" a/d)]))
;; This is used below to map names to submission directory paths and back
;; returns a (list-of (either (list name path) (list path name)))
(define (paths->map dirs)
(define (path->name dir)
(unless (directory-exists? dir)
(error 'get-conf
"directory entry for an inexistent directory: ~e" dir))
(let-values ([(_1 name _2) (split-path dir)])
(bytes->string/locale (path-element->bytes name))))
(let ([names (map path->name dirs)])
(append (map list names dirs) (map list dirs names))))
)
;; Translates an assignment name to a directory path or back
(provide assignment<->dir)
(define (assignment<->dir a/d)
(cond [(assoc a/d (get-conf 'names-dirs)) => cadr]
[else (error 'assignment<->dir "internal error: ~e" a/d)]))

View File

@ -1,18 +1,17 @@
(module hooker mzscheme
(require "config.ss" "logger.ss" "reloadable.ss")
#lang scheme/base
(provide hook)
(require "config.ss" "logger.ss" "reloadable.ss")
(define hook-file #f)
(define hook-proc #f)
(provide hook)
(define (hook what alist)
(let ([file (get-conf 'hook-file)])
(when file
(unless (equal? file hook-file)
(set! hook-file file)
(set! hook-proc (auto-reload-procedure `(file ,(path->string file))
'hook)))
(hook-proc what (current-session) alist))))
(define hook-file #f)
(define hook-proc #f)
)
(define (hook what alist)
(let ([file (get-conf 'hook-file)])
(when file
(unless (equal? file hook-file)
(set! hook-file file)
(set! hook-proc (auto-reload-procedure `(file ,(path->string file))
'hook)))
(hook-proc what (current-session) alist))))

View File

@ -1,64 +1,63 @@
(module lock mzscheme
(require mzlib/list)
#lang scheme/base
(provide wait-for-lock)
(provide wait-for-lock)
;; wait-for-lock : string -> void
;; Gets a lock on `user' for the calling thread; the lock lasts until the
;; calling thread terminates. If the lock was actually acquired, then on
;; release the cleanup-thunk will be executed (unless it is #f), even if it
;; was released when the acquiring thread crashed.
;; *** Warning: It's vital that a clean-up thunk doesn't raise an exception,
;; since this will kill the lock thread which will lock down everything
(define (wait-for-lock user . cleanup-thunk)
(let ([s (make-semaphore)])
(channel-put req-ch
(make-req (thread-dead-evt (current-thread)) user s
(and (pair? cleanup-thunk) (car cleanup-thunk))))
(semaphore-wait s)))
;; wait-for-lock : string -> void
;; Gets a lock on `user' for the calling thread; the lock lasts until the
;; calling thread terminates. If the lock was actually acquired, then on
;; release the cleanup-thunk will be executed (unless it is #f), even if it
;; was released when the acquiring thread crashed.
;; *** Warning: It's vital that a clean-up thunk doesn't raise an exception,
;; since this will kill the lock thread which will lock down everything
(define (wait-for-lock user . cleanup-thunk)
(let ([s (make-semaphore)])
(channel-put req-ch
(make-req (thread-dead-evt (current-thread)) user s
(and (pair? cleanup-thunk) (car cleanup-thunk))))
(semaphore-wait s)))
(define req-ch (make-channel))
(define req-ch (make-channel))
(define-struct req (thread-dead-evt user sema cleanup-thunk))
(define-struct req (thread-dead-evt user sema cleanup-thunk))
(thread
(lambda ()
(let loop ([locks null]
[reqs null])
(let-values ([(locks reqs)
;; Try to satisfy lock requests:
(let loop ([reqs (reverse reqs)]
[locks locks]
[new-reqs null])
(if (null? reqs)
(values locks new-reqs)
(let ([req (car reqs)]
[rest (cdr reqs)])
(if (assoc (req-user req) locks)
;; Lock not available:
(loop rest locks (cons req new-reqs))
;; Lock is available, so take it:
(begin (semaphore-post (req-sema req))
(loop (cdr reqs)
(cons (cons (req-user req) req) locks)
new-reqs))))))])
(sync
(handle-evt req-ch (lambda (req) (loop locks (cons req reqs))))
;; Release a lock whose thread is gone:
(apply choice-evt
(map (lambda (name+req)
(handle-evt
(req-thread-dead-evt (cdr name+req))
(lambda (v)
;; releasing a lock => run cleanup
(cond [(req-cleanup-thunk (cdr name+req))
=> (lambda (t) (t))])
(loop (remq name+req locks) reqs))))
locks))
;; Throw away a request whose thread is gone:
(apply choice-evt
(map (lambda (req)
(handle-evt
(req-thread-dead-evt req)
(lambda (v) (loop locks (remq req reqs)))))
reqs))))))))
(thread
(lambda ()
(let loop ([locks null]
[reqs null])
(let-values ([(locks reqs)
;; Try to satisfy lock requests:
(let loop ([reqs (reverse reqs)]
[locks locks]
[new-reqs null])
(if (null? reqs)
(values locks new-reqs)
(let ([req (car reqs)]
[rest (cdr reqs)])
(if (assoc (req-user req) locks)
;; Lock not available:
(loop rest locks (cons req new-reqs))
;; Lock is available, so take it:
(begin (semaphore-post (req-sema req))
(loop (cdr reqs)
(cons (cons (req-user req) req) locks)
new-reqs))))))])
(sync
(handle-evt req-ch (lambda (req) (loop locks (cons req reqs))))
;; Release a lock whose thread is gone:
(apply choice-evt
(map (lambda (name+req)
(handle-evt
(req-thread-dead-evt (cdr name+req))
(lambda (v)
;; releasing a lock => run cleanup
(cond [(req-cleanup-thunk (cdr name+req))
=> (lambda (t) (t))])
(loop (remq name+req locks) reqs))))
locks))
;; Throw away a request whose thread is gone:
(apply choice-evt
(map (lambda (req)
(handle-evt
(req-thread-dead-evt req)
(lambda (v) (loop locks (remq req reqs)))))
reqs)))))))

View File

@ -1,77 +1,78 @@
(module logger mzscheme
(require "config.ss" mzlib/date mzlib/port)
#lang scheme/base
(provide current-session)
(define current-session (make-parameter #f))
(require "config.ss" scheme/date scheme/port)
;; A convenient function to print log lines (which really just assembles a
;; string to print in one shot, and flushes the output)
(provide log-line)
(define (log-line fmt . args)
(let ([line (format "~a\n" (apply format fmt args))])
(display line (current-error-port))))
(provide current-session)
(define current-session (make-parameter #f))
(define (prefix)
(parameterize ([date-display-format 'iso-8601])
(format "[~a|~a] "
(or (current-session) '-)
(date->string (seconds->date (current-seconds)) #t))))
;; A convenient function to print log lines (which really just assembles a
;; string to print in one shot, and flushes the output)
(provide log-line)
(define (log-line fmt . args)
(let ([line (format "~a\n" (apply format fmt args))])
(display line (current-error-port))))
(define (combine-outputs o1 o2)
(let-values ([(i o) (make-pipe)])
(thread
(lambda ()
(let loop ()
(let ([line (read-bytes-line i)])
(if (eof-object? line)
(begin (close-output-port o1) (close-output-port o2))
(begin (write-bytes line o1) (newline o1) (flush-output o1)
(write-bytes line o2) (newline o2) (flush-output o2)
(loop)))))))
o))
(define (prefix)
(parameterize ([date-display-format 'iso-8601])
(format "[~a|~a] "
(or (current-session) '-)
(date->string (seconds->date (current-seconds)) #t))))
;; Implement a logger by making the current-error-port show prefix tags and
;; output the line on the output port
(define (make-logger-port out log)
(if (and (not out) (not log))
;; /dev/null-like output port
(make-output-port 'nowhere
always-evt
(lambda (buf start end imm? break?) (- end start))
void)
(let ([prompt? #t]
[sema (make-semaphore 1)]
[outp (cond [(not log) out]
[(not out) log]
[else (combine-outputs out log)])])
(make-output-port
'logger-output
outp
(lambda (buf start end imm? break?)
(dynamic-wind
(lambda () (semaphore-wait sema))
(lambda ()
(if (= start end)
(begin (flush-output outp) 0)
(let ([nl (regexp-match-positions #rx#"\n" buf start end)])
;; may be problematic if this hangs...
(when prompt? (display (prefix) outp) (set! prompt? #f))
(if (not nl)
(write-bytes-avail* buf outp start end)
(let* ([nl (cdar nl)]
[l (write-bytes-avail* buf outp start nl)])
(when (= l (- nl start))
;; pre-newline part written
(flush-output outp) (set! prompt? #t))
l)))))
(lambda () (semaphore-post sema))))
(lambda () (close-output-port outp))))))
(define (combine-outputs o1 o2)
(let-values ([(i o) (make-pipe)])
(thread
(lambda ()
(let loop ()
(let ([line (read-bytes-line i)])
(if (eof-object? line)
(begin (close-output-port o1) (close-output-port o2))
(begin (write-bytes line o1) (newline o1) (flush-output o1)
(write-bytes line o2) (newline o2) (flush-output o2)
(loop)))))))
o))
;; Install this wrapper as the current error port
(provide install-logger-port)
(define (install-logger-port)
(current-error-port
(make-logger-port
(and (get-conf 'log-output) (current-output-port))
(cond [(get-conf 'log-file) => (lambda (f) (open-output-file f 'append))]
[else #f])))))
;; Implement a logger by making the current-error-port show prefix tags and
;; output the line on the output port
(define (make-logger-port out log)
(if (and (not out) (not log))
;; /dev/null-like output port
(make-output-port 'nowhere
always-evt
(lambda (buf start end imm? break?) (- end start))
void)
(let ([prompt? #t]
[sema (make-semaphore 1)]
[outp (cond [(not log) out]
[(not out) log]
[else (combine-outputs out log)])])
(make-output-port
'logger-output
outp
(lambda (buf start end imm? break?)
(dynamic-wind
(lambda () (semaphore-wait sema))
(lambda ()
(if (= start end)
(begin (flush-output outp) 0)
(let ([nl (regexp-match-positions #rx#"\n" buf start end)])
;; may be problematic if this hangs...
(when prompt? (display (prefix) outp) (set! prompt? #f))
(if (not nl)
(write-bytes-avail* buf outp start end)
(let* ([nl (cdar nl)]
[l (write-bytes-avail* buf outp start nl)])
(when (= l (- nl start))
;; pre-newline part written
(flush-output outp) (set! prompt? #t))
l)))))
(lambda () (semaphore-post sema))))
(lambda () (close-output-port outp))))))
;; Install this wrapper as the current error port
(provide install-logger-port)
(define (install-logger-port)
(current-error-port
(make-logger-port
(and (get-conf 'log-output) (current-output-port))
(cond [(get-conf 'log-file) => (lambda (f) (open-output-file f 'append))]
[else #f]))))

View File

@ -1,5 +1,5 @@
(module md5 mzscheme
(require (prefix mz: mzlib/md5))
(define (md5 s)
(bytes->string/latin-1 (mz:md5 (string->bytes/utf-8 s))))
(provide md5))
#lang scheme/base
(require (prefix-in mz: file/md5))
(define (md5 s)
(bytes->string/latin-1 (mz:md5 (string->bytes/utf-8 s))))
(provide md5)

View File

@ -1,48 +1,46 @@
(module reloadable mzscheme
#lang scheme/base
(require syntax/moddep "logger.ss")
(require syntax/moddep "logger.ss")
(provide reload-module)
(define (reload-module modspec path)
;; the path argument is not needed (could use resolve-module-path here),
;; but its always known when this function is called
(let* ([name ((current-module-name-resolver) modspec #f #f)])
(log-line "(re)loading module from ~a" modspec)
(parameterize ([current-module-declare-name name]
[compile-enforce-module-constants #f])
(namespace-require '(only mzscheme module #%top-interaction))
(load/use-compiled path))))
(provide reload-module)
(define (reload-module modspec path)
;; the path argument is not needed (could use resolve-module-path here), but
;; its always known when this function is called
(let* ([name ((current-module-name-resolver) modspec #f #f)])
(log-line "(re)loading module from ~a" modspec)
(parameterize ([current-module-declare-name name]
[compile-enforce-module-constants #f])
(namespace-require '(only mzscheme module #%top-interaction))
(load/use-compiled path))))
;; pulls out a value from a module, reloading the module if its source file
;; was modified
(provide auto-reload-value)
(define module-times (make-hash-table 'equal))
(define (auto-reload-value modspec valname)
(let* ([path (resolve-module-path modspec #f)]
[last (hash-table-get module-times path #f)]
[cur (file-or-directory-modify-seconds path)])
(unless (equal? cur last)
(hash-table-put! module-times path cur)
(reload-module modspec path))
(dynamic-require modspec valname)))
;; pulls out a value from a module, reloading the module if its source file was
;; modified
(provide auto-reload-value)
(define module-times (make-hash))
(define (auto-reload-value modspec valname)
(let* ([path (resolve-module-path modspec #f)]
[last (hash-ref module-times path #f)]
[cur (file-or-directory-modify-seconds path)])
(unless (equal? cur last)
(hash-set! module-times path cur)
(reload-module modspec path))
(dynamic-require modspec valname)))
(define poll-freq 2000.0) ; poll at most once every two seconds
(define poll-freq 2000.0) ; poll at most once every two seconds
;; pulls out a procedure from a module, and returns a wrapped procedure that
;; automatically reloads the module if the file was changed whenever the
;; procedure is used
(provide auto-reload-procedure)
(define (auto-reload-procedure modspec procname)
(let ([path (resolve-module-path modspec #f)] [date #f] [proc #f] [poll #f])
(define (reload)
(unless (and proc (< (- (current-inexact-milliseconds) poll) poll-freq))
(set! poll (current-inexact-milliseconds))
(let ([cur (file-or-directory-modify-seconds path)])
(unless (equal? cur date)
(set! date cur)
(reload-module modspec path)
(set! proc (dynamic-require modspec procname))))))
(reload)
(lambda xs (reload) (apply proc xs))))
)
;; pulls out a procedure from a module, and returns a wrapped procedure that
;; automatically reloads the module if the file was changed whenever the
;; procedure is used
(provide auto-reload-procedure)
(define (auto-reload-procedure modspec procname)
(let ([path (resolve-module-path modspec #f)] [date #f] [proc #f] [poll #f])
(define (reload)
(unless (and proc (< (- (current-inexact-milliseconds) poll) poll-freq))
(set! poll (current-inexact-milliseconds))
(let ([cur (file-or-directory-modify-seconds path)])
(unless (equal? cur date)
(set! date cur)
(reload-module modspec path)
(set! proc (dynamic-require modspec procname))))))
(reload)
(lambda xs (reload) (apply proc xs))))

View File

@ -1,21 +1,19 @@
(module run-status mzscheme
#lang scheme/base
(provide current-run-status-box set-run-status
current-messenger message)
(provide current-run-status-box set-run-status
current-messenger message)
;; current-run-status-box is used to let the client know where we are in the
;; submission process.
(define current-run-status-box (make-parameter #f))
;; current-run-status-box is used to let the client know where we are in the
;; submission process.
(define current-run-status-box (make-parameter #f))
;; current-messenger is a function that will send a message to the client.
(define current-messenger (make-parameter #f))
(define (message . args)
(let ([messenger (current-messenger)])
(and messenger (apply messenger args))))
;; current-messenger is a function that will send a message to the client.
(define current-messenger (make-parameter #f))
(define (message . args)
(let ([messenger (current-messenger)])
(and messenger (apply messenger args))))
;; Set the current-run-status-box and send a message.
(define (set-run-status s)
(let ([b (current-run-status-box)])
(when b (set-box! b s) (message s))))
)
;; Set the current-run-status-box and send a message.
(define (set-run-status s)
(let ([b (current-run-status-box)])
(when b (set-box! b s) (message s))))

View File

@ -1,11 +1,7 @@
#lang scheme/base
(require scheme/list
scheme/class
mred
lang/posn
(require scheme/class mred lang/posn scheme/pretty
(prefix-in pc: mzlib/pconvert)
scheme/pretty
(only-in "main.ss" timeout-control)
"private/run-status.ss"
"private/config.ss"
@ -13,29 +9,29 @@
"sandbox.ss")
(provide (all-from-out "sandbox.ss")
get-conf
log-line
unpack-submission
make-evaluator/submission
evaluate-all
evaluate-submission
call-with-evaluator
call-with-evaluator/submission
reraise-exn-as-submission-problem
set-run-status
message
current-value-printer
check-proc
check-defined
look-for-tests
user-construct
test-history-enabled
timeout-control)
(define (unpack-submission str)
@ -76,8 +72,8 @@
(define (reraise-exn-as-submission-problem thunk)
(with-handlers ([void (lambda (exn)
(error (if (exn? exn)
(exn-message exn)
(format "exception: ~e" exn))))])
(exn-message exn)
(format "exception: ~e" exn))))])
(thunk)))
;; ----------------------------------------
@ -98,10 +94,10 @@
(define (format-history one-test)
(if (test-history-enabled)
(format "(begin~a)"
(apply string-append (map (lambda (s) (format " ~a" s))
(reverse (test-history)))))
one-test))
(format "(begin~a)"
(apply string-append (map (lambda (s) (format " ~a" s))
(reverse (test-history)))))
one-test))
(define (check-proc e result equal? f . args)
(let ([test (format "(~a~a)" f
@ -125,9 +121,7 @@
(unless ok?
(error
(format "instructor-supplied test ~a should have produced ~e, instead produced ~e"
(format-history test)
result
val)))
(format-history test) result val)))
val)))
(define (user-construct e func . args)
@ -138,18 +132,14 @@
(let loop ([found 0])
(let ([e (read p)])
(if (eof-object? e)
(when (found . < . count)
(error (format "found ~a test~a for ~a, need at least ~a test~a"
found
(if (= found 1) "" "s")
name
count
(if (= count 1) "" "s"))))
(loop (+ found
(if (and (pair? e)
(eq? (car e) name))
1
0))))))))
(when (found . < . count)
(error (format "found ~a test~a for ~a, need at least ~a test~a"
found
(if (= found 1) "" "s")
name
count
(if (= count 1) "" "s"))))
(loop (+ found (if (and (pair? e) (eq? (car e) name)) 1 0))))))))
(define list-abbreviation-enabled (make-parameter #f))

View File

@ -1 +1 @@
#lang scheme/base (provide stamp) (define stamp "11sep2008")
#lang scheme/base (provide stamp) (define stamp "12sep2008")

View File

@ -20,20 +20,28 @@ You can use the reader via MzScheme's @schemefont{#reader} form:
@schemeblock[
#, @schemefont|{
#reader(lib "reader.ss" "scribble")@{This is free-form text!}
#reader scribble/reader @foo{This is free-form text!}
}|]
Note that the reader will only read @"@"-forms as S-expressions. The
meaning of these S-expressions depends on the rest of your own code.
Note that the Scribble reader reads @"@"-forms as S-expressions. This
means that it is up to you to give meanings for these expressions in
the usual way: use Scheme functions, define your functions, or require
functions. For example, typing the above into MzScheme is likely
going to produce a ``reference to undefined identifier'' error --- you
can use @scheme[string-append] instead, or you can define @scheme[foo]
as a function (with variable arity).
A PLT Scheme manual more likely starts with
A common use of the Scribble @"@"-reader is when using Scribble as a
documentation system for producing manuals. In this case, the manual
text is likely to start with
@schememod[scribble/doc]
which installs a reader, wraps the file content afterward into a
MzScheme module, and parses the body into a document using
@schememodname[scribble/decode]. See @secref["docreader"] for more
information.
which installs the @"@" reader starting in ``text mode'', wraps the
file content afterward into a MzScheme module where many useful Scheme
and documentation related functions are available, and parses the body
into a document using @schememodname[scribble/decode]. See
@secref["docreader"] for more information.
Another way to use the reader is to use the @scheme[use-at-readtable]
function to switch the current readtable to a readtable that parses
@ -44,6 +52,8 @@ function to switch the current readtable to a readtable that parses
@;--------------------------------------------------------------------
@section{Concrete Syntax}
@subsection{The Scribble Syntax at a Glance}
Informally, the concrete syntax of @"@"-forms is
@schemeblock[
@ -55,50 +65,136 @@ Informally, the concrete syntax of @"@"-forms is
where all three parts after @litchar["@"] are optional, but at least
one should be present. (Note that spaces are not allowed between the
three parts.) @litchar["@"] is set as a non-terminating reader macro,
so it can be used as usual in Scheme identifiers unless you want to
use it as a first character of an identifier; in this case you need to
quote with a backslash (@schemefont["\\@foo"]) or quote the whole
identifier with bars (@schemefont["|@foo|"]).
three parts.) Roughly, a form matching the above grammar is read as
@schemeblock[
#, @schemefont|!{
(define |@foo| '\@bar@baz)
}!|]
Of course, @litchar["@"] is not treated specially in Scheme strings,
character constants, etc.
Roughly, a form matching the above grammar is read as
@schemeblock[
(#, @nonterm{cmd}
#, @kleenestar{@nonterm{datum}}
#, @kleenestar{@nonterm{parsed-body}})
(#, @nonterm{cmd} #, @kleenestar{@nonterm{datum}} #, @kleenestar{@nonterm{parsed-body}})
]
where @nonterm{parsed-body} is the translation of each
@nonterm{text-body} in the input. Thus, the initial @nonterm{cmd}
determines the Scheme code that the input is translated into. The
common case is when @nonterm{cmd} is a Scheme identifier, which
generates a plain Scheme form.
A @nonterm{text-body} is made of text, newlines, and nested
@"@"-forms. Note that the syntax for @"@"-forms is the same in a
@nonterm{text-body} context as in a Scheme context. A
@nonterm{text-body} that isn't an @"@"-form is converted to a string
expression for its @nonterm{parsed-body}, and newlines are converted
to @scheme["\n"] expressions.
common case is when @nonterm{cmd} is a Scheme identifier, which reads
as a plain Scheme form, with datum arguments and/or string arguments.
@scribble-examples|==={
@foo{blah blah blah}
@foo{blah "blah" (`blah'?)}
@foo[1 2]{3 4}
@foo[1 2 3 4]
@foo[#:width 2]{blah blah}
@foo{blah blah
yada yada}
@foo{
blah blah
yada yada
}
}===|
(Note that these examples show how an input syntax is read as Scheme
syntax, not what it evaluates to.)
As seen in the last example, multiple lines and the newlines that
separate them are parsed to multiple Scheme strings. More generally,
a @nonterm{text-body} is made of text, newlines, and nested
@"@"-forms, where the syntax for @"@"-forms is the same whether it's
in a @nonterm{text-body} context as in a Scheme context. A
@nonterm{text-body} that isn't an @"@"-form is converted to a string
expression for its @nonterm{parsed-body}; newlines and following
indentations are converted to @scheme["\n"] and all-space string
expressions.
@scribble-examples|==={
@foo{bar baz
blah}
@foo{bar @baz[3]
blah}
@foo{bar @baz{3}
blah}
@foo{bar @baz[2 3]{4 5}
@foo{@b{@u[3] @u{4}}
blah}
@C{while (*(p++))
*p = '\n';}
}===|
The command part of an @"@"-form is optional as well, which is read as
a list, usually a function application, but also useful when quoted
with the usual Scheme @scheme[quote]:
@scribble-examples|==={
@{blah blah}
@{blah @[3]}
'@{foo
bar
baz}
}===|
But we can also drop the datum and text parts, which leaves us with
only the command --- which is read as is, not within a parenthesized
form. This is not useful when reading Scheme code, but it can be used
inside a text block to escape a Scheme identifier. A vertical bar
(@litchar{|}) can be used to delimit the escaped identifier when
needed.
@scribble-examples|==={
@foo
@{blah @foo blah}
@{blah @foo: blah}
@{blah @|foo|: blah}
}===|
Actually, the command part can be any Scheme expression, which is
particularly useful with such escapes since they can be used with any
expression.
@scribble-examples|==={
@foo{(+ 1 2) -> @(+ 1 2)!}
@foo{A @"string" escape}
}===|
Note that an escaped Scheme string is merged with the surrounding text
as a special case. This is useful if you want to use the special
characters in your string (but note that escaping braces is not
necessary if they are balanced).
@scribble-examples|==={
@foo{eli@"@"barzilay.org}
@foo{A @"{" begins a block}
@C{while (*(p++)) {
*p = '\n';
}}
}===|
In some cases a @"@"-rich text can become cumbersome to quote. For
this, the braces have an alternative syntax --- a block of text can
begin with a ``@litchar["|{"]'' and terminated accordingly with a
``@litchar["}|"]''. Furthermore, any nested @"@" forms must begin
with a ``@litchar["|@"]''.
@scribble-examples|==={
@foo|{bar}@{baz}|
@foo|{bar |@x{X} baz}|
@foo|{bar |@x|{@}| baz}|
}===|
In cases when even this is not convenient enough, punctuation
characters can be added between the @litchar{|} and the braces and the
@"@" in nested forms. (The punctuation is mirrored for parentheses
and @litchar{<>}s.) With this, the Scribble syntax can be used as a
here-string replacement.
@scribble-examples|==={
@foo|--{bar}@|{baz}--|
@foo|<<{bar}@|{baz}>>|
}===|
The flip side of this is: how can an @"@" sign be used in Scheme code?
This is almost never an issue, because Scheme strings and characters
are still read the same, and because @litchar["@"] is set as a
non-terminating reader macro so it can be used in Scheme identifiers
as usual, except when it is the first character of an identifier. In
this case, you need to quote the identifier like other non-standard
characters --- with a backslash or with vertical bars:
@scribble-examples|==={
(define \@email "foo@bar.com")
(define |@atchar| #\@)
}===|
Note that spaces are not allowed before a @litchar{[} or a
@ -109,9 +205,13 @@ code). (More on using braces in body texts below.)
@foo{bar @baz[2 3] {4 5}}
}===|
When the above @"@"-forms appear in a Scheme expression context, the
lexical environment must provide bindings for @scheme[foo] (as a procedure or
a macro).
Finally, remember that the Scribble is just an alternate for
S-expressions --- identifiers still get their meaning, as in any
Scheme code, through the lexical context in which they appear.
Specifically, when the above @"@"-form appears in a Scheme expression
context, the lexical environment must provide bindings for
@scheme[foo] as a procedure or a macro; it can be defined, required,
or bound locally (with @scheme[let], for example).
@; FIXME: unfortunate code duplication
@interaction[
@ -132,12 +232,16 @@ a macro).
@text{@it{Note}: @bf{This is @ul{not} a pipe}.}))
]
If you want to see the expression that is actually being read, you can
use Scheme's @scheme[quote].
When you first experiment with the Scribble syntax, it is often useful
to use Scheme's @scheme[quote] to inspect how some concrete syntax is
being read.
@scribble-examples|==={
'@foo{bar}
}===|
@; FIXME: unfortunate code duplication
@interaction[
(eval:alts
#,(tt "'@foo{bar}")
'@foo{bar})
]
@;--------------------------------------------------------------------
@subsection{The Command Part}

View File

@ -63,8 +63,8 @@
w1 #t modpath src line col pos))
#t))))))))]))
(define-syntax-rule (wrap-internal lib port read whole? wrapper stx?
modpath src line col pos)
(define (wrap-internal lib port read whole? wrapper stx?
modpath src line col pos)
(let* ([body (lambda ()
(if whole?
(read port)