on this page

Text selection
Texts blocks
Receive Parsing
Server management
User data

Remote Logger - swapping text between peers

This is a pair of related demo modules for Oberon Workstation 1.3.2+ (with internet connectivity): "MODULE LogServ;" and "MODULE LogClient;". Both modules are contained in a single source file "RemoteLog.Mod"

RemoteLog has only a single purpose: to send selected texts on one computer, over the internet, to the System.Log viewer of the other computer, and vice versa. Both modules - after having established the connection -, will send their selected text when their command <module>.SendSel is clicked.

This can be used as a 2-party "chat" program, but the chat messages can be arbitrary large, so entire (text) viewer contents may be sent, while the styles such as bold or italic are preserved.
Naturally, to be useful, the modules should be installed on 2 different computers with an internet connection, but can also be both run on a single computer, for trying out their communication behavior.

To use Remote Logger

  1. determine the internet address of the computer that will act as the server.
    • both server and client are on the same machine:

      Use "localhost"

    • both machines are on the same local network:

      In this case, you can on your server Mac look up the address in System Preferences > Sharing > "<computer name>.local"

    • working via the public net:

      If you communicate over the public net, you need the public address of your router or its DNS hostname. Take also care of the following:

      • Your router should be configured to forward the server port that you intend to use (e.g. 20000).
      • You may need to adjust firewall settings on your Mac.
      • In case you do not have a static IP address: for reliable long term connecting to a (unattended) server you may need a "dynamic DNS" service, or another way to know the possibly changing public IP of your server computer.

    Note that if your server is exposed to the public net, "strangers" could stumble upon your LogServ. See the notes in the tutorial below at Security.

  2. in LogClient, edit CONST ServAddr to the address of step 1
  3. if needed, edit in both LogClient and LogServ the port that you want to use (must be the same in both modules)
  4. compile both modules (or only the one you need on each computer) from RemoteLog.Mod using ORP.Compile @
  5. on the server, run LogServ.MkService
  6. on the client, run LogClient.Connect
  7. if no connection error occured, you can now - on both sides - use the SendSel command, after selecting a text
  8. to disconnect the client, use LogClient.Disconnect After this, you may reconnect again, as long as the server is not stopped.
  9. to stop the server, use LogServ.StopService This will disconnect the client and not allow it to reconnect

About text selections

To make a large Text selection, one that is larger than the viewer containing the text, you can use a second viewer, as follows:

  1. create a second viewer on the same text using System.Copy
  2. Select the start of the intended selection in the first viewer. (one selected character is sufficient).
  3. scroll the second viewer to contain the end of the intended selection, and select it (again, one selected character is sufficient).

Now the text system effectively has the entire text between start and end selected. Even if there is only inverted text visible for the begin/end text fragments that we dragged the mouse over.
In this way, you can copy, delete (or send with Remote Logger) arbitrary long texts.

Implementation notes, tutorial

Below are some descriptions of Remote Logger as a practical introduction in the working of TCPNet.Mod, and a few related Oberon topics that you maybe did not encounter before.

Text blocks in files

From studying the Texts module of the Oberon system we can learn about two interesting procedures: Texts.Load() and Textst.Store(). In the Oberon system itself, these are not used outside the Texts module, but are nevertheless exported, suggesting fitness for wider use.
Especially if we imagine programs that will store/retrieve texts as just parts of a file that might also contain other data, Load() and Store() are an ideal "system blessed" matched pair for text (de)serialization.

A natural further use of this text block serializing pair is in combination with the communication files of TCPNet, to send and receive texts embedded in a stream of bytes.

Being convinced that Prof. Wirth intended the stand-alone usage of Load/Store, we felt entitled to solve a current problem in the Load procedure that only emerges when it is used outside the Texts module: The current Load procedure does not completely initialize its Text argument, as the "cache" fields of the Texts.TextDesc record are left un-initialized.
Therefore we corrected the problem in Texts.Mod and include it as part of Oberon Workstation 1.3.2 . This has no further impact on the Project Oberon 13 system and existing software.

Receiving and parsing messages in the network handler

An important part of the "protocol" of RemoteLog is the definition of the data to be transmitted. Thanks to RemoteLog's basicness, we need only to send one kind of data structure and only one of such an item per SendSel() user action. Therefore the receiving end of the TCP stream will need to keep track of the boundaries of these message items in the byte stream to parse each item. We do need however to allow items of variable length, because our to-be-sent text selections can be of arbitray length.

There are a few details to consider when deciding what kind of data structure and parsing method to use.

A serialized Text block on file, as implemented by Texts.Load/Texts.Store has a structure which reveals the length of the block in 2 steps. First, we read the INTEGER at the start, which yields the file position where the "content" ascii bytes start. The content is preceded by another INTEGER that yields the length of the content part, and thus makes the total text block length known. Our receiving parser only needs to extract those 2 integers to learn the end of the block (= start of next block).

Other data in the block does not need to be read by the receiving parser, because the text system will access it when needed. To keep track of where we are during successive callbacks, we distinguish 3 states within a message item in the parsing process (state 3 is the same as state 0 of the next message.):

condition for
next state
0initial, start msgmsg startfile len >= msg start + 4
1content start knownmsg start + 4file len >= content start
2content size knowncontent startfile len >= content start + content size
0'initial, start msg'msg' startfile len >= msg' start + 4

Note that we don't need a separate variable to encode the state, since the relative position of the rider contains that information already.
During each subsequent handler call, the parser tries to make as many as possible state steps, depending on the "condition for next state". This can mean anyting from zero to arbitrary many steps. Each time a step is possible in state 2 it means we have a complete message and can use Texts.Load to extract the text and display it in Oberon.Log.

The implementation is fairly simple using the WHILE DO ELSIF.. construct, which saves us a statement nesting and an (elaborate) explicit loop exit condition. The receiver code fragment in the server then becomes (same as in clients CHandler):

PROCEDURE SHandler(c: N.TCPConn; stat: SET): INTEGER;
BEGIN data := c.context(ConnData);
IF N.newrx IN stat THEN flen := Files.Length(N.InFile(c));
	WHILE (Files.Pos(data.inR) = data.msgStart) & (flen >= data.msgStart + 4 ) DO 
		Files.ReadInt(data.inR, data.msgCont)
	ELSIF (Files.Pos(data.inR) = data.msgStart + 4) & (flen >= data.msgCont) DO
		Files.Set(data.inR, N.InFile(c), data.msgCont - 4);
		Files.ReadInt(data.inR, clen); data.msgEnd := data.msgCont + clen;
	ELSIF (Files.Pos(data.inR) = data.msgCont) & (flen >= data.msgEnd) DO
		Files.Set(data.inR, N.InFile(c), data.msgStart);
		NEW(T); Texts.Load(data.inR, T); 
		Files.Set(data.inR, N.InFile(c), data.msgEnd);
		Texts.Delete(T, 0, T.len, B); Texts.Append(Oberon.Log, B);
		data.msgStart := data.msgEnd;

A few more notes:

Managing the server

In Remote Logger, we aim to allow precisely one connection to accomodate a single peer computer (a friend, or an unattended server) to exchange texts, or just to log text in one direction. This means initially to set up one server slot, and to set up a new listening slot whenever we are done with (Close) that connection.

When implementing this straightforward in LogServ.Mod this would lead to brief moments where no server slot is available, namely right after closing the current connection. However when there are zero listening slots for a given port, TCPNet.Mod will immediately remove the listening socket which results in a holdoff time (approx 30 sec.) imposed by macOS for the socket to be re-created. This is undesirable in our case, since we typically want to continue right away with a new listening slot as soon as the old one is closed.

The remedy is to take care of always having at least one server slot in the connection administration of TCPNet, by first creating a new slot, and only then close the old connection that we are done with. The second line below shows how we create a new slot before closing the old one. Nothing spectacular is revealed here, but this code ensures that no extra listening slots could be created (by using MkService on the UI) next to an existing still connected client.

PROCEDURE SHandler(c: N.TCPConn; stat: SET): INTEGER;
IF N.disconn IN stat THEN conn := NIL; MkService0; N.Close(c);	

What happens in the above handler line:

  1. module variable (current connection) conn is set to NIL. Nothing happens yet to the communication setup, the current connection (with disconn flag set) is still part of TCPNet's administration.
  2. call to MkService0 is allowed to create a new listening connection since conn = NIL; It creates a new listening connection and assigns it to conn. Note that the current connection (with disconn flag set) is still part of TCPNet's administration. At this point there are 2 connections in the TCPNet administration: a new unconnected one with status idle, and one with status disconn. There is still a reference to the disconnected slot in parameter c of SHandler
  3. Close(c). the old connection is removed from the TCPNet.Mod administration. Upon completion of the SHandler procedure, there are no references anymore to the old connection record, and subsequently its files and context data are garbage collectable.

In case of a regular error (rather than just a disconnect) in our connection, indicated by flags limrx or error, we have chosen in this program to stop the server: so in this case it is appropriate to just close the error connection, and let the listening socket on our Mac disappear. The server can now be only manually started again by a LogServ.MkService command, after waiting approx. 30 seconds.

ELSIF {N.limrx, N.error} * stat # {} THEN N.Close(c); conn := NIL;


When Oberon creates connections on the public internet, the system becomes vulnerable to a new class of risks, caused by software errors or nefarious actions beyond our control. No longer is our Oberon machine protected under a valid assumption of friendly cooperative code and pristine data, where usually only small accidents occur by the owner's own programming mistakes. The situation is exacerbated further if we put up a server accepting connections from anywhere: in that case we do not even know - let alone control - the visitor that attempts to communicate with us.

As an example, suppose we have set up LogServ on port 20000, and forwarded this port on our router. Further suppose that some computer on the internet is probing for responding ports, including 20000, and our IP address happens to be in their "list" of peers.
When they connect to LogServ, they are likely not aware of- and not compliant with our protocol, and might assume or try something completely different.

Any stream of bytes sent to LogServ will be interpreted as a sequence of Oberon Text blocks. So what happens if an unknown arbitrary stream of bytes that is not an Oberon Text is received?
It does not take long to determine from analyzing Texts.Load() and our message parser, to conclude that several things could happen: printing garbage in the Log, a TRAP for array index out of bounds, heap overflow, or even causing corruption in the text system..
These all are still manageable, but imagine that we enhanced Remote Logger with features like writing/reading files on our system! A determined malicious client - especially one knowing our software - could possibly craft special data content that changed our system, or copied contents off our Oberon directory.

The example Remote Logger program has no "defensive" features. You can however be sure that Oberon Workstation - by its construction and by Apple's sandboxing provisions - cannot be used to access files outside the current Oberon directory, so any damage is limited to that single directory. The Oberon system sofware can always be reset with "Files > Restore System".

Before this program is used regularly over the public net, we recommend that it should be "hardened" to deal with random peers, random content, and non-compliant protocols. For instance, code could be developed and integrated with the receiver parser that checks if a received byte-sequence is a valid serialized Text block of limited size.

More general, the following strategies might help to develop and safely run elaborate Oberon internet programs:

Connection user data

In typical cases, each connection created in the TCPNet module performs its communication independent of other connections. The associated (user) data and control information (like communication state) therefore should be isolated from other connections, and easily accessible in the handler procedure. The central data type of TCPConn: the connection descriptor TCPConnDesc for this purpose contains a pointer field: context that points to an empty record. The context record can now be extended by each communication program to declare its private connection administration.
This arrangement effectively extends connection records into comprehensive "instances" with the following advantages:

The context data declaration in LogClient and LogServ present an example, where the often needed file riders and the state of message parsing is kept. Note that the riders could also have been declared as local variables of the handler, but this then would require them to be opened with Files.Set each time, and their position would need to be kept in the communication record anyway.

ConnDataDesc = RECORD (N.ContextDesc) 
		outR, inR: Files.Rider; msgStart, 
		msgCont, msgEnd:INTEGER 
ConnData = POINTER TO ConnDataDesc;