2021-11-07

65: Connection-Aware Possibly Remote Python External UNO Client

<The previous article in this series | The table of contents of this series | The next article in this series>

To incorporate the LibreOffice/OpenOffice functionality into your independent (non-macro) program, possibly a long-standing one like a Web application

Topics


About: UNO (Universal Network Objects)
About: LibreOffice
About: Apache OpenOffice
About: the Python programming language

The table of contents of this article


Starting Context



Target Context


  • The reader will know how to connect his or her external Python program to any UNO server (usually a LibreOffice or Apache OpenOffice instance), which can be in a remote computer, in a connection-aware way.

Orientation


There are an article on how to create a LibreOffice or Apache OpenOffice daemon and an article on how to create a LibreOffice or Apache OpenOffice Microsoft Windows service.

There is an article on optimally using LibreOffice or Apache OpenOffice as a files converter.

There is an article on executing any UNO dispatch command and getting all the information of the execution in Python.

There is an article on creating any UNO component in Python.

There is an article on using an external Python for LibreOffice or OpenOffice.

There will be some articles on creating connection-aware external UNO clients in some other programming languages (Java, C++, and C#).


Main Body

Stage Direction
Here are Special-Student-7, Objector 28A, and Objector 28B in front of a computer.


1: The Clarification of the Goal


Special-Student-7
The goal of this article is to connect our independent Python program to any UNO server, in a scrupulous way.

"UNO server" is usually a LibreOffice or Apache OpenOffice instance, and "independent" means that the program is not any macro inside the LibreOffice or Apache OpenOffice instance.

Objector 28A
"a scrupulous way"? Do you mean that there is something like an unscrupulous way?

Special-Student-7
Sir, the most pervasively promoted way is a lazy way, which I do not particularly call "unscrupulous" though.

I said that we did not take that lazy way here.

Objector 28A
How lazy is "that lazy way"?

Special-Student-7
It presupposes that the UNO server is in the local computer and is oblivious of whether the established connection is still alive after the establishment.

Our scrupulous way can connect to any remote UNO server and can detect any severance of the connection.

Objector 28A
Is that important?

Special-Student-7
We are supposing that the independent program may be a long-standing one like a Web application, and it may be in an application server and the UNO server may be in a remote data server, and the connection may be severed in a reason and the long-standing program will usually want to know if the connection is severed.

Objector 28A
But the connection severance would be detected anyway when a processing failed, because it would fail, right?

Special-Student-7
It would do eventually if the situation was left unattended, but users would not want the situation left unattended until their requests failed.

Objector 28A
Maybe, but there is no guarantee that there will be no casualty anyway, is there?

Special-Student-7
No, no guarantee, but the casualty will want to be informed before he or she inputs a lot of data on the screen, pushes a button, and finds his or her effort futile.

Objector 28A
. . . So, what will the program do, having detected the severance? Does it just warn the users against futility, or reestablish a connection?

Special-Student-7
It can try to reestablish a connection, but the cause of the problem has to be removed first of course, in order for the try to succeed.

For example, if the demise of the UNO server is the cause, a new UNO server may be able to be started automatically, but if someone has pulled a cable off, the cable will have to be replaced, as well as the person will have to be remonstrated with about such a behavior.

Objector 28A
Remonstration is nothing for you to worry about.

Special-Student-7
But I worry about it; the person might do it again otherwise.

Anyway, detecting the connection severances fast and doing it in an orderly way is a good thing.

Objector 28A
What do you mean by "an orderly way"?

Special-Student-7
Severances are centrally detected by an events listener, rather than individually detected by scattered modules.

Objector 28B
When you say ""UNO server" is usually a LibreOffice or Apache OpenOffice instance", is a UNO server not always a LibreOffice or Apache OpenOffice instance?

Special-Student-7
Madam, I said so because a UNO server might not be a LibreOffice or Apache OpenOffice instance.

Objector 28B
Then, what might it be?

Special-Student-7
It might be a UNO server you have created.

Objector 28B
I haven't.

Special-Student-7
I know that most people have not even imagined creating their own UNO servers, but I did not want to exclude the possibility.

In fact, it is rather easy.

But anyway, if you like, please feel free to think that "UNO server" is a LibreOffice or Apache OpenOffice instance, for your purpose.

But please note that the LibreOffice or Apache OpenOffice instance has to be made a UNO server, in order to be a UNO server.

The UNO server can be in a remote computer, but in that case, the UNO server has to use the TCP/IP socket protocol.


2: Some Notes


Special-Student-7
The code introduced here supposes the Python version to be equal to or higher than 3.6.

That is because the code uses the variable type annotations feature.

Objector 28A
Is that feature a necessity for Python UNO?

Special-Student-7
Not at all, although it is an almost absolute necessity for me. Anyway, 3.6 should not be any unreasonably high hurdle nowadays.

About the Python version, you should know that you cannot use an arbitrary version.

Objector 28A
But I can't do static types checking for UNO types anyway, because I don't have the stubs for the UNO modules, can I?

Special-Student-7
Ah, the stubs indispensable at least for this article are included in my archive file introduced later.

Objector 28A
Well . . .

Special-Student-7
And it is supposed that a proper development environment is ready (there is an article for Linux or for Microsoft Windows).


3: The Code


Special-Student-7
Without further ado, this is the code.

theBiasPlanet/unoUtilitiesTests/connectingFromExternalProgramTest1/Test1Test.py

@Python Source Code
from typing import List
from typing import cast
from re import split
import sys
from threading import Condition
from threading import Thread
import time
import uno
from unohelper import Base as UnoBase
from com.sun.star.bridge import BridgeExistsException
from com.sun.star.bridge import XBridge
from com.sun.star.bridge import XBridgeFactory
from com.sun.star.bridge import XInstanceProvider
from com.sun.star.connection import NoConnectException
from com.sun.star.connection import XConnection
from com.sun.star.connection import XConnector
from com.sun.star.frame import XDesktop
from com.sun.star.lang import EventObject
from com.sun.star.lang import XComponent
from com.sun.star.lang import XEventListener
from com.sun.star.uno import XComponentContext
from com.sun.star.uno import XInterface

class Test1Test:
	class InitialUnoObjectsProvider (UnoBase, XInstanceProvider):
		def __init__ (a_this: "Test1Test.InitialUnoObjectsProvider", a_localUnoObjectsContext: XComponentContext) -> None:
			UnoBase.__init__ (a_this)
			a_this.i_localUnoObjectsContext: XComponentContext
			
			a_this.i_localUnoObjectsContext = a_localUnoObjectsContext
		
		def __del__ (a_this: "Test1Test.InitialUnoObjectsProvider") -> None:
			None
		
		def getInstance (a_this: "Test1Test.InitialUnoObjectsProvider", a_initialUnoObjectName: str) -> XInterface:
			if (a_initialUnoObjectName == "theBiasPlanet.UnoObjectsContext"):
				return a_this.i_localUnoObjectsContext
			else:
				return None
	
	class UnoConnectionEventsListener (UnoBase, XEventListener):
		def __init__ (a_this: "Test1Test.UnoConnectionEventsListener") -> None:
			UnoBase.__init__ (a_this)
		
		def __del__ (a_this: "Test1Test.UnoConnectionEventsListener") -> None:
			None
		
		def disposing (a_this: "Test1Test.UnoConnectionEventsListener", a_event: EventObject) -> None:
			sys.stdout.write ("### The UNO connection has been severed.\n")
			sys.stdout.flush ()
			try:
				Test1Test.s_unoConnectionMaintainingThreadsCondition.acquire ()
				Test1Test.s_unoConnectionInformation.clear ()
				Test1Test.s_unoConnectionMaintainingThreadsCondition.notifyAll ()
			except (Exception) as l_exception:
				# Will not happen.
				None
			finally:
				Test1Test.s_unoConnectionMaintainingThreadsCondition.release ()
	
	class UnoConnectionInformation:
		def __init__ (a_this: "Test1Test.UnoConnectionInformation") -> None:
			a_this.i_unoBridge: XBridge
			a_this.i_remoteUnoObjectsContext: XComponentContext
			
			a_this.i_unoBridge = None
			a_this.i_remoteUnoObjectsContext = None
		
		def __del__ (a_this: "Test1Test.UnoConnectionInformation") -> None:
			None
		
		def isEmpty (a_this: "Test1Test.UnoConnectionInformation") -> bool:
			return a_this.i_remoteUnoObjectsContext is None
		
		def getUnoBridge (a_this: "Test1Test.UnoConnectionInformation") -> XBridge:
			return a_this.i_unoBridge
		
		def getRemoteUnoObjectsContext (a_this: "Test1Test.UnoConnectionInformation") -> XComponentContext:
			return a_this.i_remoteUnoObjectsContext
		
		def setUnoBridge (a_this: "Test1Test.UnoConnectionInformation", a_unoBridge: XBridge) -> None:
			a_this.i_unoBridge = a_unoBridge
		
		def setRemoteUnoObjectsContext (a_this: "Test1Test.UnoConnectionInformation", a_remoteUnoObjectsContext: XComponentContext) -> None:
			a_this.i_remoteUnoObjectsContext = a_remoteUnoObjectsContext
		
		def clear (a_this: "Test1Test.UnoConnectionInformation") -> None:
			a_this.i_unoBridge = None
			a_this.i_remoteUnoObjectsContext = None
	
	s_unoConnectionMaintainingThreadsCondition: Condition = Condition ()
	s_unoConnectionUsingThreadsCondition: Condition = Condition ()
	s_unoConnectionInformation: "Test1Test.UnoConnectionInformation"
	
	@staticmethod
	def main (a_arguments: List [str]) -> None:
		l_resultStatus: int = -1
		try:
			if (len (a_arguments) != 2):
				raise Exception ("The arguments have to be these.\nThe argument 1: the server URL like 'socket,host=localhost,port=2002,tcpNoDelay=1;urp;StarOffice.ComponentContext'")
			l_unoServerUrl: str = a_arguments [1]
			l_unoServerUrlTokens: List [str] = split (";", l_unoServerUrl)
			if len (l_unoServerUrlTokens) < 3:
				raise Exception ("The server URL has to have 3 tokens delimited by ';'.")
			l_localUnoObjectsContext: "XComponentContext" = uno.getComponentContext ()
			l_unoBridgesFactory: XBridgeFactory = cast (XBridgeFactory, l_localUnoObjectsContext.getServiceManager ().createInstanceWithContext ("com.sun.star.bridge.BridgeFactory", l_localUnoObjectsContext))
			l_initialUnoObjectsProvider: "Test1Test.InitialUnoObjectsProvider" = Test1Test.InitialUnoObjectsProvider (l_localUnoObjectsContext)
			l_unoConnectionEventsListener: "Test1Test.UnoConnectionEventsListener" = Test1Test.UnoConnectionEventsListener ()
			l_ending: bool = False
			def l_unoConnectingThreadFunction () -> None:
				l_connectionIsEstablished: bool = False
				while True:
					l_connectionIsEstablished = False
					if l_ending:
						break
					try:
						Test1Test.s_unoConnectionMaintainingThreadsCondition.acquire ()
						l_unoConnection: XConnection = None
						try:
							l_unoConnectionConnector: XConnector = cast (XConnector, l_localUnoObjectsContext.getServiceManager ().createInstanceWithContext ("com.sun.star.connection.Connector", l_localUnoObjectsContext))
							l_unoConnection = l_unoConnectionConnector.connect (l_unoServerUrlTokens [0])
						except (NoConnectException) as l_exception:
							raise Exception ("{0:s}: {1:s}".format ("The UNO connection could not be established.", str (l_exception)))
						try:
							Test1Test.s_unoConnectionInformation.setUnoBridge (l_unoBridgesFactory.createBridge ("", l_unoServerUrlTokens [1], l_unoConnection, l_initialUnoObjectsProvider))
						except (BridgeExistsException) as l_exception:
							# This can't happen
							None
						Test1Test.s_unoConnectionInformation.setRemoteUnoObjectsContext (cast (XComponentContext, Test1Test.s_unoConnectionInformation.getUnoBridge ().getInstance (l_unoServerUrlTokens [2])))
						if Test1Test.s_unoConnectionInformation.getRemoteUnoObjectsContext () == None:
							Test1Test.s_unoConnectionInformation.clear ()
							raise Exception ("The remote instance is not provided.")
						cast (XComponent, Test1Test.s_unoConnectionInformation.getUnoBridge ()).addEventListener (l_unoConnectionEventsListener)
						l_connectionIsEstablished = True
						sys.stdout.write ("### A UNO connection has been established.\n")
						sys.stdout.flush ()
					except (Exception) as l_exception:
						sys.stderr.write ("### An error has occurred: \"{0:s}\".\n".format (str (l_exception)))
						sys.stderr.flush ()
					finally:
						Test1Test.s_unoConnectionMaintainingThreadsCondition.release ()
					# This cannot be inside any 's_unoConnectionMaintainingThreadsCondition' lock, because it could cause a dead lock (because there is a reverse locks nest bellow), and this does not have to be inside the above lock, because although the connection might have been severed now, that is inevitable anyway, because the connection can be severed even in the above lock.
					if l_connectionIsEstablished:
						try:
							Test1Test.s_unoConnectionUsingThreadsCondition.acquire ()
							Test1Test.s_unoConnectionUsingThreadsCondition.notifyAll ()
						except (Exception) as l_exception:
							# Will not happen.
							None
						finally:
							Test1Test.s_unoConnectionUsingThreadsCondition.release ()
					try:
						Test1Test.s_unoConnectionMaintainingThreadsCondition.acquire ()
						# This has to be checked, because the connection severance might have been detected and the waking call might have been already dispatched.
						if not Test1Test.s_unoConnectionInformation.isEmpty ():
							# can safely wait, because the possible connection severance has not been detected.
							Test1Test.s_unoConnectionMaintainingThreadsCondition.wait ()
							# Coming here means that the connection has been severed.
					finally:
						Test1Test.s_unoConnectionMaintainingThreadsCondition.release ()
					time.sleep (3)
			l_unoConnectingThread: Thread = Thread (target = l_unoConnectingThreadFunction)
			l_unoConnectingThread.start ()
			
			# Do whatever you want in other daemon threads. Start
			def l_unoWorkingThreadFunction () -> None:
				l_thereWasNoConnection: bool = False
				l_errorHasOccurred: bool = False
				while True:
					try:
						l_thereWasNoConnection = False
						l_errorHasOccurred = False
						Test1Test.s_unoConnectionMaintainingThreadsCondition.acquire ()
						if not Test1Test.s_unoConnectionInformation.isEmpty ():
							sys.stdout.write ("### Doing something Start.\n")
							sys.stdout.flush ()
							l_unoDesktop: XDesktop = cast (XDesktop, Test1Test.s_unoConnectionInformation.getRemoteUnoObjectsContext ().getServiceManager ().createInstanceWithContext ("com.sun.star.frame.Desktop", Test1Test.s_unoConnectionInformation.getRemoteUnoObjectsContext ()))
							sys.stdout.write ("### Doing something End.\n")
							sys.stdout.flush ()
						else:
							l_thereWasNoConnection = True
							sys.stdout.write ("{0:s}".format ("### Warning: there is no UNO connection.\n"))
							sys.stdout.flush ()
					except (Exception) as l_exception:
						l_errorHasOccurred = True
						sys.stderr.write ("### An error has occurred: \"{0:s}\".\n".format (str (l_exception)))
						sys.stderr.flush ()
					finally:
						Test1Test.s_unoConnectionMaintainingThreadsCondition.release ()
					# a new connection may have been established now (because 's_unoConnectionMaintainingThreadsCondition' is not protected here), but there is no problem in checking.
					if l_thereWasNoConnection or l_errorHasOccurred:
						try:
							Test1Test.s_unoConnectionUsingThreadsCondition.acquire ()
							# has to check that there is really no connection, not that just the connection severance is detected, because this thread has to wait in order for the severance to be detected.
							try:
								Test1Test.s_unoConnectionMaintainingThreadsCondition.acquire ()
								if Test1Test.s_unoConnectionInformation.isEmpty ():
									l_thereWasNoConnection = True
								else:
									try:
										Test1Test.s_unoConnectionInformation.getRemoteUnoObjectsContext ().getServiceManager ()
									except (Exception) as l_exception:
										l_thereWasNoConnection = True
							finally:
								Test1Test.s_unoConnectionMaintainingThreadsCondition.release ()
							if l_thereWasNoConnection:
								# a connection might have been established now (because 's_unoConnectionMaintainingThreadsCondition' is not protected here), but can safely wait, because anyway, the waking call is not dispatched yet at least (because 's_unoConnectionUsingThreadsCondition' has been protected since before the confirmation of non-connection).
								Test1Test.s_unoConnectionUsingThreadsCondition.wait ()
						finally:
							Test1Test.s_unoConnectionUsingThreadsCondition.release ()
					time.sleep (3)
			l_unoWorkingThread: Thread = Thread (target = l_unoWorkingThreadFunction)
			l_unoWorkingThread.daemon = True
			l_unoWorkingThread.start ()
			# Do whatever you want in other daemon threads. End
			
			# waiting for a user input to quit
			sys.stdout.write ("### Push 'Enter' to quit.\n")
			sys.stdout.flush ()
			sys.stdin.read (1)
			try:
				Test1Test.s_unoConnectionMaintainingThreadsCondition.acquire ()
				l_ending = True
				if not Test1Test.s_unoConnectionInformation.isEmpty ():
					sys.stdout.write ("### Severing the UNO connection.\n")
					sys.stdout.flush ()
					cast (XComponent, Test1Test.s_unoConnectionInformation.getUnoBridge ()).dispose ()
					Test1Test.s_unoConnectionInformation.clear ()
				else:
					sys.stdout.write ("### No UNO connection is established.\n")
					sys.stdout.flush ()
			except (Exception) as l_exception:
				raise Exception ("{0:s}: {1:s}.".format ("The UNO connection could not be severed", str (l_exception)))
			finally:
				Test1Test.s_unoConnectionMaintainingThreadsCondition.release ()
			l_unoConnectingThread.join ()
			l_resultStatus = 0
		except (Exception) as l_exception:
			sys.stderr.write ("### An error has occurred: \"{0:s}\".\n".format (str (l_exception)))
			sys.stderr.flush ()
		sys.exit (l_resultStatus)

# some static members initialization Start
Test1Test.s_unoConnectionInformation = Test1Test.UnoConnectionInformation ()
# some static members initialization End

if __name__ == "__main__":
	Test1Test.main (sys.argv)

Objector 28B
. . . Well, it is a rather lengthy piece of code; I may prefer the lazy way.

Special-Student-7
You know, the length of the code should not really matter so much for you: it is a chunk of boilerplate code and you can just copy it if you will: copying 1 line and copying 20 lines are not much different as for your labor.

Objector 28B
It's 200 lines!

Special-Student-7
Whether it is 20 lines or 200 lines, your copying labor will not be much different.

Being intimidated by the length of code is just an emotional reaction not supported by any reasonable cause.


4: Executing the Code


Special-Student-7
There are some things you have to do when you execute any Python external UNO client.

1st, you have to include a modules path, namely '%the office product directory%/program' for Linux or '%the office product directory%\program' for Microsoft Windows (in fact, it may have been already set automatically somehow, depending on the environment).

2nd, you have to pass an operating system environment variable, namely 'URE_BOOTSTRAP' with the value of '%the office product directory%/program/fundamentalrc' for Linux or '%the office product directory%/program/fundamental.ini' for Microsoft Windows (in fact, it may be unnecessary, depending on the environment).

In fact, this should execute the code in this Linux.

@bash Source Code
bash -c "export URE_BOOTSTRAP=/usr/lib/libreoffice/program/fundamentalrc; export PYTHONPATH=${PYTHONPATH}:/usr/lib/libreoffice/program; python3 -m theBiasPlanet.unoUtilitiesTests.connectingFromExternalProgramTest1.Test1Test \"socket,host=localhost,port=2002,tcpNoDelay=1;urp;StarOffice.ComponentContext\""

"socket,host=localhost,port=2002,tcpNoDelay=1;urp;StarOffice.ComponentContext" is the address of the UNO server.

Stage Direction
Special-Student-7 opens a terminal in the Linux machine in front of them, and execute the Linux command. "### The UNO connection could not be established." keeps coming every 3 seconds.

Objector 28A
. . . Aren't they errors perchance?

Special-Student-7
They are errors of course.

Stage Direction
Special-Student-7 nods contentedly.

Objector 28A
Why are you so smug about that?

Special-Student-7
I am not particularly smug; it is just of course because no UNO server is up yet.

Let met start up a UNO server.

Stage Direction
Special-Student-7 starts up a LibreOffice instance. "### A UNO connection has been established." appears on the console, and then "### Doing something Start." and "### Doing something End." start coming every 3 seconds.

Objector 28B
Oh, it has connected, it seems.

Special-Student-7
Let me kill the UNO server.

Stage Direction
Special-Student-7 kills the LibreOffice instance with the 'kill' command. "### The UNO connection has been severed." and "### Warning: there is no UNO connection." appear, and then "### The UNO connection could not be established." starts coming again.

Special-Student-7
Note that ending the LibreOffice instance GUI-wise may not really stop the instance, so I have used the 'kill' command.

Anyway, let me restart a LibreOffice instance.

Stage Direction
Special-Student-7 starts up a LibreOffice instance. "### A UNO connection has been established." appears on the console, and then "### Doing something Start." and "### Doing something End." start coming every 3 seconds.

Objector 28B
Oh, but how can I stop the program?

Special-Student-7
Pushing the 'Enter' key on the console will stop the program.

Stage Direction
Special-Student-7 push the 'Enter' key, and the program stops.


5: Understanding the Code


Special-Student-7
Let us understand the code; I said you could copy the code but I did not say you did not need to understand the code.

Objector 28B
Then, the code length matters after all!

Special-Student-7
Well, the issue is what you are comparing it with: compared with opting for the lazy way, just copying without understanding would be no worse, because you would not understand anyway. I am saying that copying and understanding is the best.

Objector 28B
Whatever . . .

Special-Student-7
There are 3 standing threads: the main thread, the connection-establishing thread ("l_unoConnectingThread"), and the working thread ("l_unoWorkingThread"), and 1 sporadically evoked thread: the connection-severance-detecting thread.

1st of all, the program has to do bootstrapping, which means that the program becomes a UNO program.

Note that the program has to become a UNO program in order to connect to a UNO server, because the program connects to the UNO server by means of UNO.

Specifically, "uno.getComponentContext ()" does the bootstrapping and achieves a local UNO objects context.

"local" means that it is of the program, not of the UNO server.

The local UNO environment has its own UNO services manager, which can instantiate some UNO services in the local UNO environment.

Of course, the local UNO environment does not have any UNO component of like spread sheets documents, but has some UNO components that are necessary in order to connect the local UNO environment to any UNO server, and the program instantiates those UNO components via the local UNO services manager.

In fact, the program instantiates the "com.sun.star.connection.Connector" UNO service.

Then, the program calls the "connect" method of the "com.sun.star.connection.Connector" instance and achieve a "com.sun.star.connection.XConnection" instance.

Objector 28A
So, the program has already connected to the UNO server.

Special-Student-7
Well, the method name is "connect", but the program has to establish a bridge to the UNO server, in order to really be connected.

Meanwhile, the program prepares an initial UNO objects provider.

Objector 28B
What the hell is that?

Special-Student-7
The UNO server can achieve some UNO objects from the local program via the initial UNO objects provider when the bridge is established.

Objector 28B
Why does the UNO server have to achieve them?

Special-Student-7
I said "some UNO objects", but usually, it is the local UNO objects context, which may be wanted by the UNO server because it is usually via the local UNO objects context that the local UNO environment is handled.

Objector 28B
"may be" . . .

Special-Student-7
But, actually, the LibreOffice instance does not want it, because it has no intention of handling such UNO clients. So, you do not have to really worry about the initial UNO objects provider if the UNO server is a LibreOffice or Apache OpenOffice instance.

Objector 28B
So . . .

Special-Student-7
Your program does not really have to prepare any initial UNO objects provider, although our program has done so, because it is meant to be scrupulous.

Then, the program instantiates the "com.sun.star.bridge.BridgeFactory" UNO service.

Then, the program creates a UNO bridge by calling the "createBridge" method of the "com.sun.star.bridge.BridgeFactory" instance.

Then, the program achieves a remote UNO objects context by the "getInstance" method of the "com.sun.star.bridge.XBridge" instance.

You know, that remote UNO objects context is the access point to the UNO server, so you will want to keep it.

For your information, the remote UNO objects context can be gotten from the UNO server because the UNO server offers it via its own initial UNO objects provider.

Objector 28B
So, congratulations?

Special-Student-7
But the program does one more thing: registering an events listener into the bridge.

The evens listener has to have implemented the "com.sun.star.lang.XEventListener" UNO interface, which has the "disposing" method, which is called when the connection is severed.

The "addEventListener" method registers the events listener into the UNO bridge.

Objector 28B
So, congratulations!

Special-Student-7
Some words on the multiple threads structure of the program.

The "l_unoConnectingThread" thread is active while there is no connection and tries to establish a connection; when the connection is established, the thread begins to wait indefinitely.

The events listener (which runs in an evoked thread) notifies the "l_unoConnectingThread" thread when the connection is severed, and the "l_unoConnectingThread" thread becomes active again, trying to establish a new connection.

The "l_unoWorkingThread" thread is active while there is a connection and does something; when the connection is severed, the thread begins to wait indefinitely.

Imagine that the working thread is dealing with user requests of a Web application; if a user is unlucky, the user might experience an error, but otherwise, the user would be warned before the user did some onerous labor futilely that the Web application was temporarily inoperable (the "### Warning: there is no UNO connection." message represents that situation).

Objector 28B
Well, there are some lengthy comments in the code.

Special-Student-7
Handling of multiple threads is usually rather delicate, and it is so in this case.

There are some points to be attended.

1st, any dead lock has to be avoided.

In the "l_unoConnectingThread" thread, there is a seemingly tortuous procedure of "s_unoConnectionMaintainingThreadsCondition"'s being locked and then released, and then "s_unoConnectionUsingThreadsCondition"'s being locked and then released, and then "s_unoConnectionMaintainingThreadsCondition"'s being locked and then released, but that is because "s_unoConnectionUsingThreadsCondition" cannot be locked while "s_unoConnectionMaintainingThreadsCondition" is being locked, because that could cause a dead lock.

Objector 28B
Why could that cause a dead lock?

Special-Student-7
The "l_unoWorkingThread" thread has to do lockings in the reverse order, and 2 threads' doing lockings in the reverse orders could cause a dead lock, because the thread 1 could lock an object A; the thread 2 could lock an object B; the thread 1 wanted to lock the object B but could not, because the object B was already locked by the thread 2 while the thread 2 wanted to lock the object A but could not, because the object A was already locked by the thread 1, a dead lock.

Objector 28B
Why does "l_unoWorkingThread" have to do lockings in the reverse order?

Special-Student-7
That is because of the 2nd point to be attended.

Any thread can wait only when it is guaranteed to be properly awaken.

That is because otherwise, the thread could be keep waiting indefinitely.

Objector 28B
Of course, but what does that have to do with "l_unoWorkingThread"'s locking order?

Special-Student-7
In order for the thread to wait on "s_unoConnectionUsingThreadsCondition", "s_unoConnectionUsingThreadsCondition" has to have been locked, but also "s_unoConnectionMaintainingThreadsCondition" has to be locked in order for "s_unoConnectionInformation" to be checked (because "s_unoConnectionMaintainingThreadsCondition" is protecting "s_unoConnectionInformation").

Objector 28B
Why doesn't the thread check "s_unoConnectionInformation" and release "s_unoConnectionMaintainingThreadsCondition", and then lock "s_unoConnectionUsingThreadsCondition"?

Special-Student-7
That is because "s_unoConnectionInformation" might be changed after "s_unoConnectionMaintainingThreadsCondition" is released and before "s_unoConnectionUsingThreadsCondition" is locked, then, the checking would be unreliable.

Objector 28B
. . . Why does "s_unoConnectionInformation" have to be checked in the first place?

Special-Student-7
That is because if the connection is not really severed at the moment of waiting, the thread would not be awaken, because the awaking call would be dispatched when a new connection was established, but as the connection was not severed, there would no reestablishment, so, no awaking call.

Objector 28B
But "s_unoConnectionMaintainingThreadsCondition" has been released before the thread starts waiting anyway, so, a connection might have been already established before the thread starts waiting, I presume.

Special-Student-7
Yes, but as "s_unoConnectionUsingThreadsCondition" is locked, the awaking call cannot have been dispatched yet, which is the point.

Objector 28B
So, the thread is guaranteed to be properly awakened, do you mean?

Special-Student-7
Yes.

Objector 28B
Why can't "s_unoConnectionUsingThreadsCondition" be locked inside the "s_unoConnectionMaintainingThreadsCondition" lock segment?

Special-Student-7
Because then, "s_unoConnectionMaintainingThreadsCondition" would be kept locked, blocking the "l_unoConnectingThread" thread, so, the "l_unoWorkingThread" would wait for a new connection to be established, while blocking the establishment.

Objector 28B
. . . When the "l_unoConnectingThread" thread starts waiting, is the thread guaranteed to be properly awaken?

Special-Student-7
Yes. The connection might have been already severed, but the severance is guaranteed to have not been detected yet (because "s_unoConnectionMaintainingThreadsCondition" is locked), so, the awaking call is guaranteed to have not been dispatched yet.

Objector 28B
So, you are making sure that the thread does not start waiting after the awaking call has been already dispatched . . .

Special-Student-7
Yes. That is the issue, because otherwise, what the waiting thread is expecting would not come.

And the 3rd point to be attended is that any thread has to wait when another thread must run.

Objector 28B
Huh? Is that true? As it is multiple threads, the another thread will run anyway, right?

Special-Student-7
Not necessarily, depending on the run-time environment; multiple threads may not really run simultaneously: a thread may keep running unless it waits.

You may think that the "l_unoWorkingThread" thread does not have to wait, but it has to wait in order for the connection-severance-detecting thread to be guaranteed to run.

Objector 28B
But as "sleep" is there . . .

Special-Student-7
Ah, please suppose that "sleep" is not there: "sleep" is there just because incessant message outputs are a nuisance.

Objector 28B
But if the program was a Web server . . .

Special-Student-7
You are right: if the program was a Web server, it would not be a loop, and the connection-severance-detecting thread would be evoked between user requests.


6: A Workable Sample


Special-Student-7
Here is a workable sample, which automatically makes a save file of whatever file stored locally by the LibreOffice or Apache OpenOffice instance (note that the LibreOffice or Apache OpenOffice instance is supposed to be local in this case).

The main project is 'connectionAwareExternalUnoClients' and how to build the project is described in a previous article.

Objector 28B
"build"? Does a Python project have to be built?

Special-Student-7
Ah, not necessarily has to be built, but "build" here means doing static types checking, creating stubs, and compiling (creating '.pyc' files), which are not particularly necessary though.

The program can be executed like this.

@bash or CMD Source Code
gradle i_executePythonExecutableFileTask -Pi_mainModuleName="theBiasPlanet.connectionAwareExternalUnoClients.programs.ConnectionAwareExternalUnoClientConsoleProgram" -Pi_commandLineArguments="socket,host=localhost,port=2002,tcpNoDelay=1;urp;StarOffice.ComponentContext"

Note that its code is not written as flatly as the above code, but what is being done is the same, stashed in my utility classes like 'theBiasPlanet.unoUtilities.processesHandling.UnoProcessEnvironment.UnoProcessEnvironment', 'theBiasPlanet.unoUtilities.connectionsHandling.UnoConnectionConnector.UnoConnectionConnector', and 'theBiasPlanet.unoUtilities.connection.UnoConnection.UnoConnection'.

Objector 28B
Why is it not written "flatly"?

Special-Student-7
The above code has been written only for the explanation's sake; I usually do not write that way; I usually more appropriately organize things in classes.


References


<The previous article in this series | The table of contents of this series | The next article in this series>