diff --git a/collects/compiler/zo-parse.ss b/collects/compiler/zo-parse.ss index 7bcefcbde9..cc44ec16fe 100644 --- a/collects/compiler/zo-parse.ss +++ b/collects/compiler/zo-parse.ss @@ -194,7 +194,7 @@ (define (read-module v) (match v - [`(,name ,self-modidx ,functional? ,et-functional? + [`(,name ,self-modidx ,lang-info ,functional? ,et-functional? ,rename ,max-let-depth ,dummy ,prefix ,kernel-exclusion ,reprovide-kernel? ,indirect-provides ,num-indirect-provides ,protects diff --git a/collects/framework/private/scheme.ss b/collects/framework/private/scheme.ss index 1fb505cc69..a1b1d0f928 100644 --- a/collects/framework/private/scheme.ss +++ b/collects/framework/private/scheme.ss @@ -369,7 +369,8 @@ set-tab-size introduce-let-ans - move-sexp-out)) + move-sexp-out + kill-enclosing-parens)) (define init-wordbreak-map (λ (map) @@ -1042,6 +1043,21 @@ [else (bell)])) (end-edit-sequence)) + (define/public (kill-enclosing-parens begin-inner) + (begin-edit-sequence) + (let ([begin-outer (find-up-sexp begin-inner)]) + (cond + [begin-outer + (let ([end-outer (get-forward-sexp begin-outer)]) + (cond + [(and end-outer (> (- end-outer begin-outer) 2)) + (delete (- end-outer 1) end-outer) + (delete begin-outer (+ begin-outer 1)) + (tabify-selection begin-outer (- end-outer 2))] + [else (bell)]))] + [else (bell)])) + (end-edit-sequence)) + (inherit get-fixed-style) (define/public (mark-matching-parenthesis pos) (let ([open-parens (map car (scheme-paren:get-paren-pairs))] @@ -1238,6 +1254,8 @@ (λ (e p) (send e introduce-let-ans p))) (add-pos-function "move-sexp-out" (λ (e p) (send e move-sexp-out p))) + (add-pos-function "kill-enclosing-parens" + (lambda (e p) (send e kill-enclosing-parens p))) (let ([add-edit-function (λ (name call-method) @@ -1359,7 +1377,8 @@ ) (send keymap map-function "c:c;c:b" "remove-parens-forward") (send keymap map-function "c:c;c:l" "introduce-let-ans") - (send keymap map-function "c:c;c:o" "move-sexp-out"))) + (send keymap map-function "c:c;c:o" "move-sexp-out") + (send keymap map-function "c:c;c:e" "kill-enclosing-parens"))) (define keymap (make-object keymap:aug-keymap%)) (setup-keymap keymap) diff --git a/collects/handin-server/info.ss b/collects/handin-server/info.ss index 574c25002b..3770014a89 100644 --- a/collects/handin-server/info.ss +++ b/collects/handin-server/info.ss @@ -1,5 +1,3 @@ #lang setup/infotab -(define scribblings '(("scribblings/handin-server.scrbl" (user-doc)))) - (define compile-omit-paths '("status-web-root")) diff --git a/collects/handin-server/private/config.ss b/collects/handin-server/private/config.ss index 2625ccf4c5..892c348c4e 100644 --- a/collects/handin-server/private/config.ss +++ b/collects/handin-server/private/config.ss @@ -4,7 +4,11 @@ ;; 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 server-dir + (let ([dir (or (getenv "PLT_HANDINSERVER_DIR") (current-directory))]) + (if (directory-exists? dir) + dir + (error 'config "handin server directory does not exist: ~e" dir)))) (define config-file (path->complete-path "config.ss" server-dir)) @@ -96,10 +100,11 @@ (define (paths->map dirs) (define (path->name dir) (unless (directory-exists? dir) - (error 'get-conf - "directory entry for an inexistent directory: ~e" dir)) + (if (file-exists? dir) + (error 'get-conf "directory entry points at a file: ~e" dir) + (make-directory* dir))) (let-values ([(_1 name _2) (split-path dir)]) - (bytes->string/locale (path-element->bytes name)))) + (path-element->string name))) (let ([names (map path->name dirs)]) (append (map list names dirs) (map list dirs names)))) diff --git a/collects/handin-server/private/lock.ss b/collects/handin-server/private/lock.ss index 9804eb8f0c..c9c9f1cc36 100644 --- a/collects/handin-server/private/lock.ss +++ b/collects/handin-server/private/lock.ss @@ -20,44 +20,45 @@ (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))))))) +(define (lock-loop) + (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)))))) + +(define lock-thread (thread lock-loop)) diff --git a/collects/handin-server/private/logger.ss b/collects/handin-server/private/logger.ss index 3f6176fe38..fc5a42e054 100644 --- a/collects/handin-server/private/logger.ss +++ b/collects/handin-server/private/logger.ss @@ -35,11 +35,7 @@ ;; 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) + (open-output-nowhere) (let ([prompt? #t] [sema (make-semaphore 1)] [outp (cond [(not log) out] diff --git a/collects/handin-server/scribblings/checker-utils.scrbl b/collects/handin-server/scribblings/checker-utils.scrbl new file mode 100644 index 0000000000..7ad162bb5f --- /dev/null +++ b/collects/handin-server/scribblings/checker-utils.scrbl @@ -0,0 +1,35 @@ +#lang scribble/doc +@(require "common.ss") + +@title[#:style 'toc]{Checker Utilities} + +The checker utilities are provided to make writing checker functions. +They are provided in a few layers, each layer provides new +functionality in addition to the lower one. These modules are (in +order): + +@itemize[ + +@item{@schememodname[scheme/sandbox]: contains basic sandbox + evaluation utilities. This is in MzLib since it can be used + independently.} + +@item{@schememodname[handin-server/sandbox]: contains a wrapper that + configures MzLib's sandbox for the handin server.} + +@item{@schememodname[handin-server/utils]: contains additional + utilities for dealing with handin submissions, as well as a few + helpers for testing code.} + +@item{@schememodname[handin-server/checker]: automates the task of + creating a checker function (in + @filepath{/checker.ss} modules) to cope with + common submission situations.}] + +The following sections describe each of these modules. + +@local-table-of-contents[] + +@include-section["sandbox.scrbl"] +@include-section["utils.scrbl"] +@include-section["checker.scrbl"] diff --git a/collects/handin-server/scribblings/checker.scrbl b/collects/handin-server/scribblings/checker.scrbl new file mode 100644 index 0000000000..3e36d3a304 --- /dev/null +++ b/collects/handin-server/scribblings/checker.scrbl @@ -0,0 +1,351 @@ +#lang scribble/doc +@(require "common.ss") + +@title{checker} + +@defmodulelang[handin-server/checker]{ + +The @schememodname[handin-server/checker] module provides a +higher-level of utilities, helpful in implementing `checker' functions +that are intended for a more automated system. This module is a +language module---a typical checker that uses it looks like this: + +@schemeblock[ + (module checker (lib "checker.ss" "handin-server") + (check: :language 'intermediate + :users pairs-or-singles-with-warning + :coverage? #t + (!procedure Fahrenheit->Celsius 1) + (!test (Fahrenheit->Celsius 32) 0) + (!test (Fahrenheit->Celsius 212) 100) + (!test (Fahrenheit->Celsius -4) -20) + ...))] + +} + +@defform/subs[(check: keys-n-vals body ...) + ([keys-n-vals code:blank + (code:line :key val keys-n-vals)])]{ + +Constructs (and provides) an appropriate checker function, using +keywords for features that you want, the body of the checker can +contain arbitrary code, using all utilities from +@schememodname[handin-server/utils], as well as additional ones (see +below).} + +Keywords for configuring @scheme[check:]: + +@itemize[ + +@item{@indexed-scheme[:users]---specification of users that are + acceptable for submission. Can be either a list of user lists, each + representing a known team, or procedure which will accept a list of + users and throw an exception if they are unacceptable. The default + is to accept only single-user submissions. The + @scheme[pairs-or-singles-with-warning] procedure is a useful value + for pair submission where the pairs are unknown.} + +@item{@indexed-scheme[:eval?]---whether submissions should be + evaluated. Defaults to @scheme[#t]. Note that if it is specified + as @scheme[#f], then the checker body will not be able to run any + tests on the code, unless it contains code that performs some + evaluation (e.g., using the facilities of + @schememodname[handin-server/utils]).} + +@item{@indexed-scheme[:language]---the language that is used for + evaluating submissions, same as the @scheme[_language] argument for + @scheme[make-evaluator] (see @schememodname[handin-server/sandbox]). + There is no default for this, so it must be set or an error is + raised.} + +@item{@indexed-scheme[:teachpacks]---teachpacks for evaluating + submissions, same as the @scheme[_teachpacks] argument for + @scheme[make-evaluator] (see @schememodname[handin-server/sandbox]). + This defaults to null---no teachpacks.} + +@item{@indexed-scheme[:create-text?]---if true, then a textual version + of the submission is saved as @filepath{text.scm} in a + @filepath{grading} subdirectory (or any suffix that is specified by + @scheme[:output] below, for example @filepath{hw.java} is converted + into a textual @filepath{grading/text.java}). This is intended for + printouts and grading, and is in a subdirectory so students will not + see it on the status web server. Defaults to @scheme[#t].} + +@item{@indexed-scheme[:untabify?]---if true, then tabs are converted + to spaces, assuming a standard tab width of 8 places. This is + needed for a correct computation of line lengths, but note that + DrScheme does not insert tabs in Scheme mode. Defaults to + @scheme[#t].} + +@item{@indexed-scheme[:textualize?]---if true, then all submissions + are converted to text, trying to convert objects like comment boxes + and test cases to some form of text. Defaults to @scheme[#f], + meaning that an exception is raised for submissions that are not all + text.} + +@item{@indexed-scheme[:maxwidth]---a number that specifies maximum + line lengths for submissions (a helpful feature for reading student + code). Defaults to 79. This feature can be disabled if set to + @scheme[#f]. (This is effective only when saving a textual version + of the submission files.)} + +@item{@indexed-scheme[:output]---the name of the original handin file + (unrelated to the text-converted files). Defaults to + @filepath{hw.scm}. (The suffix changes the defaults of + @scheme[:markup-prefix] and @scheme[:prefix-re].) Can be + @scheme[#f] for removing the original file after processing.} + +@item{@indexed-scheme[:multi-file]---by default, this is set to + @scheme[#f], which means that only DrScheme is used to send + submissions as usual. See @secref{multi-file} for setting up + multi-file submissions.} + +@item{@indexed-scheme[:names-checker]---used for multi-file + submissions; see @secref{multi-file} for details.} + +@item{@indexed-scheme[:markup-prefix]---used as the prefix for + @scheme[:student-lines] and @scheme[:extra-lines] below. The + default is @scheme[";;> "] or @scheme["//> "], depending on the + suffix of @scheme[:output] above. (Note: if you change this, make + sure to change @scheme[:prefix-re] too.)} + +@item{@indexed-scheme[:prefix-re]---used to identify lines with markup + (@scheme[";>"] or @scheme["//>"] etc), so students cannot fool the + system by writing marked-up code. The default is @scheme[";>"] or + @scheme["//>"], depending on the suffix of :output above.} + +@item{@indexed-scheme[:student-line]---when a submission is converted + to text, it begins with lines describing the students that have + submitted it; this is used to specify the format of these lines. It + is a string with holes that that @scheme[user-substs] fills out. + The default is @scheme["Student: {username} ({Full Name} <{Email}>)"], + which requires @scheme["Full Name"] and @scheme["Email"] entries in + the server's extra-fields configuration. These lines are prefixed + with @scheme[";;> "] or the prefix specified by + @scheme[:makup-prefix] above.} + +@item{@indexed-scheme[:extra-lines]---a list of lines to add after the + student lines, all with a @scheme[";;> "] or :markup-prefix too. + Defaults to a single line: + @scheme["Maximum points for this assignment: <+100>"]. (Can use + @scheme["{submission}"] for the submission directory.) See also + @scheme[add-header-line!].} + +@item{@indexed-scheme[:user-error-message]---a string that is used to + report an error that occurred during evaluation of the submitted + code (not during additional tests). It can be a plain string which + will be used as the error message, or a string with single a + @scheme["~a"] (or @scheme["~e"], @scheme["~s"], @scheme["~v"]) that + will be used as a format string with the actual error message. The + default is @scheme["Error in your code --\n~a"]. Useful examples of + these messages: + + @scheme["There is an error in your program, hit \"Run\" to debug"] + + @scheme["There is an error in your program:\n----\n~a\n----\nHit \"Run\" and debug your code."] + + Alternatively, the value can be a procedure that will be invoked + with the error message. The procedure can do anything it wants, and + if it does not raise an exception, then the checker will proceed as + usual. For example: + + @schemeblock{ + (lambda (msg) + (add-header-line! "Erroneous submission!") + (add-header-line! (format " --> ~a" msg)) + (message (string-append + "You have an error in your program -- please hit" + " \"Run\" and debug your code.\n" + "Email the course staff if you think your code is" + " fine.\n" + "(The submission has been saved but marked as" + " erroneous.)") + '(ok)) + (message "Handin saved as erroneous." 'final))} + + (Note that if you do this, then additional tests should be adjusted + to not raise an exception too.)} + +@item{@indexed-scheme[:value-printer]---if specified, this will be + used for @scheme[current-value-printer].} + +@item{@indexed-scheme[:coverage?]---collect coverage information when + evaluating the submission. This will cause an error if some input + is not covered. This check happens after checker tests are run, but + the information is collected and stored before, so checker tests do + not change the result. Also, you can use the @scheme[!all-covered] + procedure in the checker before other tests, if you want that + feedback earlier.}] + +Within the body of @scheme[check:], @scheme[users] and +@scheme[submission] will be bound to the checker arguments---a +(sorted) list of usernames and the submission as a byte string. In +addition to the functionality below, you can use +@scheme[((submission-eval) expr)] to evaluate expressions in the +submitted code context, and you can use +@scheme[(with-submission-bindings (id ...) body ...)] to evaluate the +body when @scheme[id]'s are bound to their values from the submission +code.} + +@deftogether[(@defform[(pre: body ...)] + @defform[(post: body ...)])]{ + + These two macros define a pre- and a post-checker. In their bodies, + @scheme[_users] and @scheme[_submission] are bound as in + @scheme[check:], but there is nothing else special about these. See + the description of the @scheme[pre-checker] and + @scheme[post-checker] values for what can be done with these, and + note that the check for valid users is always first. An example for + a sophisticated @scheme[post:] block is below---it will first + disable timeouts for this session, then it will send a email with a + submission receipt, with CC to the TA (assuming a single TA), and + pop-up a message telling the student about it: + + @schemeblock[ + (require net/sendmail) + (post: + (define info + (format "hw.scm: ~a ~a" + (file-size "hw.scm") + (file-or-directory-modify-seconds "hw.scm"))) + (timeout-control 'disable) + (log-line "Sending a receipt: ~a" info) + (send-mail-message + "course-staff@university.edu" + "Submission Receipt" + (map (lambda (user) (user-substs user "{Full Name} <{Email}>")) + users) + (list (user-substs (car users) "{TA Name} <{TA Email}>")) + null + `("Your submission was received" ,info)) + (message (string-append + "Your submission was successfully saved." + " You will get an email receipt within 30 minutes;" + " if not, please contact the course staff.") + '(ok)))]} + +@defparam[submission-eval eval (any/c . -> . any)]{ + + Holds an evaluation procedure for evaluating code in the submission + context.} + +@; JBC: is this always just a list of strings? +@defproc[(user-data [user string?]) (listof string?)]{ + + Returns a user information given a username. The returned + information is a list of strings that corresponds to the configured + @scheme[extra-fields].} + +@defproc[(user-substs [user string?] [fmt string?]) string]{ + + Uses the mappings in @scheme[user-data] to substitute user + information for substrings of the form ``@tt{{some-field-name}}'' in + @scheme[fmt]. This procedure signals an error if a field name is + missing in the user data. Also, ``@tt{{username}}'' will always be + replaced by the username and ``@tt{{submission}}'' by the current + submission directory. + + This is used to process the @scheme[:student-line] value in the + checker, but it is provided for additional uses. See the above + sample code for @scheme[post:] for using this procedure.} + +@defproc[(pairs-or-singles-with-warning [users (listof string?)]) + any]{ + + Intended for use as the @scheme[:users] entry in a checker. It will + do nothing if there are two users, and throw an error if there are + more. If there is a single user, then the user will be asked to + verify a single submission. If the student cancels, then an + exception is raised so the submission directory is retracted. If + the student approves this, the question is not repeated (this is + marked by creating a directory with a known name). This is useful + for cases where you want to allow free pair submissions---students + will often try to submit their work alone, and later on re-submit + with a partner.} + +@defproc[(teams-in-file [team-file path-string?]) + ((listof string?) . -> . void?)]{ + + @italic{Returns} a procedure that can be used for the :users entry + in a checker. The team file (relative from the server's main + directory) is expected to have user entries---a sequence of + s-expressions, each one a string or a list of strings. The + resulting procedure will allow submission only by teams that are + specified in this file. Furthermore, if this file is modified, the + new contents will be used immediately, so there is no need to + restart the server of you want to change student teams. (But + remember that if you change @scheme[("foo" "bar")] to + @scheme[("foo" "baz")], and there is already a @filepath{bar+foo} + submission directory, then the system will not allow ``@tt{foo}'' to + submit with ``@tt{bar}''.)} + +@defproc[(add-header-line! [line string?]) void?]{ + During the checker operation, can be used to add header lines to the + text version of the submitted file (in addition to the + @scheme[:extra-lines] setting). It will not have an effect if + @scheme[:create-text?] is false.} + +@defproc[(procedure/arity? [proc procedure?] [arity number?]) + boolean?]{ + Returns @scheme[#t] if @scheme[proc] is a procedure that accepts + @scheme[arity] arguments.} + +@defform[(!defined id ...)]{ + Checks that the given identifiers are defined in the (evaluated) + submission, and throws an error otherwise.} + +@defform[(!procedure id arity)]{ + + Checks that @scheme[id] is defined, and is bound to a procedure.} + +@defform[(!procedure* expr arity)]{ + + Similar to @scheme[!procedure] but omits the defined check, making + it usable with any expression, which is then evaluated in the + submission context.} + +@deftogether[(@defform[(!integer id)] + @defform[(!integer* expr)])]{ + + Similar to @scheme[!procedure] and @scheme[!procedure*] for + integers.} + +@defform*[((!test expr) + (!test expr result) + (!test expr result equal?))]{ + + The first form checks that the given expression evaluates to a + non-@scheme[#f] value in the submission context, throwing an error + otherwise. The second form compares the result of evaluation, + requiring it to be equal to @scheme[result]. The third allows + specifying an equality procedure. Note that the @scheme[result] and + @scheme[equal?] forms are @italic{not} evaluated in the submission + context.} + +@defproc*[([(!all-covered) void?] + [(!all-covered [proc (string? . -> . any)]) void?])]{ + + When coverage information is enabled (see @scheme[:coverage?] + above), checks the collected coverage information and throws an + error with source information if some code is left uncovered. If + @scheme[proc] is provided, it is applied to a string argument that + describes the location of the uncovered expression + (@scheme[":"], @scheme["#"], or + @scheme["(unknown position)"]) instead of throwing an error. The + collected information includes only execution coverage by submission + code, excluding additional checker tests. You do not have to call + this explicitly---it is called at the end of the process + automatically when @scheme[:coverage?] is enabled. It is made + available so you can call it earlier (e.g., before testing) to show + clients a coverage error first, or if you want to avoid an error. + For example, you can do this: + + @schemeblock[ + (!all-covered + (lambda (where) + (case (message (string-append + "Incomplete coverage at "where", do you want" + " to save this submission with 10% penalty?")) + [(yes) (add-header-line! "No full coverage <*90%>") + (message "Handin saved with penalty.")] + [else (error "aborting submission")])))]} diff --git a/collects/handin-server/scribblings/client-customization.scrbl b/collects/handin-server/scribblings/client-customization.scrbl new file mode 100644 index 0000000000..9b28fd59e5 --- /dev/null +++ b/collects/handin-server/scribblings/client-customization.scrbl @@ -0,0 +1,61 @@ +#lang scribble/doc +@(require "common.ss") + +@title{Client Customization} + +@itemize[ +@item{Rename (or make a copy of) the @filepath{handin-client} +collection directory. The new name should describe your class +uniquely. For example, @filepath{uu-cpsc2010} is a good name for CPSC +2010 at the University of Utah.} + +@item{Edit the first three definitions of @filepath{info.ss} in your + renamed client collection: + @itemize[ + @item{For @scheme[name], choose a name for the handin tool as it + will appear in DrScheme's interface (e.g., the @onscreen{XXX} for + the @onscreen{Manage XXX Handin Account...} menu item). Again, + make the name specific to the course, in case a student installs + multiple handin tools. Do not use @onscreen{Handin} as the last + part of the name, since @onscreen{Handin} is always added for + button and menu names.} + + @item{Uncomment the definitions of @scheme[tools], + @scheme[tool-names], and @scheme[tool-icons]. (But leave the + latter field's definition as @filepath{icon.png}.)} + + @item{For @scheme[server:port], uncomment the line, and use the + hostname and port where the server will be running to accept + handin submissions.}] + + Optionally uncomment and edit the next two definitions, + @scheme[web-menu-name] and @scheme[web-address], to add an item to + the @onscreen{Help} menu that opens a (course-specific) web page.} + +@item{Replace @filepath{icon.png} in your renamed directory with a new + 32x32 icon. This icon is displayed on startup with DrScheme's + splash screen, and it is included at half size on the + @onscreen{Handin} button. A school logo is typically useful, as it + provides a recognizably local visual cue. If students might use + multiple installed handin tools, then make sure to vary the icon + according to the course.} + +@item{Replace @filepath{server-cert.pem} in your renamed directory + with a server certificate. The file @filepath{server-cert.pem} in + @filepath{handin-client} collection is ok for testing, but the point + of this certificate is to make handins secure, so you should + generate a new (self-certifying) certificate and keep its key + private. (See @secref{server-setup}.)} + +@item{Run @commandline{mzc --collection-plt .plt } where + @tt{} is the name that you chose for your directory (i.e., + whatever you changed @filepath{handin-client} to).} + +@item{Distribute @filepath{.plt} to students for installation + into their copies of DrScheme. The students need not have access to + the DrScheme installation directory; the tool will be installed on + the filesystem in the student's personal space. If you want to + install it once on a shared installation, use setup-plt with the + @DFlag{all-users} flag.} + +] diff --git a/collects/handin-server/scribblings/common.ss b/collects/handin-server/scribblings/common.ss new file mode 100644 index 0000000000..2130de2c68 --- /dev/null +++ b/collects/handin-server/scribblings/common.ss @@ -0,0 +1,20 @@ +#lang scheme/base + +(require scheme/require) + +(require scribble/manual + (for-label scheme + (subtract-in handin-server/checker scheme) + ;; scheme/sandbox + handin-server/sandbox + handin-server/utils + mred + "hook-dummy.ss")) + +(provide (all-from-out scribble/manual) + (for-label (all-from-out scheme + handin-server/checker + handin-server/sandbox + handin-server/utils + mred + "hook-dummy.ss"))) diff --git a/collects/handin-server/scribblings/handin-server.scrbl b/collects/handin-server/scribblings/handin-server.scrbl index 81b3f55a80..9bcfe07d7d 100644 --- a/collects/handin-server/scribblings/handin-server.scrbl +++ b/collects/handin-server/scribblings/handin-server.scrbl @@ -1,1331 +1,24 @@ #lang scribble/doc -@(require scribble/manual - (for-label scheme) - #;(for-label scheme/sandbox) - (for-label handin-server/sandbox - handin-server/utils - (only-in handin-server/checker - pre: post: submission-eval user-data - user-substs pairs-or-singles-with-warning - teams-in-file add-header-line! procedure/arity? - !defined !procedure !procedure* !integer !integer* - check: - !test !all-covered) - mred)) +@(require "common.ss") -@(require (for-label handin-server/scribblings/hook-dummy)) - -@(define (comment . args) "") - -@comment{Is there an existing mechanism for comments?} -@comment{There's no enumerate?} -@comment{commandline _and_ exec?} -@comment{using commandline for stand-alone URLs?} -@comment{TO-DO: sandbox docs, create index, TOC?} +@;{John's comments: + - There's no enumerate? + - commandline _and_ exec? + - using commandline for stand-alone URLs? + - TO-DO: sandbox docs, create index, TOC? + More comments labeled with `JBC' below +;} @title{@bold{Handin Server}} +@table-of-contents[] -@section{Handin-Server and Client} +@include-section["server-client.scrbl"] +@include-section["quick-start.scrbl"] +@include-section["wheres-the-collection.scrbl"] +@include-section["client-customization.scrbl"] +@include-section["server-setup.scrbl"] +@include-section["checker-utils.scrbl"] +@include-section["other-utils.scrbl"] -The @filepath{handin-server} directory contains a server to be run by a -course instructor for accepting homework assignments and reporting on -submitted assignments. - -The @filepath{handin-client} directory contains a client to be -customized then re-distributed to students in the course. The -customized client will embed a particular hostname and port where the -server is running, as well as a server certificate. - -With a customized client, students simply install a @filepath{.plt} -file---so there's no futzing with configuration dialogs and -certificates. A student can install any number of clients at once -(assuming that the clients are properly customized, as described -below). - -The result, on the student's side, is a @onscreen{Handin} button in -DrScheme's toolbar. Clicking the @onscreen{Handin} button allows the -student to type a password and upload the current content of the -definitions and interactions window to the course instructor's server. -The @onscreen{File} menu is also extended with a @onscreen{Manage...} -menu item for managing a handin account (i.e., changing the password -and other information, or creating a new account if the instructor -configures the server to allow new accounts). Students can submit -joint work by submitting with a concatenation of usernames separated -by a ``@tt{+}''. - -On the instructor's side, the handin server can be configured to check -the student's submission before accepting it. - -The handin process uses SSL, so it is effectively as secure as the -server and each user's password. - - -@section{Quick Start for a Test Drive} - -@itemize{ -@item{Create a new directory.} - -@item{Copy @filepath{server-cert.pem} from the - @filepath{handin-client} collection to the new directory. - - NOTE: For real use, you need a new certificate. - - NOTE: See also @secref{wheres-the-collection}.} - -@item{Copy @filepath{private-key.pem} from the - @filepath{handin-server} collection to the new directory. - - NOTE: For real use, you need a new key.} - -@item{Create a file @filepath{users.ss} with the following content: - @schemeblock[ - ((tester ("8fe4c11451281c094a6578e6ddbf5eed" - "Tester" "1" "test@cs")))]} - -@item{Make a @filepath{test} subdirectory in your new directory.} - -@item{Create a file @filepath{config.ss} with the following content: - @schemeblock[((active-dirs ("test")))]} - -@item{In your new directory, run @commandline{mred -l handin-server}} - -@item{In the @filepath{handin-client} collection, edit - @filepath{info.ss} and uncomment the lines that define - @scheme[server:port], @scheme[tools], @scheme[tool-names], and - @scheme[tool-icons].} - -@item{Run @commandline{setup-plt -l handin-client} - - NOTE: Under Windows, the executable is ``@tt{Setup PLT}'' instead of - ``@tt{setup-plt}''. - - NOTE: The command line arguments are optional.} - -@item{Start DrScheme, click @onscreen{Handin} to run the client, - submit with username ``@tt{tester}'' and password ``@tt{pw}''. - - The submitted file will be @filepath{.../test/tester/handin.scm}.} - -@item{Check the status of your submission by pointing a web browser at - @tt{https://localhost:7980/servlets/status.ss}. Note the ``s'' in - ``https''. Use the ``@tt{tester}'' username and ``@tt{pw}'' - password, as before.} -} - - -@section[#:tag "wheres-the-collection"]{Where is the collection?} - -If you obtained the server and client by installing a @filepath{.plt} -file, then the @filepath{handin-server} and @filepath{handin-client} -directories might be in your PLT addon space. Start MzScheme, and -enter -@schemeblock[(collection-path "handin-server")] -@schemeblock[(collection-path "handin-client")] -to find out where these collections are. - - -@section{Client Customization} - -@itemize{ -@item{Rename (or make a copy of) the @filepath{handin-client} -collection directory. The new name should describe your class -uniquely. For example, @filepath{uu-cpsc2010} is a good name for CPSC -2010 at the University of Utah.} - -@item{Edit the first three definitions of @filepath{info.ss} in your - renamed client collection: - @itemize{ - @item{For @scheme[name], choose a name for the handin tool as it - will appear in DrScheme's interface (e.g., the @onscreen{XXX} for - the @onscreen{Manage XXX Handin Account...} menu item). Again, - make the name specific to the course, in case a student installs - multiple handin tools. Do not use @onscreen{Handin} as the last - part of the name, since @onscreen{Handin} is always added for - button and menu names.} - - @item{Uncomment the definitions of @scheme[tools], - @scheme[tool-names], and @scheme[tool-icons].} - - @item{For @scheme[server:port], uncomment the line, and use the - hostname and port where the server will be running to accept - handin submissions.}} - - Optionally uncomment and edit the next two definitions, - @scheme[web-menu-name] and @scheme[web-address], to add an item to - the @onscreen{Help} menu that opens a (course-specific) web page.} - -@item{Replace @filepath{icon.png} in your renamed directory with a new - 32x32 icon. This icon is displayed on startup with DrScheme's - splash screen, and it is included at half size on the - @onscreen{Handin} button. Again, choose a distinct icon for the - benefit of students who install multiple handin tools. A school - logo is typically useful, as it provides a recognizably local visual - cue.} - -@item{Replace @filepath{server-cert.pem} in your renamed directory - with a server certificate. The file @filepath{server-cert.pem} in - @filepath{handin-client} collection is ok for testing, but the point - of this certificate is to make handins secure, so you should - generate a new (self-certifying) certificate and keep its key - private. (See @secref{server-setup}.)} - -@item{Run @commandline{mzc --collection-plt .plt } where - @tt{} is the name that you chose for your directory (i.e., - whatever you changed @filepath{handin-client} to).} - -@item{Distribute @filepath{.plt} to students for installation - into their copies of DrScheme. The students need not have access to - the DrScheme installation directory; the tool will be installed on - the filesystem in the student's personal space. If you want to - install it once on a shared installation, use setup-plt with the - @DFlag{all-users} flag.} - -} - -@section{Bogus Section} - - -@section[#:tag "server-setup"]{Server Setup} - -@declare-exporting[#:use-sources (handin-server/scribblings/hook-dummy)] - - -You must prepare a special directory to host the handin server. To -run the server, you should either be in this directory, or you should -set the @envvar{PLT_HANDINSERVER_DIR} environment variable. - -This directory contains the following files and sub-directories: -@itemize{ -@item{@filepath{server-cert.pem}: the server's certificate. To create - a certificate and key with openssl: - @commandline{openssl req -new -nodes -x509 -days 365 - -out server-cert.pem -keyout private-key.pem}} - -@item{@filepath{private-key.pem}: the private key to go with - @filepath{server-cert.pem}. Whereas @filepath{server-cert.pem} gets - distributed to students with the handin client, - @filepath{private-key.pem} is kept private.} - -@item{@filepath{config.ss}: configuration options. The file format is - @schemeblock[(( ) ...)] - - The following keys can be used: - - @itemize{ - @item{@indexed-scheme[active-dirs] --- a list of directories that - are active submissions, relative to the current directory or - absolute; the last path element for each of these (and - @scheme[inactive-dirs] below) should be unique, and is used to - identify the submission (for example, in the client's submission - dialog and in the status servlet).} - - @item{@indexed-scheme[inactive-dirs] --- a list of inactive - submission directories (see above for details).} - - @item{@indexed-scheme[port-number] --- the port for the main handin - server; the default is 7979.} - - @item{@indexed-scheme[https-port-number] --- the port number for the - handin-status HTTPS server; the default is @scheme[#f] which - indicates that no HTTPS server is started.} - - @item{@indexed-scheme[session-timeout] --- number of seconds before - the session times-out. The client is given this many seconds for - the login stage and then starts again so the same number of - seconds is given for the submit-validation process; the default is - 300.} - - @item{@indexed-scheme[session-memory-limit] --- maximum size in - bytes of memory allowed for per-session computation, if - per-session limits are supported (i.e., when using MrEd and - MzScheme with the (default) exact garbage collector and memory - accounting); the default is 40000000.} - - @item{@indexed-scheme[default-file-name] --- the default filename - that will be saved with the submission contents. The default is - @filepath{handin.scm}.} - - @item{@indexed-scheme[max-upload] --- maximum size in bytes of an - acceptable submission; the default is 500000.} - - @item{@indexed-scheme[max-upload-keep] --- maximum index of - submissions to keep; the most recent submission is - @filepath{handin.scm} (by default), the next oldest is in - @filepath{BACKUP-0/handin.scm}, next oldest is - @filepath{BACKUP-1/handin.scm}, etc. The default is 9.} - - @item{@indexed-scheme[user-regexp] --- a regular expression that is - used to validate usernames; alternatively, this can be @scheme[#f] - meaning no restriction, or a list of permitted strings. Young - students often choose exotic usernames that are impossible to - remember, and forget capitalization, so the default is fairly - strict--- @scheme[#rx"^[a-z][a-z0-9]+$"]; a @scheme["+"] is always - disallowed in a username, since it is used in a submission - username to specify joint work.} - - @item{@indexed-scheme[user-desc] --- a plain-words description of - the acceptable username format (according to user-regexp above); - @scheme[#f] stands for no description; the default is - @scheme["alphanumeric string"] which matches the default - user-regexp.} - - @item{@indexed-scheme[username-case-sensitive] --- a boolean; when - @scheme[#f], usernames are case-folded for all purposes; defaults - to @scheme[#f] (note that you should not set this to @scheme[#t] - on Windows or when using other case-insensitive filesystems, since - usernames are used as directory names).} - - @item{@indexed-scheme[allow-new-users] --- a boolean indicating - whether to allow new-user requests from a client tool; the default - is @scheme[#f].} - - @item{@indexed-scheme[allow-change-info] --- a boolean indicating - whether to allow changing user information from a client tool - (changing passwords is always possible); the default is - @scheme[#f].} - - @item{@indexed-scheme[master-password] --- a string for an MD5 hash - for a password that allows login as any user; the default is - @scheme[#f], which disables the password.} - - @item{@indexed-scheme[log-output] --- a boolean that controls - whether the handin server log is written on the standard output; - defaults to @scheme[#t].} - - @item{@indexed-scheme[log-file] --- a path (relative to handin - server directory or absolute) that specifies a filename for the - handin server log (possibly combined with the @scheme[log-output] - option), or @scheme[#f] for no log file; defaults to - @filepath{log}.} - - @item{@indexed-scheme[web-base-dir] --- if @scheme[#f] (the - default), the built-in web server will use the - @filepath{status-web-root} in this collection for its - configuration; to have complete control over the built in server, - you can copy and edit @filepath{status-web-root}, and add this - configuration entry with the name of your new copy (relative to - the handin server directory, or absolute).} - - @item{@indexed-scheme[web-log-file] --- a path (relative to handin - server directory or absolute) that specifies a filename for - logging the internal HTTPS status web server; or @scheme[#f] (the - default) to disable this log.} - - @item{@indexed-scheme[extra-fields] --- a list that describes extra - string fields of information for student records; each element in - this list is a list of three values: the name of the field, the - regexp (or @scheme[#f], or a list of permitted string values), and - a string describing acceptable strings. The default is - @schemeblock[ - '(("Full Name" #f #f) - ("ID#" #f #f) - ("Email" #rx"^[^@<>\"`',]+@[a-zA-Z0-9_.-]+[.][a-zA-Z]+$" - "a valid email address"))] - You can set this to a list of fields that you are interested in - keeping, for example: - @schemeblock[ - '(("Full Name" - #rx"^[A-Z][a-zA-Z]+(?: [A-Z][a-zA-Z]+)+$" - "full name, no punctuations, properly capitalized") - ("Utah ID Number" - #rx"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$" - "Utah ID Number with exactly nine digits") - ("Email" - #rx"^[^@<>\"`',]+@cs\\.utah\\.edu$" - "A Utah CS email address"))] - The order of these fields will be used both on the client GUI side - and in the @filepath{users.ss} file (see below). - - @comment{a hyperlink here for users.ss?} - - The second item in a field description can also be the symbol - @scheme['-], which marks this field as one that is hidden from the - user interface: students will not see it and will not be able to - provide or modify it; when a new student creates an account, such - fields will be left empty. This is useful for adding information - that you have on students from another source, for example, adding - information from a course roster. You should manually edit the - @filepath{users.ss} file and fill in such information. (The third - element for such descriptors is ignored.)} - - @item{@indexed-scheme[hook-file] --- a path (relative to handin - server directory or absolute) that specifies a filename that - contains a `hook' module. This is useful as a general device for - customizing the server through Scheme code. The file is expected - to contain a module that provides a @scheme[hook] function, which - should be receiving three arguments: - - @defproc[(hook [operation symbol?] - [connection-context (or/c number? symbol? false?)] - [relevant-info (listof (list/c symbol? any))]) - void?]{ - - The @scheme[operation] argument indicates the operation that is - now taking place. It can be one of the following: - @indexed-scheme['server-start], - @indexed-scheme['server-connect], @indexed-scheme['user-create], - @indexed-scheme['user-change], @indexed-scheme['login], - @indexed-scheme['submission-received], - @indexed-scheme['submission-committed], - @indexed-scheme['submission-retrieved], - @indexed-scheme['status-login], or - @indexed-scheme['status-file-get]. - - The @scheme[connection-context] argument is a datum that - specifies the connection context (a number for handin - connections, a @scheme['wN] symbol for servlet connections, and - @scheme[#f] for other server operations). - - The @scheme[relevant-info] contains an alist of information - relevant to this operation. Currently, the hook is used in - several places after an operation has completed. - - For example, here is a simple hook module that sends - notification messages when users are created or their - information has changed: - - @schememod[ - mzscheme - (provide hook) - (require net/sendmail) - (define (hook what session alist) - (when (memq what '(user-create user-change)) - (send-mail-message - "course-staff@university.edu" - (format "[server] ~a (~a)" what session) - '("course-staff@university.edu") '() '() - (map (lambda (key+val) - (apply format "~a: ~s" key+val)) - alist))))]}}} - - Changes to @filepath{config.ss} are detected, the file will be - re-read, and options are reloaded. A few options are fixed at - startup time: port numbers, log file specs, and the - @scheme[web-base-dir] are as configured at startup. All other - options will change the behavior of the running server (but things - like @scheme[username-case-sensitive?] it would be unwise to do - so). (For safety, options are not reloaded until the file parses - correctly, but make sure that you don't save a copy that has - inconsistent options: it is best to create a new configuration file - and move it over the old one, or use an editor that does so and not - save until the new contents is ready.) This is most useful for - closing & opening submissions directories.} - -@item{@filepath{users.ss} (created if not present if a user is added): - keeps the list of user accounts, along with the associated password - (actually the MD5 hash of the password), and extra string fields as - specified by the 'extra-fields configuration entry (in the same - order). The file format is - @schemeblock[ - (( ( ...)) - ...)] - - For example, the default @scheme['extra-field] setting will make this: - @schemeblock[ - (( ( )) - ...)] - - Usernames that begin with ``solution'' are special. They are used - by the HTTPS status server. Independent of the - @scheme['user-regexp] and @scheme['username-case-sensitive?] - configuration items, usernames are not allowed to contain characters - that are illegal in Windows pathnames, and they cannot end or begin - in spaces or periods. - - If the @scheme['allow-new-users] configuration allows new users, the - @filepath{users.ss} file can be updated by the server with new - users. It can always be updated by the server to change passwords. - - If you have access to a standard Unix password file (from - @filepath{/etc/passwd} or @filepath{/etc/shadow}), then you can - construct a @filepath{users.ss} file that will allow users to use - their normal passwords. To achieve this, use a list with 'unix as - the first element and the system's encrypted password string as the - second element. Such passwords can be used, but when users change - them, a plain md5 hash will be used. - - You can combine this with other fields from the password file to - create your @filepath{users.ss}, but make sure you have information - that matches your 'extra-fields specification. For example, given - this system file: - @verbatim[#:indent 2]{ - foo:wRzN1u5q2SqRD:1203:1203:L.E. Foo :/home/foo:/bin/tcsh - bar:$1$dKlU0OkJ$t63TzKz:1205:1205:Bar Z. Lie:/home/bar:/bin/bash} - you can create this @filepath{users.ss} file: - @schemeblock[ - ((foo ((unix "wRzN1u5q2SqRD") "L.E. Foo" "?")) - (bar ((unix "$1$dKlU0OkJ$t63TzKz") "Bar Z. Lie" "?")))] - which can be combined with this setting for @scheme['extra-fields] - in your @filepath{config.ss}: - @schemeblock[ - ... - (extra-fields (("Full Name" #f #f) - ("TA" '("Alice" "Bob") "Your TA"))) - ...] - and you can tell your students to use their department username and - password, and use the @onscreen{Manage ...} dialog to properly set - their TA name. - - Finally, a password value can be a list that begins with a - @scheme['plaintext] symbol, which will be used without encryption. - This may be useful for manually resetting a forgotten passwords.} - -@item{@filepath{log} (or any other name that the @scheme['log-file] - configuration option specifies (if any), created if not present, - appended otherwise): records connections and actions, where each - entry is of the form - @verbatim{[|