Source file genserve.icn
#
# genserve.icn - Generic server for line-oriented text-based protocols.
#
# Authors: Jafar Al-Gharaibeh and Clinton Jeffery
#
# Based on server.icn from the CVE project.
#
# Date: 11/07/2011
#

$define SERVER_TIMEOUT 5
$define DEFAULT_PORT 8080

$include "posix.icn"

class SocketDriver(
	 socket,
	 user,		# Associated user object, or &null if none

	 dtype,		# one of "listen", "admin", or "user"

	 pending_output,# string containing generated output not yet sent
			# across the socket barrier.

	 pending_input,	# unhandled input (typically from incomplete lines).

	 partition,	# A string that indicates a logical break in the
			# inputs. "\n" for example. This could be extended
			# to be a list of strings.

	 Error		# Indicates a socket failure if set
	 )

   ##
   # returns the pending input on socket. If partition is set, the input
   # is broken into parts based on partition and generated one by one.
   # If Error is set then the socket is invalid and can't be trusted anymore.
   #
   method read_socket()
      local buffer, buffer2
      if buffer2 := pending_input || ready( socket ) then {
	 pending_input := ""
	 if /partition then # no partitions, return everything
	    return buffer2
	   
	 buffer2 ? {
	    while buffer := tab(find(partition)) do {
	       move(*partition)
	       pending_input := &subject[&pos:0]
	       suspend buffer
	       }
	    if *(buffer := tab(0)) > 0 then
	       pending_input := buffer
	    } # buffer2 ? ...
	 } 
      else
	 Error := "Failure in reading input from socket: " || image(socket)
   end
    
   method write_socket(s)
      pending_output ||:= s
      #writes(socket, s)
   end
    
   # 
   # flushes any pending output on the socket
   #
   method flush()
      if *(\pending_output) > 0 then {
	 writes(socket, pending_output)
	 pending_output := ""
	 }
   end
    
initially
   /pending_input := ""
   /pending_output := ""
   /parition := "\n"
end

#
#  this should evolve to a good logger and should be moved to a separate file
#
class LogHandler()
   method logit(L[])
      local msg, s
      msg:=""
      every s := !L do
	 msg ||:= s
      
      write(&clock," ",msg)
   end
end

#
#  A basic server class, ideas from cved
#
class BasicServer(
	 port,		# one port for now. Some future servers may need to
			# listen to several ports.

	 server_name,	# text name of the server. Default: Unicon Server

	 Tsocket_drivers, # socket to SocketDriver objects
	 socket_list,
	 logger,	# Logger object
	 fileHandler	# fileTransfer object ? to be added
	 )

   ##
   #  Starts listening on a specific port for TCP requests
   #  open up network to listen for connections until server is shut down
   #  @param p   the port to listen to. Default is the class variable port
   #
   method open_listener_TCP(p)
      local listen_sock
      /p := port
      if not(listen_sock := server_socket(p)) then {
         logger.logit("open_listener_TCP(:",image(port),
		      "): starting server failed because ", sys_errstr(&errno))
         shutdown()
      }
      logger.logit("open_listener_TCP():", server_name, 
		   " started on port ", port)

      add_socket(listen_sock, &null, "listen")
   end
   
   #
   # add sock to the list of open connections
   #
   method add_socket(sock, user, typ)
      Tsocket_drivers[sock] := SocketDriver(sock, user, typ)
      put(socket_list, sock)
   end
   
   ##
   # close connection to sock and remove it with its driver
   #@param sock socket to be removed
   method remove_socket(sock)
      local i
      Tsocket_drivers[sock].pending_output := ""
      delete( Tsocket_drivers, sock  )
   
      every i := 1 to *socket_list do {
	 if socket_list[i] === sock then {
	    delete(socket_list, i)
	    }
	 }
      close( sock )
   end

   ##
   # open a listner socket at port p
   # @param p port to use
   #
   method server_socket(p)
      return open(":" || p, "nl")
   end
   
   method process_input(driver, buffer)
    stop("Error: Server should implement method process_input(driver, buffer)")
   end

   ##
   # run forever
   #
   method run()
      local i, sock, L, buffer, driver, listen_sock

      repeat {
	 L := select( socket_list, SERVER_TIMEOUT*1000)

	 every sock := !L do {
	    driver := Tsocket_drivers[sock]

	    if /driver then {
	       logger.logit("Run(): This should not happen!  _
			  What is the best way to handle this?  _
			  Driver is null; do not know socket:" || image(sock))
	       next  # fail?
	       }

	    case driver.dtype of {
	    "listen": {

	       # If they can't log in, stop listening to them.
	       # login() would have closed the socket already
	       if not login(driver) then
		  remove_socket(sock)
                     
	       # create a net listener socket to replace the last one
	       # note that the last one gets connected to a user in the
	       # lower level code regardless of whether the login works,
	       # and is thus not usable for listening for any new users.
	       # This is a feature of the unicon networking design, and
	       # probably a good one.

	       if listen_sock := server_socket(port) then 
		  add_socket(listen_sock, &null, "listen")
	       else {
		  logger.logit("Run(): can't open a new listener!")
		  # need to do more here
		  }
	       }

	    # Read from connected, "live" sockets
	    "admin" | "user" : {
	       every buffer := driver.read_socket() do {
		  process_input(driver, buffer)
		  }

	       if \driver.Error then {
		  logger.logit ("Run() :", driver.Error)
		  remove_socket(sock)
		  }
	       }

	    default: {
	       write("unknown driver.dtype ", image(driver.dtype))
	       }
               # end default
	    } # end case
	 } # end every sock := !L

	 every driver := !Tsocket_drivers do
	    driver.flush()
      } # end repeat
   end

   #
   # login -- authenticate the connection.
   # Not all servers require "real" login. Those servers that
   # do so need to override this method with proper login
   #
   # @param driver the driver object associated with this login
   method login(driver)
      driver.dtype := "user"
      return
   end
   
   #
   # shutdown: gracefully shuts down the server.
   #
   method shutdown()
      local sock
      # remove sockets
      every sock := key(Tsocket_drivers) do
	 remove_socket( sock )

      logger.logit("shutdown(): ", server_name, " ", server_version())
      exit(0)
   end
   
   method server_version()
      return "0.1"
   end
   
initially
   /server_name := "Unicon Server"
   /port := DEFAULT_PORT
   logger := LogHandler()
   socket_list := []
   Tsocket_drivers := table()
end

#
# SIGINT_handler: handles CTRL-Cs to this process
#
procedure SIGINT_handler(s)
   server.shutdown()
end

global server

$ifdef MAIN

procedure main()
    trap("SIGINT", SIGINT_handler)
    
    server := BasicServer(9000, "Generic Server")
    
    write("opening TCP connection...")
    server.open_listener_TCP()

    write("running the server...")
    server.run()
    write("Good bye")
end
$endif

This page produced by UniDoc on 2021/04/15 @ 23:59:43.