#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%].
}
}