The Main Layer

This layer contains the main function, command line parsing and the server startup code.

The Main Module

This is a conventional main file with command line processing. Here is the usage function to show the command line options.

fun usage () = (
    print "Usage: swerve [options]\n";
    print "Options:\n";
    print "  -f file     : (required) specify the server configuration file\n";
    print "  -h          : print this help message\n";
    print "  -T id       : enable test messages according to the id\n";
    print "  -D level    : set the logging level to Debug immediately\n";
    ()
    )

The configuration file must be provided and it is immediately parsed. Any errors are written to stdout during the parsing.

The main function starts up CML and jumps to the command line processing. I have to set up the multicast channels in the signal manager before the configuration file is parsed. Otherwise the file I/O handling with finalisation, which depends on the garbage collector signal, will hang when attaching to the signal.

fun main(arg0, argv) =
let
    fun run() =
    (
        SignalMgr.init();   (* required by OpenMgr *)
        process_args argv
    )
in
    RunCML.doit(run, NONE)
end

val _ = SMLofNJ.exportFn("swerve", main)

After the command line options have been processed comes the run function.

fun run() =
let
in
    TraceCML.setUncaughtFn fatal_exception; 
    (* TraceCML.setTraceFile TraceCML.TraceToErr; *)

    StartUp.startup();

    (*  Make CML run our finish() function at every shutdown. *)
    ignore(RunCML.addCleaner("finish", [RunCML.AtShutdown], 
                    fn _ => StartUp.finish()));

    Listener.run();
    success()               (* shouldn't get here *)
end

The two main-line steps are to run the startup code and then start the listener. I also arrange for the StartUp.finish function to be run when CML is shutdown. CML has a system of "cleaner" functions that can be run at various points. The AtShutdown point ensures that the cleaner will be run whether the server exits normally or fails on a fatal exception. The only way the cleaner won't run is a crash of the run-time. More information on cleaners can be found in the CML source code in the file core-cml/cml-cleanup-sig.sml.

The run function never returns normally. Instead the server is shutdown by calling the RunCML.shutdown function. This is done through either of the success or fail functions in the Common module. If there is an exception out of the run function then it will be caught in the process_args function which will call fail.

The run function also contains some commented-out debugging code which uses the TraceCML module. I used this during debugging to trace the termination of threads. For an example see the HTTP_1_0 module. Uncaught exceptions in a thread are reported via the fatal_exception function (not shown here).

The Startup Module

Here is the startup function.

fun startup() =
let
in
    MyProfile.start();

    if Cfg.haveServerConfig()
    then
        ()
    else
    (
        Log.error ["The server configuration has not been specified."];
        raise FatalX
    );

    (*  Give up if there have been errors already. *)
    if Log.numErrors() > 0 then raise FatalX else ();

    (*  The configuration code checks that all of the files and
        directories exist.
    *)
    setuid();
    create_lock();

    (*  Give up again. *)
    if Log.numErrors() > 0 then raise FatalX else ();

    ()
end

It checks that the configuration has been successfully read. If the configuration file was not specified or if there were any errors while processing the configuration then the server will exit with a fatal error. The FatalX exception is defined in the Common module and caught as explained above.

Next the startup function sets the user and group ids if configured and creates the lock and pid files. If there are any more errors from doing this then it is also a fatal error. The Startup.finish function, called at shutdown time, removes the lock and pid files. I'll skip the code for setting the ids and show the locking functions.

and create_lock() =
let
    val Cfg.ServerConfig {var_dir, ...} = Cfg.getServerConfig()
    val lock_file = Files.appendFile var_dir "lock"
    val pid_file  = Files.appendFile var_dir "pid"
in
    Log.inform Log.Debug (fn() => TF.L ["Creating lock file ", lock_file]);

    if FileIO.exclCreate lock_file
    then
        let
            val strm = TextIO.openOut pid_file
            val pid = Posix.ProcEnv.getpid()
            val w   = Posix.Process.pidToWord pid
        in
            TextIO.output(strm, SysWord.fmt StringCvt.DEC w);
            TextIO.output(strm, "\n");
            TextIO.closeOut(strm)
        end
        handle x => (Log.logExn x; raise x)
    else
    (
        Log.error ["Another server is already running."];
        raise FatalX
    )
end
handle _ => raise FatalX



and remove_lock() =
let
    val Cfg.ServerConfig {var_dir, ...} = Cfg.getServerConfig()
    val lock_file = Files.appendFile var_dir "lock"
    val pid_file  = Files.appendFile var_dir "pid"
in
    FileIO.removeFile pid_file;
    FileIO.removeFile lock_file
end

Here is the FileIO.exclCreate function.

fun exclCreate file =
(
    IO.close(FS.createf(file, FS.O_WRONLY, FS.O.excl,
               FS.S.flags[FS.S.irusr, FS.S.iwusr]));
    true
)
handle
  x as OS.SysErr (_, eopt) =>
    (if isSome eopt andalso valOf eopt = Posix.Error.exist
     then 
        false       (* failed to exclusively create *)
     else
        (Log.logExnArg file x; raise x) (* failed with error *)
    )

| x => (Log.logExnArg file x; raise x)

I've settled for creating a lock file with the Posix.FileSys.createf function using the excl flag. In UNIX terms this means using the open system call with the O_CREAT, O_WRONLY and O_EXCL flags and mode 0600. This will work fine as long as the directory containing the lock file is not mounted via NFS. I've made it a requirement in the server configuration that this be so.

I can check for the EEXIST errno code by catching the OS.SysErr exception. It just so happens that the OS.syserror type it contains is the same as the Posix.Error.syserror type and the Posix.Error module contains example values for each error code.

Any other error while creating the lock file will be logged and propagated as an exception.

Once the lock file is created I can write the process' pid into a file straightforwardly. The only tricky bit is tracking down the right type conversion functions. The SML basis documentation doesn't make it explicit that the Posix.ProcEnv.pid type is the same as the Posix.Process.pid type.