acefael

.. on software

Follow me on GitHub

A Network Service Shell

29 Apr 2016

“You cannot teach a man anything, ..” — Galileo

You must go and play with tcpip_connection and tcpip_connection_stream and see for yourself. So that is what I did. The result is a module and exemplar called “daemon”, available on my github page. I think it can be used to build network services with Smallworld™ and I’ll show you how.

The daemon main loop is as follows:

heavy_thread.new(
  _proc()
    _local sock << tcpip_connection_stream.new(port)
    _loop
      handle_client_in_separate_thread(sock.get())
    _endloop
  _endproc )

A listening socket can be made in Smallworld™ with tcpip_connection_stream.new(port). Calling .get() on it waits for and accepts connections. .get() returns an accepted socket, which is of type tcpip_connection in magik. That connection is best handled in a new thread, because the listening thread should quickly return to accept new connections. This logic is in the run method on my “daemon” exemplar. Client connections are handled as outlined in the following lines:

heavy_thread.new(
  _proc(accepted_socket)
    _loop
      _local service_name <<
        read_service_to_call_from(
	  accepted_socket.input )
      daemon.services[ service_name ].invoke( ... )
    _endloop
  _endproc )

To understand above lines, you need to know how to register services in my network service shell. They are simply put in the global daemon.services like so (yes indeed, no xml configuration):

daemon.services[:list_themes] <<
  _proc(args)
    ...
  _endproc

This means a service can be anything that understands the invoke() protocol. This includes procs. The following block of code implements a service that lists system themes in the Electricity ACE of the Cambridge Database:

_global list_themes_service<<
_proc(args)

  ## args is a property_list with the keys/values
  ## :input - socket input stream
  ## :output - socket output stream
  ## :socket - the socket itself (use only if necessary)

  # this service reads _nothing_ from the client

  _local all_themes << rope.new()
  _local ace_name << :|Electricity|
  _local themes << gis_program_manager.ace(ace_name).themes()
  _for thms _over themes.fast_elements()
  _loop
    # themes have this hierarchy thing going on.
    # we walk the hierarchy to find them all.
    _for thm _over thms.fast_depth_first()
    _loop
      _if thm.is_class_of?( theme )
      _then
        all_themes.add(thm)
      _endif
    _endloop
  _endloop

  # this service writes to the client:
  # number of themes (uint32)
  # for each theme:
  #   number of chars for theme name (uint32)
  #   theme name (num-chars bytes)

  args[:output].put_unsigned_int( all_themes.size )
  _for thm _over all_themes.elements()
  _loop
    args[:output].put_unsigned_int(thm.name.size)
    args[:output].put_ascii_chars(thm.name)
  _endloop
  args[:output].flush()
_endproc
$

# register proc (service) with daemon
daemon.services[:list_themes] <<
  list_themes_service
$

Before or after registering the service call daemon.run() to start my network shell. A client for the list_system_themes service can be found in this gist.

To sum up, each TCP/IP connection is handled in a separate thread and there are no explicitly engineered bounds on the number of threads created. Taking into consideration that the Magik VM on 4.3 is single-threaded, there appears to be no point in having many cuncurrent client connections. In light of the outstanding service provided by one Magik VM I would not place the network shell in a heavy-use multi user environment without a facade, and the use of a proxy that serialises requests seems the architecture of choice.