Search code examples
socketsexceptionsmlsmlnjkeyboardinterrupt

Handling Keyboard Interrupts


I've got a minimal TCP server running in the SML/NJREPL, and I'm wondering how to gracefully close the listener socket on a keyboard interrupt. A stripped-down version of the server is

fun sendHello sock = 
    let val res = "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello world!\r\n\r\n"
        val slc = Word8VectorSlice.full (Byte.stringToBytes res)
    in 
      Socket.sendVec (sock, slc);
      Socket.close sock
    end

fun acceptLoop serv =
    let val (s, _) = Socket.accept serv
    in print "Accepted a connection...\n";
       sendHello s;
       acceptLoop serv
    end

fun serve port =
    let val s = INetSock.TCP.socket()
    in Socket.Ctl.setREUSEADDR (s, true);
       Socket.bind(s, INetSock.any port);
       Socket.listen(s, 5);
       print "Entering accept loop...\n";
       acceptLoop s
    end

The problem is that if I start this server listening on a port, cancel with a keyboard interrupt, then try to restart on the same port, I get an error.

Standard ML of New Jersey v110.76 [built: Thu Feb 19 00:37:13 2015]
- use "test.sml" ;;
[opening test.sml]
[autoloading]
[library $SMLNJ-BASIS/basis.cm is stable]
[autoloading done]
val sendHello = fn : ('a,Socket.active Socket.stream) Socket.sock -> unit
val acceptLoop = fn : ('a,Socket.passive Socket.stream) Socket.sock -> 'b
val serve = fn : int -> 'a
val it = () : unit
- serve 8181 ;;
stdIn:2.1-2.11 Warning: type vars not generalized because of
   value restriction are instantiated to dummy types (X1,X2,...)
Entering accept loop...
Accepted a connection...
  C-c C-c
Interrupt
- serve 8181 ;;
stdIn:1.2-1.12 Warning: type vars not generalized because of
   value restriction are instantiated to dummy types (X1,X2,...)

uncaught exception SysErr [SysErr: Address already in use [<UNKNOWN>]]
  raised at: <bind.c>
- 

So I'd like to be able to close out the listening socket when some error occurs. I see Interrupt in the REPL when I issue a keyboard interrupt, so I assumed that Interrupt is the constructor of the exception I'm expected to catch. However, adding the appropriate handle line to either acceptLoop or serve doesn't seem to do what I want.

fun acceptLoop serv =
    let val (s, _) = Socket.accept serv
    in print "Accepted a connection...\n";
       sendHello s;
       acceptLoop serv
    end
    handle Interrupt => Socket.close serv

fun serve port =
    let val s = INetSock.TCP.socket()
    in Socket.Ctl.setREUSEADDR (s, true);
       Socket.bind(s, INetSock.any port);
       Socket.listen(s, 5);
       print "Entering accept loop...\n";
       acceptLoop s
       handle Interrupt => Socket.close s
    end

(then in REPL)

- use "test.sml" ;;
[opening test.sml]
val sendHello = fn : ('a,Socket.active Socket.stream) Socket.sock -> unit
val acceptLoop = fn : ('a,Socket.passive Socket.stream) Socket.sock -> 'b
val serve = fn : int -> 'a
val it = () : unit
- serve 8182 ;;
stdIn:3.1-3.11 Warning: type vars not generalized because of
   value restriction are instantiated to dummy types (X1,X2,...)
Entering accept loop...
Accepted a connection...
  C-c C-c
Interrupt
- serve 8182 ;;
stdIn:1.2-1.12 Warning: type vars not generalized because of
   value restriction are instantiated to dummy types (X1,X2,...)

uncaught exception SysErr [SysErr: Address already in use [<UNKNOWN>]]
  raised at: <bind.c>
- 

Doing the same with a variable (handle x => (Socket.close s; raise x)) or wildcard (handle _ => Socket.close s) exception match has the same effect as above.


Solution

  • You've hit a pretty big limitation with Standard ML per se, which is that the standard language does not make any provisions for concurrent programming. And you need concurrency in this particular case.

    Luckily, you're using SML/NJ, which has some extensions that allow concurrency support — continuations.

    In SML/NJ, you can install an interrupt handler and then resume whatever program continuation you want. Here's how your serve function might look like (I'm a beginner myself when it comes to continuations in SML/NJ, so this is more of a hint, rather than "this is how you do it" example):

    fun serve port =
      (*
       * Capture the current continuation, which is basically the next REPL
       * prompt after the server is done accepting requests.
       *)
      SMLofNJ.Cont.callcc (fn serverShutdownCont =>
        let
          val s = INetSock.TCP.socket()
    
          (*
           * The interrupt handler that is called when ^C is pressed.
           * Shuts down the server and returns the continuation that should
           * be resumed next, i.e. `serverShutdownCont`.
           *)
          fun interruptHandler (signal, n, cont) =
            let in
              print "Shutting down server... "
            ; Socket.close s
            ; print "done.\n"
            ; serverShutdownCont
            end
        in
          (* Register the interrupt handler. *)
          Signals.setHandler (Signals.sigINT, Signals.HANDLER interruptHandler);
          Socket.Ctl.setREUSEADDR (s, true);
          Socket.bind(s, INetSock.any port);
          Socket.listen(s, 5);
          print "Entering accept loop...\n";
          acceptLoop s
        end)
    

    A very good resource to find out more about this is Unix System Programming with Standard ML, in which a small web server is developed, so you'll probably find it very useful.

    Another thing you will bump into down the road is concurrency in the accept loop. Right now, your program can only process one HTTP request at a time. If you want to support more at a time, not necessarily in parallel, but at least concurrently (interleaved), then you'll have to look into Concurrent ML (CML), which is a concurrent extension to Standard ML, implemented as a library on top of the continuations that SML/NJ provides. CML is shipped with SML/NJ.

    A very good tutorial on CML, written by the library's author, John Reppy, is Concurrent Programming in ML. I've recently worked through the first part of the book and it's really thoroughly explained.