#lang scribble/doc
@(require
  scribble/manual
  (for-label racket "../errno.rkt" "../server.rkt"))

@title[#:tag "server"]{Server}

@defmodule[(planet "server.rkt" ("murphy" "9p.plt" 2 0))]{

  This module re-exports all bindings from the @racket["server/filesystem.rkt"],
  @racket["server/handle.rkt"] and @racket["server/util.rkt"] modules.

}

@section[#:tag "server/data"]{Supporting Data Structures}

@defmodule[(planet "data.rkt" ("murphy" "9p.plt" 2 0) "server")]{

  Utilities to support the operation of the 9P server.

}

@defform[(touch-mode id)
         #:contracts ([id (symbols 'name 'length 'mode 'mtime 'gid)])]{

  Enumeration of possible requested operations when changing the
  directory entry for a file.

}

@defclass[server-context% object% ()]{

  Server side representation of user rights in a filesystem.
  Encapsulates all access control operations.
  
  @defconstructor[([user string?])]{

    Initializes the new context with the given user name.

  }
  
  @defmethod[#:mode public-final
             (->user) string?]{

    Unwraps the user name contained in the context.

  }

  @defmethod[#:mode public
             (in-group? [group string?]) any/c]{

    Checks whether the user of this context is a member of the given group.
    
    The default implementation returns @racket[#t] iff the user name is
    the same as the group name.

  }

  @defmethod[#:mode public
             (can-access? [file (is-a?/c server-file%)] [mode natural-number/c])
             any/c]{

    Checks whether the user of this context is allowed to access the given
    @racket[file] using the open mode @racket[mode].
    
    The default implementation checks the @racket[open-mode-direction] of the
    mode against the permission bits of the file as obtained by calling
    @method[server-file<%> read-stat] on the file. If the user of this context
    is the owner of the file, the @racket[file-mode-user] is checked, if she
    is a member of the file's owning group, the @racket[file-mode-group] is
    checked, otherwise the @racket[file-mode-others] is checked.

  }

  @defmethod[#:mode public
             (can-touch? [file (is-a?/c server-file%)] [mode (or/c symbol? (listof symbol?))])
             any/c]{

    Checks whether the user of this context is allowed to modify the
    directory entry of the given @racket[file] changing one of the
    properties indicated by @racket[mode].
    
    The default implementation allows @itemlist[

      @item{

        @racketid[name] changes to anybody with write access to the file's
        @method[server-file<%> parent],
        
      }
      
      @item{

        @racketid[length] truncation to anybody with write access to the
        file itself,
        
      }
      
      @item{

        @racketid[mode] changes to the file's owner if she also has write
        access to the file's @method[server-file<%> parent] and
        
      }
      
      @item{

        @racketid[mtime] changes to the file's owner if she also has write
        access to the file itself.
        
      }
      
    ]
    
    Changes to the file's @racketid[gid] are forbidden by default.

  }

  @defmethod[#:mode public
             (can-remove? [file (is-a?/c server-file%)]) any/c]{

    Checks whether the user of this context is allowed to delete
    the given @racket[file].
    
    The default implementation allows anyone with write access to both
    the file itself and to its @method[server-file<%> parent] to delete
    it.

  }

}

@section[#:tag "server/filesystem"]{Filesystem}

@defmodule[(planet "filesystem.rkt" ("murphy" "9p.plt" 2 0) "server")]{

  Implementation of the @racket[filesystem<%>] interface for the server side.

}

@defclass[server-filesystem% object% (filesystem<%>)]{

  Server side implementation of a 9P filesystem.

  @defconstructor[([with-root (is-a?/c server-directory%)]
                   [with-roots dict?]
                   [port-no (integer-in 0 65535) 564]
                   [hostname (or/c string? #f) #f]
                   [max-allow-wait natural-number/c 4]
                   [reuse? any/c #f]
                   [9wrapper (or/c path-string? #f) (find-executable-path "9")])]{

    Listens for TCP connections at @racket[hostname] and @racket[port-no]
    and starts a server event loop.
    
    Either @racket[with-root] or @racket[with-roots] must be specified to
    determine which directories are served as filesystem roots: @itemlist[

      @item{

        If @racket[with-root] is used, the server offers a single root
        directory under the default name @racket[""].
        
      }
      
      @item{

        If @racket[with-roots] is used, it should be passed a dictionary
        mapping root names to directory objects.
        
      }
      
    ]
    
    If @racket[9wrapper] is not @racket[#f] it should be a path to
    the @racket["9"] command line program from the
    @link["http://swtch.com/plan9port/"]{"Plan9 from User Space"}
    utility collection. If this program is available, the convenience methods
    @method[server-filesystem% mount] and @method[server-filesystem% unmount]
    can be used.

  }

  @defmethod[#:mode public
             (make-context [user string?]) (is-a?/c server-context%)]{

    Creates a new access control context for the given @racket[user].

  }
    
  @defmethod[#:mode override
             (authenticate [root string?] [#:user user string?])
             (is-a?/c server-file-handle%)]{

    Opens an authentication channel suitable as a token when attaching
    to the filesystem root directory @racket[root] as the given
    @racket[user].
    
    The default implementation just raises a filesystem exception with
    the message @racket[ENOSYS], indicating that authentication is not
    required.

  }
    
  @defmethod[#:mode override-final
             (attach [root string?]
                     [#:user user string?]
                     [#:token auth (or/c (is-a?/c server-file-handle%) #f)])
             (is-a?/c server-directory-handle%)]{

    Opens the filesystem root directory @racket[root] as the given
    @racket[user].
    
    Finds the root directory with the given name, invokes
    @method[server-filesystem% make-context] to create a new access
    control context for the given @racket[user] and finally invokes
     attach] on the root directory to create
    the actual file handle to return.

  }

  @defmethod[#:mode public-final
             (mount [mountpoint path-string?]) any/c]{

    Mounts the default root directory with the name @racket[""] offered
    by this server in the local filesystem at the path @racket[mountpoint].
    
    Returns whether the external command called to perform the mount
    was successful.

  }

  @defmethod[#:mode public-final
             (unmount) any/c]{

    Unmounts the default root directory in the local filesystem.
    
    Returns whether the root was previously mounted and is now
    successfully unmounted.

  }

  @defmethod[#:mode override
             (clunk) void?]{

    Refine this method with @racket[augment].

    Unmounts the filesystem's default root if it is mounted and drops
    all connections to the whole filesystem.

  }

}

@section[#:tag "server/handle"]{Files and Handles}

@defmodule[(planet "handle.rkt" ("murphy" "9p.plt" 2 0) "server")]{

  Implementations of the @racket[file-handle<%>] and
  @racket[directory-handle<%>] interfaces for the server side.

}

@defclass[server-file-handle% object% (file-handle<%>)]{

  Server side container for an access context and an actual object
  representing a file.
  
  @defconstructor[([file (is-a?/c server-file%)]
                   [context (is-a?/c server-context%)]
                   [current-i/o-state any/c #f])]{}

  @defmethod[#:mode public-final
             (->file) (is-a?/c server-file%)]{

    Unwraps the file accessed by this handle or raises an exception
    if the handle has already been invalidated.

  }

  @defmethod[#:mode public-final
             (->context) (is-a?/c server-context%)]{

    Unwraps the access control context contained in this handle.

  }

  @defmethod[#:mode override-final
             (walk [name string?] ...) (is-a?/c server-file-handle%)]{

    Moves from the file represented by this handle to a different
    file and returns a file handle for the target.
    
    Delegates its work to @method[server-file-handle% walk-self] for
    an empty walk, @method[server-file-handle% walk-parent] for a walk
    to @racket[".."] or @method[server-file-handle% walk-child] for a
    walk to the name of some child.
    
    Walks of multiple steps are automatically broken down into one
    step per method call.

  }

  @defmethod[#:mode public-final
             (walk-self) (is-a?/c server-file-handle%)]{

    Creates another handle for the file represented by this handle.
    
    Uses the @method[server-file% attach] method of the underlying file
    to obtain a new handle.

  }

  @defmethod[#:mode public-final
             (walk-parent) (is-a?/c server-directory-handle%)]{

    Moves from the file represented by this handle to its parent
    directory and returns a handle for that directory.
    
    Uses the @method[server-file<%> parent] method of the underlying file
    to find the parent directory and invokes @method[server-file% attach]
    on it to obtain a new handle.

  }

  @defmethod[#:mode public
             (walk-child [name string?]) (is-a?/c server-file-handle%)]{

    Moves from the file represented by this handle to a child and returns
    a handle for that child.
    
    The default implementation just raises a filesystem exception with the
    message @racket[ENOTDIR].

  }

  @defmethod[#:mode override-final
             (read-stat) stat?]{

    Obtains the directory entry information for the file.
    
    Invokes @method[server-file<%> read-stat] on the underlying file.

  }

  @defmethod[#:mode override-final
             (write-stat [stat stat?]) void?]{

    Changes the directory entry information for the file.
    
    Verifies the validity of the @racket[stat] argument and checks
    permissions by invoking @method[server-context% can-touch?] on the
    underlying access control context. If everything is in order,
    @method[server-file<%> write-stat] is invoked on the underlying file.
    
    The method may raise a filesystem exception with the message
    @racket[EINVAL] or @racket[EACCESS] if the argument validity checks
    or the access permission checks fail.

  }

  @defmethod[#:mode public-final
             (i/o-state) any/c]{

    Retrieves the current I/O state of the handle. The file is open
    as viewed from this handle iff the return value is not @racket[#f].

  }

  @defmethod[#:mode override-final
             (open [mode natural-number/c]) natural-number/c]{

    Opens the file for reading or writing data and returns the maximum
    I/O unit size.
    
    Checks access permissions by invoking @method[server-context% can-access?]
    on the underlying access control context. If everything is in order,
    @method[server-file<%> open] is invoked on the underlying file and
    the returned I/O state is stored in the handle.
    
    The method may raise a filesystem exception with the message
    @racket[EACCESS] if the access permission check fails.

  }

  @defmethod[#:mode override-final
             (read [size natural-number/c] [offset natural-number/c])
             (or/c bytes? eof-object?)]{

    Reads data from the file after it has been opened for reading.
    
    Invokes @method[server-file<%> read] on the underlying file if an
    I/O state is associated with the handle.
    
    The method may raise a filesystem exception with the message
    @racket[ENOTCONN] if the handle has no associated I/O state.

  }

  @defmethod[#:mode override-final
             (write [data bytes?] [offset natural-number/c])
             natural-number/c]{

    Writes data to the file after it has been opened for writing.
    
    Invokes @method[server-file<%> write] on the underlying file if an
    I/O state is associated with the handle.
    
    The method may raise a filesystem exception with the message
    @racket[ENOTCONN] if the handle has no associated I/O state.

  }

  @defmethod[#:mode override
             (clunk) void?]{

    Refine this method with @racket[augment]. A refinement will also
    be called if the file is removed using @method[server-file-handle% remove].
    
    Closes the file if it is open and invalidates the file handle.
    
    Invokes @method[server-file<%> clunk] on the underlying file if an
    I/O state is associated with the handle.

  }

  @defmethod[#:mode override-final
             (remove) void?]{

    Closes the file if it is open, removes it from the storage device
    and invalidates the file handle.
    
    Invokes @method[server-file<%> remove] on the underlying file.

  }

}

@defclass[server-directory-handle% server-file-handle% (directory-handle<%>)]{

  Server side container for an access context and an actual object
  representing a directory.
  
  @defmethod[#:mode override-final
             (walk-child [name string?]) (is-a?/c server-file-handle%)]{

    Moves from the file represented by this handle to a child and returns
    a handle for that child.
    
    Checks execute permissions by invoking @method[server-context% can-access?]
    on the underlying access control context. If everything is in order,
    @method[server-directory<%> child] is invoked on the underlying directory
    to obtain the child and @method[server-file% attach] is used on the
    child to produce a file handle to be returned.
    
    The method may raise a filesystem exception with the message
    @racket[EACCESS] if the access permission check fails.

  }

  @defmethod[#:mode override-final
             (in-entries) sequence?]{

    Returns an iterable sequence of directory entries.
    
    Checks read permissions by invoking @method[server-context% can-access?]
    on the underlying access control context. If everything is in order,
    @method[server-directory<%> in-entries] is invoked on the underlying
    directory.
    
    The method may raise a filesystem exception with the message
    @racket[EACCESS] if the access permission check fails.

  }

  @defmethod[#:mode override-final
             (create [name string?] [perm natural-number/c] [mode natural-number/c])
             (values (is-a?/c server-file-handle%) natural-number/c)]{

    Creates a new entry in the directory.
    
    Checks write permissions by invoking @method[server-context% can-access?]
    on the underlying access control context. If everything is in order,
    @method[server-directory<%> create] is invoked on the underlying directory
    to obtain a new child and @method[server-file% attach] is used on the
    child to produce a file handle and I/O unit size to be returned.
    
    The method may raise a filesystem exception with the message
    @racket[EACCESS] if the access permission check fails.

  }
  
}

@definterface[server-file<%> ()]{

  The implicit interface of the class @racket[server-file%].

  @defmethod[#:mode public
             (parent) (is-a?/c server-directory%)]{

    Retrieves the parent directory of this file.
    
    The default implementation just raises a filesystem with the message
    @racket[ENOSYS], but this behaviour is rarely useful so you should
    normally override this method or use a mixin like
    @racket[server-file:parent-mixin] to implement it.

  }

  @defmethod[#:mode public
             (read-stat [context (is-a?/c server-context%)]) stat?]{

    Obtains the directory entry information for the file.
    
    The default implementation just raises a filesystem exception with the
    message @racket[ENOSYS].

  }

  @defmethod[#:mode public
             (write-stat [context (is-a?/c server-context%)] [stat stat?]) void?]{

    Changes the directory entry information for a file.
    
    The default implementation just raises a filesystem exception with the
    message @racket[EROFS].

  }

  @defmethod[#:mode public
             (open [context (is-a?/c server-context%)] [mode natural-number/c])
             (values any/c natural-number/c)]{

    Opens the file for I/O operations and returns some object serving as
    the I/O state and the maximum I/O unit size.
    
    The default implementation just returns
    @racket[(values #t (- (max-message-size) 24))].

  }

  @defmethod[#:mode public
             (read [context (is-a?/c server-context%)]
                   [i/o-state any/c]
                   [size natural-number/c]
                   [offset natural-number/c])
             (or/c bytes? eof-object?)]{

    Reads data from the file after it has been opened for reading.
    
    The default implementation constantly returns @racket[eof].

  }

  @defmethod[#:mode public
             (write [context (is-a?/c server-context%)]
                    [i/o-state any/c]
                    [data bytes?]
                    [offset natural-number/c])
             natural-number/c]{

    Writes data to the file after it has been opened for writing.
    
    The default implementation just raises a filesystem exception with
    the message @racket[EROFS].

  }

  @defmethod[#:mode public
             (clunk [context (is-a?/c server-context%)]
                    [i/o-state any/c])
             void?]{

    Disposes of an I/O context when the file is closed.
    
    The default implementation does nothing.

  }

  @defmethod[#:mode public
             (remove [context (is-a?/c server-context%)]) void?]{

    Deletes the file from the storage medium.
    
    The default implementation just raises a filesystem exception with
    the message @racket[EROFS].

  }

}

@defclass[server-file% object% (server-file<%>)]{

  Server side file access object.

  @defmethod[#:mode public
             (attach [context (is-a?/c server-context%)] [mode (or/c natural-number/c #f) #f])
             (if mode
                 (values (is-a?/c server-file-handle%) natural-number/c)
                 server-file-handle%)]{

    Creates a new file handle connected to this file. The behaviour of the
    method depends on the value of the @racket[mode] argument: @itemlist[

      @item{

        If the @racket[mode] is a natural number, the file is immediately
        opened for I/O operations with this mode and an I/O state is
        associated with the new file handle.
        
        The method returns the new file handle and the I/O unit size in
        this case.

      }
      
      @item{

        If the @racket[mode] is @racket[#f], the file is not opened for I/O
        operations. The new file handle has no initial I/O state.
        
        The method only returns the new file handle in this case.

      }

    ]
    
    The default implementation creates a new instance of
    @racket[server-file-handle%] referencing the file and containing
    the given @racket[context]. If applicable, an I/O state for the
    new handle is created by invoking @method[server-file-handle% open].

  }
  
}

@defclass[server-file-cursor% object% ()]{

  Helper class to implement read and write methods for files.
  
  @defconstructor[([current-offset natural-number/c 0]
                   [with-i/o-unit natural-number/c (- (max-message-size) 24)])]{

    Initializes the cursor object with a starting @racket[offset] in the
    file and an maximum I/O unit size.

  }

  @defmethod*[#:mode public-final
              ([(offset) natural-number/c]
               [(offset [new-offset natural-number/c]) void?])]{

    Retrieves or changes the current file offset of the cursor.
    
    If the cursor has been invalidated the method raises a filesystem
    exception with the message @racket[ENOTCONN].

  }

  @defmethod[#:mode public-final
             (i/o-unit) natural-number/c]{

    Retrieves the maximum I/O unit size of the cursor.

  }

  @defmethod[#:mode pubment
             (read [size natural-number/c] [at-offset natural-number/c (offset)])
             (or/c bytes? eof-object?)]{

    Reads data from the cursor at the specified position. The method
    ensures that refinements are never passed requested sizes larger
    than the maximum I/O unit and that the current position of the cursor
    is correctly tracked based on the incoming @racket[at-offset] value
    and the outgoing result.
    
    The default implementation constantly returns @racket[eof].

  }

  @defmethod[#:mode pubment
             (write [data bytes?] [at-offset natural-number/c (offset)])
             natural-number/c]{

    Writes data to the cursor at the specified position. The method
    ensures that the current position of the cursor is correctly
    tracked based on the incoming @racket[at-offset] value and the
    outgoing result of the refinements.
    
    The default implementation just raises a filesystem exception with
    the message @racket[EROFS].

  }

  @defmethod[#:mode pubment
             (clunk) void?]{

    Invalidates the cursor.

  }

}

@defmixin[server-file:cursor-mixin (server-file<%>) ()]{

  Mixin that delegates all read and write operations from a file class
  to cursor objects.
  
  @defmethod[#:mode public
             (make-cursor [context (is-a?/c server-context%)] [mode natural-number/c])
             (is-a?/c server-file-cursor%)]{

    Creates a new cursor for the file using the given open @racket[mode].

    The default implementation just raises a filesystem exception with the
    message @racket[ENOSYS].

  }

  @defmethod[#:mode override-final
             (open [context (is-a?/c server-context%)] [mode natural-number/c])
             (values (is-a?/c server-file-cursor%) natural-number/c)]{

    Opens the file for I/O operations and returns a cursor obtained
    by a call to @method[server-file:cursor-mixin make-cursor] together
    with its maximum I/O unit size.

  }

  @defmethod[#:mode override-final
             (read [context (is-a?/c server-context%)]
                   [i/o-state (is-a?/c server-file-cursor%)]
                   [size natural-number/c]
                   [offset natural-number/c])
             (or/c bytes? eof-object?)]{

    Delegates the operation to @method[server-file-cursor% read].

  }

  @defmethod[#:mode override-final
             (write [context (is-a?/c server-context%)]
                    [i/o-state (is-a?/c server-file-cursor%)]
                    [data bytes?]
                    [offset natural-number/c])
             natural-number/c]{

    Delegates the operation to @method[server-file-cursor% write].

  }

  @defmethod[#:mode override-final
             (clunk [context (is-a?/c server-context%)]
                    [i/o-state (is-a?/c server-file-cursor%)])
             void?]{

    Delegates the operation to @method[server-file-cursor% clunk].

  }

}

@defclass[server-directory-cursor% server-file-cursor% ()]{

  Generic cursor for directories.
  
  @defconstructor/auto-super[([entries sequence?])]{

    Initializes the cursor object with the given sequence of
    @racket[stat] directory @racket[entries].

  }

  @defmethod[#:mode augment-final
             (read [size natural-number/c] [at-offset natural-number/c])
             (or/c bytes? eof-object?)]{

    Reads packed binary representations of the directory entries from the
    underlying sequence.
    
    Only allows the offset to be the one where the last read operation
    left off or zero to reset the sequence iteration to the beginning.

  }

  @defmethod[#:mode augment-final
             (write [data bytes?] [at-offset natural-number/c])
             natural-number/c]{

    Always fails with a filesystem exception with the message @racket[EISDIR].

  }

  @defmethod[#:mode pubment
             (clunk) void?]{

    Resets the sequence iteration and invalidates the cursor.

  }

}

@definterface[server-directory<%> (server-file<%>)]{

  The implicit interface of the class @racket[server-directory%].
  
  @defmethod[#:mode public
             (child [name string?]) (is-a?/c server-file%)]{

    Finds the file with a given @racket[name] in the directory.

    The default implementation just raises a filesystem exception with
    the message @racket[ENOENT].

  }

  @defmethod[#:mode public
             (in-entries [context (is-a?/c server-context%)]) sequence?]{

    Generates a sequence of @racket[stat] directory entries for this
    directory. The special entries with the names @racket["."] and
    @racket[".."] that are usually visible to the user @emph{should not}
    be generated.
    
    The default implementation returns an empty sequence.

  }

  @defmethod[#:mode public
             (create [context (is-a?/c server-context%)]
                     [name string?]
                     [perm natural-number/c]
                     [mode natural-number/c])
             (is-a?/c server-file%)]{

    Creates a new entry in the directory. The open @racket[mode] is passed
    for completeness' sake in case the flags make a difference in the setup
    of the new file, but the returned file also gets an explicit synthetic
    @method[server-file<%> open] call if this method is invoked through
    @method[server-directory-handle% create].

  }

}

@defclass[server-directory% (server-file:cursor-mixin server-file%) (server-directory<%>)]{

  Server side directory access object.

  @defmethod[#:mode override-final
             (make-cursor [context (is-a?/c server-context%)]
                          [mode natural-number/c])
             (is-a?/c server-directory-cursor%)]{

    Creates a @racket[server-directory-cursor%] based on a sequence
    returned by @method[server-directory<%> in-entries].

  }

  @defmethod[#:mode override
             (attach [context (is-a?/c server-context%)]
                     [mode (or/c natural-number/c #f) #f])
             (if mode
                 (values (is-a?/c server-directory-handle%) 0)
                 (is-a?/c server-directory-handle%))]{

    Creates a new directory handle connected to this directory.
    
    Since directories are never implicitly opened when they are created,
    the @racket[mode] argument is effectively ignored. If it is given,
    the seconds return value of the method is constantly @racket[0]
    in the default implementation.
    
    The default implementation creates a new instance of
    @racket[server-directory-handle%] referencing the file and containing
    the given @racket[context].

  }

}

@section[#:tag "server/util"]{Additional Utilities}

@defmodule[(planet "util.rkt" ("murphy" "9p.plt" 2 0) "server")]{

  Convenience classes to implement file systems in memory and to convert
  byte strings and ports into file cursors.

}

@definterface[server-file/stat<%> (server-file<%>)]{

  Extension of the basic file interface with directory entry information.
  
  Although not canonical for on-disk filesystems, associating the directory
  entry information directly with the file objects is useful for virtual
  in-memory filesystems.

  @defmethod[#:mode public-final
             (on-name-change [key any/c]
                             [listener (or/c (-> string? (or/c string? #f) any) #f)])
             void?]{

    Register or unregister a @racket[listener] to be invoked when the
    directory entry data for the file is changed and its name changes
    or if the file is removed.
    
    The listeners get the old and new names of the file as arguments.
    If the file is removed, the new name is set to @racket[#f].
    
    The @racket[key] is some arbitrary value identifying the listener.
    To remove the callback again, provide the same key and set the listener
    argument to @racket[#f].

  }
  
  @defmethod*[#:mode public-final
              ([(name) string?]
               [(name [new-name string?]) void?])]{

    Retrieve or change the current name of the file.
    
    If the name is changed, registered change listeners are called with
    the old and new names.

  }

  @defmethod[#:mode public
             (content-length) natural-number/c]{

    Retrieve the length of the file's content as it should be reported
    in the directory entry for the file.
    
    The default implementation constantly returns @racket[0].

  }
  
  @defmethod[#:mode public-final
             (touch [context (is-a?/c server-context%)]
                    [modified? any/c]
                    [time natural-number/c (current-seconds)])
             void?]{

    Updates the access time of the file to @racket[time].
    If @racket[modified?] isn't @racket[#f], the modification time
    of the file is also updated. The new value of the @racket[stat-muid]
    field is taken from the username stored in the @racket[context].

  }

  @defmethod[#:mode pubment
             (truncate [context (is-a?/c server-context%)]
                       [time natural-number/c (current-seconds)])
             void?]{

    Empties the file and updates its modification time.
    
    The default implementation just raises a filesystem exception with
    the message @racket[EINVAL].

  }

}

@defmixin[server-file:stat-mixin (server-file<%>) (server-file/stat<%>)]{

  Simple implementation of directory entry information in memory.
  
  @defconstructor/auto-super[([current-name string?]
                              [mode natural-number/c]
                              [uid string? (or (getenv "USER") "nobody")]
                              [gid string? uid]
                              [muid string? uid]
                              [mtime natural-number/c (current-seconds)]
                              [atime natural-number/c mtime]
                              [type natural-number/c 0]
                              [dev natural-number/c 0]
                              [version natural-number/c 0]
                              [path natural-number/c (eq-hash-code this)])]{

    Initializes the file object with values for the fields of the
    directory entry record.

  }
  
  @defmethod[#:mode override-final
             (read-stat [context (is-a?/c server-context%)]) stat?]{

    Synthesizes a @racket[stat] directory entry from the file object's
    fields. The @racket[stat-mode] gets a directory bit added automatically
    if the file is a directory. The @racket[stat-qid] is created from
    the file type bits of the @racket[stat-mode] and the version and path
    values passed to the constructor. The @racket[stat-length] is taken
    from a call to the @method[server-file/stat<%> content-length] method.

  }
  
  @defmethod[#:mode override-final
             (write-stat [context (is-a?/c server-context%)] [stat stat?]) void?]{

    This method takes the following actions to update the directory entry
    information for the file: @itemlist[

      @item{

        If a new name is given in the @racket[stat] argument,
        @method[server-file/stat<%> name] is used to change the name of
        the file and notify any interested listeners.

      }
      
      @item{

        If a new group identifier is given in the @racket[stat] argument,
        the new value is stored.

      }
      
      @item{

        If a new file mode is given in the @racket[stat] argument, the
        new value is stored.

      }
      
      @item{

        If a new length of @racket[0] is given in the @racket[stat]
        argument, the file is emptied with a call to
        @method[server-file/stat<%> truncate].

      }
      
      @item{

        The file's access time is updated. If the file was truncated
        or a modification time was explicitly specified in the
        @racket[stat] argument, the modification time is also updated
        and the user identifier responsible for the last modification
        is taken from the user name contained in the @racket[context]
        argument.

      }

    ]

  }

  @defmethod[#:mode override
             (remove [context (is-a?/c server-context%)]) void?]{

    Refine this method with @racket[augment].

    Unless the refinement method raises an exception, the file's name
    is set to @racket[#f] and all name change listeners are notified
    to indicate that the file is no longer available.

  }

}

@definterface[server-file/parent<%> (server-file<%>)]{

  Extension of the basic file interface with parent tracking.
  
  @defmethod*[#:mode override
              ([(parent) (is-a?/c server-directory%)]
               [(parent [new-parent (or/c (is-a?/c server-directory%) #f)]) void?])]{

    Retrieves or sets the parent directory of the file.
    
    If the parent is set to @racket[#f], reading it causes a filesystem
    exception with the message @racket[ENOSYS].

  }

}

@defmixin[server-file:parent-mixin (server-file<%>) (server-file/parent<%>)]{

  Simple implementation of parent tracking in memory.
  
  @defconstructor/auto-super[([current-parent (or/c (is-a?/c server-directory%) #f)
                                              (and (is-a? this server-directory%) this)])]{

    Initializes the file with its initial parent.
    
    By default directories are used as their own parents, which is
    suitable for filesystem roots. Files don't have a default parent,
    but adding files to @racket[server-hash-directory%] instances also
    sets the parent information, so an explicit constructor argument
    to set the parent is often unnecessary.

  }

}

@defclass[server-bytes-cursor% server-file-cursor% ()]{

  Generic cursor accessing byte strings.
  
  @defconstructor/auto-super[([current-content bytes?]
                              [can-read? any/c #t]
                              [can-write? any/c (not (immutable? current-content))]
                              [can-resize? any/c can-write?]
                              [commit (or/c (-> bytes? any) #f) #f])]{

    Initializes the cursor with content to read or write and a specification
    of its operation mode.

  }

  @defmethod*[#:mode public-final
              ([(content) bytes?]
               [(content [new-content bytes?]) void?])]{

    Retrieves or sets the content currently visible through the cursor.

  }

  @defmethod[#:mode augment-final
             (read [size natural-number/c] [at-offset natural-number/c])
             (or/c bytes? eof-object?)]{

    Reads data from the cursor.
    
    If the @racketid[can-read?] constructor parameter was @racket[#f],
    the method always raises a filesystem exception with the message
    @racket[EPERM].

  }

  @defmethod[#:mode augment-final
             (write [data bytes?] [at-offset natural-number/c])
             natural-number/c]{

    Writes data to the cursor. If the new @racket[data] doesn't fit
    into the existing byte string representing the content at
    @racket[at-offset], a new content buffer is allocated unless
    the @racketid[can-resize?] constructor argument was @racket[#f]
    in which case the data to be written is truncated.
    
    If a write offset completely outside the current content buffer
    is given, the method raises a filesystem exception with the message
    @racket[EFBIG].
    
    If the @racketid[can-write?] constructor parameter was @racket[#f],
    the method always raises a filesystem exception with the message
    @racket[EPERM].

  }

  @defmethod[#:mode pubment
             (clunk) void?]{

    Unless the refinement raises an exception, if a @racketid[commit]
    procedure was specified as a constructor argument it is applied
    to the final byte string content of the cursor.

  }

}

@defclass[server-bytes-file%
          (server-file:cursor-mixin (server-file:parent-mixin (server-file:stat-mixin server-file%)))
          ()]{

  In-memory file with byte string storage.
  
  @defconstructor/auto-super[([current-content bytes? #""])]{

    Initializes the file with its content as a byte string.

  }
  
  @defmethod*[#:mode public-final
              ([(content) bytes?]
               [(content [new-content bytes?]) void?])]{

    Retrieves or sets the content of the file.

  }

  @defmethod[#:mode public-final #;override-final
             (content-length [context (is-a?/c server-context%)])
             natural-number/c]{

    Overrides @method[server-file/stat<%> content-length]. This method is
    final, so it cannot be overridden.
    
    Determines the length of the file's current content in bytes.

  }

  @defmethod[#:mode augment-final
             (truncate [context (is-a?/c server-context%)]
                       [time natural-number/c])
             void?]{

    Resets the file's current content to the empty byte string.

  }

  @defmethod[#:mode override-final
             (make-cursor [context (is-a?/c server-context%)]
                          [mode natural-number/c])
             (is-a?/c server-bytes-cursor%)]{

    Creates an instance of @racket[server-bytes-cursor%] suitable for the
    given open @racket[mode] that accesses the current content of the file.
    
    If the cursor is writable, changes are committed to the file once the
    cursor is clunked.

  }

}

@defclass[server-value-file%
          (server-file:cursor-mixin (server-file:parent-mixin (server-file:stat-mixin server-file%)))
          ()]{

  In-memory file with arbitrary Racket value storage.
  
  @defconstructor/auto-super[([current-content any/c (void)])]{

    Initializes the file with its contents, which can be any value.

  }
  
  @defmethod*[#:mode public-final
              ([(content) any/c]
               [(content [new-content any/c]) void?])]{

    Retrieves or sets the content of the file.

  }

  @defmethod[#:mode public-final
             (content->bytes) bytes?]{

    Converts the content of the file into a byte string using
    @racket[write] to a byte string port.
    
    The result of this method is cached as long as the content value is
    not changed.

  }

  @defmethod[#:mode public-final #;override-final
             (content-length [context (is-a?/c server-context%)])
             natural-number/c]{

    Overrides @method[server-file/stat<%> content-length]. This method is
    final, so it cannot be overridden.
    
    Determines the byte length of the file's content as a byte string.

  }

  @defmethod[#:mode augment-final
             (truncate [context (is-a?/c server-context%)]
                       [time natural-number/c])
             void?]{

    Resets the file's content to @racket[(void)].

  }

  @defmethod[#:mode override-final
             (make-cursor [context (is-a?/c server-context%)]
                          [mode natural-number/c])
             (is-a?/c server-bytes-cursor%)]{

    Creates an instance of @racket[server-bytes-cursor%] suitable for the
    given open @racket[mode] that accesses the current content of the file
    as a byte string.
    
    If the cursor is writable, the changed bytes are @racket[read] back
    in and stored as the new content of the file once the cursor is clunked.
    If the modified contents of the file cannot be read as an S-expression,
    the clunk operation raises a filesystem exception with the message
    @racket[EIO].

  }

}

@defclass[server-port-cursor% server-file-cursor% ()]{

  Generic cursor accessing input and output ports.
  
  @defconstructor/auto-super[([input-port (or/c input-port? #f) #f]
                              [block? any/c #f]
                              [output-port (or/c output-port? #f) #f]
                              [flush? any/c #f]
                              [close? any/c #t]
                              [cleanup (or/c (-> any) #f) #f])]{

    Initializes the cursor with the given input and output ports
    and a specification of its operation mode.

  }

  @defmethod[#:mode public-final
             (->input-port) (or/c input-port? #f)]{

    Unwraps the underlying input port of the cursor.

  }

  @defmethod[#:mode public-final
             (->output-port) (or/c output-port? #f)]{

    Unwraps the underlying output port of the cursor.

  }

  @defmethod[#:mode augment-final
             (read [size natural-number/c] [at-offset natural-number/c])
             (or/c bytes? eof-object?)]{

    Reads data from the underlying input port. @racket[at-offset] must
    either be @racket[0] or the offset where the last read or write
    operation on the cursor left off.
    
    If the constructor parameter @racketid[block?] was not @racket[#f],
    @racket[read-bytes!] is used to read data, otherwise
    @racket[read-bytes-avail!] is used.
    
    If the cursor has no underlying input port, the method raises a
    filesystem exception with the message @racket[EPERM].

  }

  @defmethod[#:mode augment-final
             (write [data bytes?] [at-offset natural-number/c])
             natural-number/c]{

    Writes data to the underlying output port. @racket[at-offset] must
    either be @racket[0] or the offset where the last read or write
    operation on the cursor left off.
    
    If the constructor parameter @racketid[flush?] was not @racket[#f],
    @racket[flush-output] is applied to the output port after the
    @racket[data] has been written to it.
    
    If the cursor has no underlying input port, the method raises a
    filesystem exception with the message @racket[EPERM].

  }

  @defmethod[#:mode pubment
             (clunk) void?]{

    Invokes the refinement method, closes the underlying ports unless the
    constructor parameter @racketid[close?] was @racket[#f] and calls the
    specified cleanup thunk if the constructor parameter @racketid[cleanup]
    was not @racket[#f].
    
    All these operations are performed, even if some of them raise
    exceptions. The last raised exception is re-raised before the method
    returns.

  }

}

@defclass[server-log-file%
          (server-file:parent-mixin (server-file:stat-mixin server-file%))
          ()]{

  In-memory file providing access to Racket log messages.
  
  @defconstructor/auto-super[([logger logger? (current-logger)]
                              [current-log-level log-level? 'info])]{

    Initializes the file with the given @racket[logger] and
    @racket[current-log-level].

  }

  @defmethod[#:mode public-final
             (->logger) logger?]{

    Unwraps the logger associated with the file.

  }
  
  @defmethod*[#:mode public-final
              ([(log-level) log-level?]
               [(log-level [new-level log-level?]) void?])]{

    Retrieves or changes the log level associated with the file.

  }

  @defmethod[#:mode override-final
             (open [context (is-a?/c server-context%)] [mode natural-number/c])
             (values log-receiver? natural-number/c)]{

    Opens the file, creating a log receiver as the I/O state and using
    a maximum I/O unit size of @racket[(- (max-message-size) 24)].

  }

  @defmethod[#:mode override-final
             (read [context (is-a?/c server-context%)]
                   [i/o-state log-receiver?]
                   [size natural-number/c]
                   [offset natural-number/c])
             (or/c bytes? eof-object?)]{

    Reads a single log event from the log receiver. Blocks if
    no message is currently available.
    
    The log event is encoded using @racket[write] to a bytes output port
    and truncated to fit into the I/O unit size.

  }

}

@defclass[server-hash-directory%
          (server-file:parent-mixin (server-file:stat-mixin server-directory%))
          ()]{

  Generic in-memory directory.
  
  @defconstructor/auto-super[([with-children (listof (and/c (is-a?/c server-file/stat<%>)
                                                            (is-a?/c server-file/parent<%>)))
                                             null])]{

    Initializes the directory with a list of files it contains.
    
    Specifying a non-null @racket[with-children] constructor argument is
    functionally equivalent to adding every element of the list as an entry
    to the directory using the @method[server-hash-directory% add-child]
    method.

  }
  
  @defmethod[#:mode override-final
             (child [name string?])
             (and/c (is-a?/c server-file/stat<%>)
                    (is-a?/c server-file/parent<%>))]{

    Retrieves a child from the directory.
    
    The method raises a filesystem exception with the message
    @racket[ENOENT] if no child of the given @racket[name] exists.

  }

  @defmethod[#:mode override-final
             (in-entries [context (is-a?/c server-context%)]) sequence?]{

    Generates a sequence of @racket[stat] directory entries by iterating
    over all files in the directory and asking each of them for their
    directory entry information by invoking @method[server-file<%> read-stat]
    on them.

  }

  @defmethod[#:mode public-final
             (add-child [child (and/c (is-a?/c server-file/stat<%>)
                                      (is-a?/c server-file/parent<%>))])
             void?]{

    Adds a new child to the directory.
    
    Adding a child also registers a name change listener on it that
    automatically updates the hash table mapping child names to children
    for the directory when a file is renamed or removes the child when
    it is deleted.

  }

  @defmethod[#:mode public-final
             (remove-child [child/name (or/c (and/c (is-a?/c server-file/stat<%>)
                                                    (is-a?/c server-file/parent<%>))
                                             string?)])
             void?]{

    Removes an existing child from the directory. @racket[child/name] can
    either be the file to remove or its name.
    
    Removing a child also unregisters the name change listener of the
    directory.

  }

  @defmethod[#:mode override
             (create [context (is-a?/c server-context%)]
                     [name string?]
                     [perm natural-number/c]
                     [mode natural-number/c])
             (and/c (is-a?/c server-file/stat<%>)
                    (is-a?/c server-file/parent<%>))]{

    Creates a new child in the directory. This method can create
    subdirectories of type @racket[server-hash-directory%] and
    regular files of type @racket[server-bytes-file%].

  }

}
