#
# 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.