2019-08-25

25: Creating a Safe, Attentive LibreOffice/OpenOffice Daemon

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

As a files conversion server, etc.. One that doesn't brutally kill the office instance together with the ongoing user requests. With a workable sample

Topics


About: UNO (Universal Network Objects)
About: LibreOffice
About: Apache OpenOffice
About: C++

The table of contents of this article


Starting Context



Target Context


  • The reader will know how to create a safe, attentive C++ LibreOffice or Apache OpenOffice daemon and have a workable sample.

Orientation


There is an article on how to create a safe, attentive LibreOffice or Apache OpenOffice Windows Service.

There is an article on how to create a Windows Service Without Visual Studio.


Main Body

Stage Direction
Here are Special-Student-7, Morris (a C++ programmer), Chloe (a C++ programmer), and Renee (a Java programmer) in front of a computer.


0: A Note


Renee
This seems to be about creating a daemon in C++.

Special-Student-7
Yes, this is, madam.

Renee
Can't I create it in another programming language, a more normal one, like Java?

Stage Direction
Morris scowls.

Morris
What do you mean by "more normal"? Are you implying that C++ is abnormal?

Renee
Ah, I mean, more amiable one.

Morris
Ugh . . .

Chloe
Morris, what's the matter with you? I thought we were people who did not give any damn about amiability.

Morris
Ugh . . .

Special-Student-7
Well, any proper daemon has to interact with the operating system, especially catch operating system signals.

Can Java do that? I am thinking that it cannot (at least purely, being operating systems independent), so, am assuming that any proper daemon is to be created in C++ or C.

C++ may not be very "amiable", but still there are some cases for which C++ is indispensable.

Renee
I don't need the daemon to be exactly proper; if an office instance is always up, that seems fine.

Special-Student-7
What you are mentioning seems to be a kind of non-daemon resident program, which you may be able to create in your more "amiable" programming language, but a major concern of this article is to shut the office instance down safely and attentively, so, the major issue will be whether your resident program can be orderly shut down.

Renee
Well . . .

Special-Student-7
Of course, it will be probably possible for you to devise a mechanism to orderly shut your resident program down not by operating system signal, and then, the concept of this article should be applicable to also your resident program.

Renee
"the concept" . . .

Special-Student-7
Sorry, I am not going to show code in another programming language than C++ here, as I am not sure about the needs.


1: What the Office Daemon Should Do


Special-Student-7
You may think that what the office daemon has to do is to just 'fork' and 'exec' an office instance at startup and kill the instance process at shutdown, but that is not the case.

Morris
It is I who decide what my office daemon has to do.

Special-Student-7
Certainly, sir, but here, we will have a more versatile, safer and more attentive daemon.

Chloe
I don't understand what you mean by "more versatile", "safer", or "more attentive".

Special-Student-7
Madam, our more versatile, safer and more attentive daemon takes care of these 2 things: 1) making the daemon prevent any shutdown of the office instance except by itself and 2) shutting the office instance safely and attentively when the daemon itself is shutting down.

Morris
"1)" is absurd. If the instance process is killed, it will be killed.

Special-Student-7
Sir, I am talking about shutting down, not about "kill"ing; if the instance process is going to be brutally killed as with the 'kill' command, certainly, it cannot be prevented.

Morris
I don't see your point; who, are you assuming, will attempt an uninvited shutdown? A user? But how can he or she, while the office GUI is not visible, being a daemon?

Special-Student-7
A typical problem is 'soffice --convert-to'.

Morris
What about that?

Special-Student-7
Any 'soffice --convert-to' execution tries to shut the office instance down, at least for the versions I have tried, and we want to prevent it.

Chloe
What do you mean by "safely" in "2)"?

Special-Student-7
It is not safe to brutally kill the office instance process when some documents are opened, especially edited.

Chloe
But LibreOffice will not be particularly broken as far as I have experienced.

Special-Student-7
The office product itself might not be irreparably broken, but the next startup might require some recoveries.

Chloe
Such recoveries can be just skipped, as I am not interested in recovering unsaved modifications.

Special-Student-7
That is fine, but I have experienced some cases that require the safe mode.

Chloe
Did you?

Special-Student-7
And I said "safely and attentively", not just "safely".

Chloe
What's the difference?

Special-Student-7
Most users would not be very happy with their requests barbarically terminated.

"attentively" means that the daemon will wait until the ongoing critical sessions are completed and then shut the office instance down.

Chloe
Well, that will be better, I'm sure.


2: How the Office Daemon Can Do So


Special-Student-7
Our office daemon not only starts an office instance up, but also becomes a UNO client to the office instance.

So, of course, the office instance has to be made a UNO server, and you have to know how to make your program a UNO client.

How to create a C++ daemon is explained in an article, by the way.

Anyway, in order to accomplish "1)" in the previous section, the office daemon registers itself as an office-instance-termination-attempts listener into the office instance and vetoes any office-instance-termination-attempt unless it itself is shutting down.

Chloe
Hmm, can any client "veto"? How democratic!

Special-Student-7
Yes, any UNO client can veto.

As for "2)", the office daemon waits until all the ongoing critical sessions of the UNO clients have been completed and then courteously asks the office instance to die instead of mercilessly killing the office instance.

Chloe
Well, how can the daemon know whether all the critical sessions have been completed?

Special-Student-7
We will have a counter of critical sessions; each UNO client increments the counter whenever it enters any critical block and decrements the counter whenever it leaves the block.

As the counter has to be shared by the office daemon and the UNO clients, of course, it cannot be a commonplace local variable in the office daemon, but a variable in a singleton UNO object shared by all the participants, which are the office daemon and the UNO clients.

As we do not want to allow the UNO clients new critical sessions after the office instance has been planned to be shut down, we will have also a the-office-instance-is-planned-to-be-shut-down flag, which is of course also in the singleton UNO object; the UNO clients does not enter any critical block any more if the flag is up.

Morris
I don't see the necessity for such a dull mechanism. I could just let the UNO clients veto shutdowns until they completed their ongoing critical sessions. That way, I would not need such a dumb shared counter: each UNO client could judge by itself whether it should accept a shutdown.

Special-Student-7
The problem is that any shutdown attempt first tries to close the opened documents and only after all the opened documents have been closed, lets the UNO clients veto; so, vetoing does not protect critical sessions.

Morris
. . . The vetoing mechanism is very stupid; a vetoing UNO client will not, naturally, want opened-documents to be closed, because the client obviously wants to continue what it is doing; the mechanism is not thought out well with use cases in mind.

Special-Student-7
In fact, the UNO client can veto closing of any opened document, but the problem is that any shutdown attempt abandons the effort at the 1st encounter with any document-closing-veto; so, all the UNO clients except one cannot recognize that the office instance has been planned to be shut down, keeping newly entering critical blocks.

Morris
The mechanism is certainly inconsiderate.

Chloe
As you are.

Anyway, what if a UNO client has aborted, leaving some critical session counts?

Special-Student-7
That is certainly a possibility to be considered, and I have thought of 2 solutions.

The 1st one is to not make the office daemon wait indefinitely but let have a grace period after which the office daemon will shut the office instance anyway.

The 2nd one is to have an external tool that lets the operator decrement such unattended counts.

Chloe
How can the operator know what are the unattended counts?

Special-Student-7
Our singleton UNO object can manage the critical session counts per UNO client and disclose them to the operator, probably via the tool.

Chloe
How can I create a singleton UNO object, by the way?

Special-Student-7
It is explained in an article.


3: Some Code Fragments of a Safe Attentive Office Daemon


Special-Student-7
Let us see the important parts of a safe attentive office daemon.

An entire office daemon can be gotten in a later section.

As we need the UNO object singleton, we need the UNO interface that is to be implemented by the UNO component.

How to create any UNO interface is explained in an article.

This is our UNOIDL definition of the UNO interface.

@UNOIDL Source Code
#ifndef __theBiasPlanet_unoDatumTypes_unoUtilities_officeInstance_XOfficeInstance_idl__
#define __theBiasPlanet_unoDatumTypes_unoUtilities_officeInstance_XOfficeInstance_idl__

#include <com/sun/star/uno/XInterface.idl>
#include <com/sun/star/lang/IllegalArgumentException.idl>
#include <com/sun/star/uno/Exception.idl>

module theBiasPlanet {
	module unoDatumTypes {
		module unoUtilities {
			module officeInstance {
				interface XOfficeInstance {
					interface ::com::sun::star::uno::XInterface;
					boolean incrementCriticalSessionCount ( [in] string a_connectionIdentification);
					void decrementCriticalSessionCount ( [in] string a_connectionIdentification);
					long getCriticalSessionCount ();
					boolean serverIsPlannedToBeTerminated ();
				};
			};
		};
	};
};
#endif


Morris
It is not symmetric that only 'incrementCriticalSessionCount' but not 'decrementCriticalSessionCount' has the 'boolean' return.

Special-Student-7
That is because 'incrementCriticalSessionCount' fails when the office instance has been planned to be shut down, while 'decrementCriticalSessionCount' does not fail; only when 'incrementCriticalSessionCount' has succeeded, the UNO client is supposed to enter the critical block.

Morris
. . . Fair enough.

Special-Student-7
This is the code of our singleton UNO component, where '::theBiasPlanet::unoUtilitiesTests::localUnoObjectsTest1::UnoComponentBase' is from another article on defining any UNO component in C++.

theBiasPlanet/unoUtilitiesTests/officeDaemonTest1/OfficeInstance.hpp

@C++ Source Code
#ifndef __theBiasPlanet_unoUtilitiesTests_officeDaemonTest1_OfficeInstance_hpp__
	#define __theBiasPlanet_unoUtilitiesTests_officeDaemonTest1_OfficeInstance_hpp__
	
	#include <condition_variable>
	#include <mutex>
	#include <com/sun/star/uno/Reference.hxx>
	#include <com/sun/star/uno/XComponentContext.hpp>
	#include <cppuhelper/compbase1.hxx>
	#include "theBiasPlanet/unoDatumTypes/unoUtilities/officeInstance/XOfficeInstance.hpp"
	#include "theBiasPlanet/unoUtilitiesTests/localUnoObjectsTest1/UnoComponentBase.hpp"
	
	using namespace ::std;
	using namespace ::cppu;
	using namespace ::com::sun::star::uno;
	using namespace ::theBiasPlanet::unoDatumTypes::unoUtilities::officeInstance;
	
	namespace theBiasPlanet {
		namespace unoUtilitiesTests {
			namespace officeDaemonTest1 {
				class OfficeInstance: public ImplInheritanceHelper1 <::theBiasPlanet::unoUtilitiesTests::localUnoObjectsTest1::UnoComponentBase, XOfficeInstance> {
					private:
						Reference <XComponentContext> & i_underlyingRemoteUnoObjectsContextInXComponentContext;
						recursive_mutex i_threadsMutex;
						condition_variable_any i_threadsCondition;
						int i_criticalSessionCount;
						bool i_serverIsPlannedToBeTerminated;
					public:
						static string const c_officeInstanceSingletonUrl;
						OfficeInstance (Reference <XComponentContext> & a_underlyingRemoteUnoObjectsContextInXComponentContext);
						~OfficeInstance ();
						virtual sal_Bool SAL_CALL incrementCriticalSessionCount (OUString const & a_connectionIdentification) override;
						virtual void SAL_CALL decrementCriticalSessionCount (OUString const & a_connectionIdentification) override;
						virtual sal_Int32 SAL_CALL getCriticalSessionCount () override;
						virtual sal_Bool SAL_CALL serverIsPlannedToBeTerminated () override;
						virtual void setServerAsPlannedToBeTerminated ();
						virtual bool waitUntilNoCriticalSession (int a_timeOutInMilliseconds);
						virtual bool terminate (int a_gracePeriodInMillisecondsBeforeForcing);
				};
			}
		}
	}
#endif

theBiasPlanet/unoUtilitiesTests/staticVariablesInitializer/StaticVariablesInitializer.cpp

@C++ Source Code
~
#include "theBiasPlanet/unoUtilitiesTests/officeDaemonTest1/OfficeInstance.hpp"

namespace theBiasPlanet {
	namespace unoUtilitiesTests {
		~
		namespace officeDaemonTest1 {
			string const OfficeInstance::c_officeInstanceSingletonUrl ("/singletons/theBiasPlanet.unoUtilitiesTests.officeDaemonTest1.OfficeInstance");
		}
	}
}


theBiasPlanet/unoUtilitiesTests/officeDaemonTest1/OfficeInstance.cpp

@C++ Source Code
#include "theBiasPlanet/unoUtilitiesTests/officeDaemonTest1/OfficeInstance.hpp"
#include <iostream>
#include <thread>
#include <com/sun/star/container/ElementExistException.hpp>
#include <com/sun/star/container/NoSuchElementException.hpp>
#include <com/sun/star/frame/XDesktop.hpp>
#include <com/sun/star/frame/XStorable2.hpp>
#include <com/sun/star/lang/XComponent.hpp>
#include <com/sun/star/util/CloseVetoException.hpp>
#include <com/sun/star/util/XCloseable.hpp>
#include "theBiasPlanet/unoUtilities/stringsHandling/UnoExtendedStringHandler.hpp"

using namespace ::com::sun::star::container;
using namespace ::com::sun::star::frame;
using namespace ::com::sun::star::lang;
using namespace ::com::sun::star::uno;
using namespace ::com::sun::star::util;
using namespace ::theBiasPlanet::unoUtilities::stringsHandling;

namespace theBiasPlanet {
	namespace unoUtilitiesTests {
		namespace officeDaemonTest1 {
			OfficeInstance::OfficeInstance (Reference <XComponentContext> & a_underlyingRemoteUnoObjectsContextInXComponentContext): ImplInheritanceHelper1 <::theBiasPlanet::unoUtilitiesTests::localUnoObjectsTest1::UnoComponentBase, XOfficeInstance> (string ("::theBiasPlanet::unoUtilitiesTests::officeDaemonTest1::OfficeInstance")), i_underlyingRemoteUnoObjectsContextInXComponentContext (a_underlyingRemoteUnoObjectsContextInXComponentContext), i_criticalSessionCount (0), i_serverIsPlannedToBeTerminated (false) {
			}
			
			OfficeInstance::~OfficeInstance  () {
			}
			
			sal_Bool SAL_CALL OfficeInstance::incrementCriticalSessionCount (OUString const & a_connectionIdentification) {
				unique_lock <recursive_mutex> l_lock (i_threadsMutex);
				if (i_serverIsPlannedToBeTerminated) {
					return sal_False;
				}
				else {
					i_criticalSessionCount ++;
					cout << string ("### the critical sessions count has been incremented to '") << i_criticalSessionCount << string ("'.") << endl << flush;
					return sal_True;
				}
			}
			
			void SAL_CALL OfficeInstance::decrementCriticalSessionCount (OUString const & a_connectionIdentification) {
				unique_lock <recursive_mutex> l_lock (i_threadsMutex);
				i_criticalSessionCount --;
				cout << string ("### the critical sessions count has been decremented to '") << i_criticalSessionCount << string ("'.") << endl << flush;
				if (i_criticalSessionCount <= 0) {
					i_threadsCondition.notify_all ();
				}
			}
			
			sal_Int32 SAL_CALL OfficeInstance::getCriticalSessionCount () {
				unique_lock <recursive_mutex> l_lock (i_threadsMutex);
				return i_criticalSessionCount;
			}
			
			sal_Bool SAL_CALL OfficeInstance::serverIsPlannedToBeTerminated () {
				unique_lock <recursive_mutex> l_lock (i_threadsMutex);
				return i_serverIsPlannedToBeTerminated;
			}
			
			void OfficeInstance::setServerAsPlannedToBeTerminated () {
				unique_lock <recursive_mutex> l_lock (i_threadsMutex);
				i_serverIsPlannedToBeTerminated = true;
			}
			
			bool OfficeInstance::waitUntilNoCriticalSession (int a_timeOutInMilliseconds) {
				unique_lock <recursive_mutex> l_lock (i_threadsMutex);
				if (i_criticalSessionCount > 0) {
					if (a_timeOutInMilliseconds == -1) {
						i_threadsCondition.wait (l_lock);
						return true;
					}
					else {
						if (i_threadsCondition.wait_for (l_lock, chrono::milliseconds (a_timeOutInMilliseconds)) == cv_status::no_timeout) {
							return true;
						}
						else {
							return false;
						}
					}
				}
				else {
					return true;
				}
			}
			
			bool OfficeInstance::terminate (int a_gracePeriodInMillisecondsBeforeForcing) {
				setServerAsPlannedToBeTerminated ();
				waitUntilNoCriticalSession (a_gracePeriodInMillisecondsBeforeForcing);
				Reference <XDesktop> l_underlyingUnoDesktopInXDesktop (* ( (Reference <XInterface> *) (i_underlyingRemoteUnoObjectsContextInXComponentContext->getValueByName (UnoExtendedStringHandler::getOustring (string ("/singletons/com.sun.star.frame.theDesktop"))).getValue ())), UNO_QUERY);
				Reference <XEnumeration> l_underlyingChildObjectsIteratorInXEnumeration = l_underlyingUnoDesktopInXDesktop->getComponents ()->createEnumeration ();
				Reference <XCloseable> l_underlyingChildObjectInXCloseable;
				string l_documentUrl;
				while (l_underlyingChildObjectsIteratorInXEnumeration->hasMoreElements ()) {
					try {
						l_underlyingChildObjectInXCloseable.set (Reference <XCloseable> (l_underlyingChildObjectsIteratorInXEnumeration->nextElement (), UNO_QUERY).get ());
						if (! (l_underlyingChildObjectInXCloseable.is ())) {
							continue;
						}
						l_documentUrl = UnoExtendedStringHandler::getString (Reference <XStorable2> (l_underlyingChildObjectInXCloseable, UNO_QUERY)->getLocation ());
						try {
							l_underlyingChildObjectInXCloseable->close (false);
							cout << string ("The document '") << l_documentUrl << string ("' has been closed.") << endl << flush;
						}
						catch (CloseVetoException const & l_exception) {
							cout << string ("The document '") << l_documentUrl << string ("' has been vetoed to be closed.") << endl << flush;
							Reference <XComponent> (l_underlyingChildObjectInXCloseable, UNO_QUERY)->dispose ();
							cout << string ("The document '") << l_documentUrl << string ("' has been disposed.") << endl << flush;
						}
						catch (DisposedException const & l_exception) {
							cout << string ("The document '") << l_documentUrl << string ("' has been disposed.") << endl << flush;
						}
					}
					catch (exception const & l_exception) {
						cout << string ("An error has occurred: ") << string (l_exception.what ()) << string (".") << endl << flush;
					}
				}
				bool l_shutdownTryResult (false);
				// loops because the shutdown may be vetoed.
				while (true) {
					try {
						l_shutdownTryResult = l_underlyingUnoDesktopInXDesktop->terminate ();
					}
					catch (DisposedException l_exception) {
						return true;
					}
					if (l_shutdownTryResult) {
						return l_shutdownTryResult;
					}
					this_thread::sleep_for (milliseconds (1000));
				}
			}
		}
	}
}

Chloe
Hmm . . ., it has some additional methods that do not correspond to the UNO interface, whose intention, I can of course guess.

Special-Student-7
Yes. The methods like 'terminate' are not supposed to be called by any ordinary UNO client but only by the office daemon in my present plan, so, they are not disclosed to ordinary UNO clients.

"i_serverIsPlannedToBeTerminated" is the the-office-instance-is-planned-to-be-shut-down flag and "i_criticalSessionCount" is the critical sessions counter.

Whenever the flag is down or the counter is plus, any shutdown is vetoed.

The "incrementCriticalSessionCount" method allows the counter to be incremented only when the flag is down, while the "decrementCriticalSessionCount" method allows the counter to be decremented anytime, being supposed to be called only after the counter has been successfully incremented.

The 'terminate' method takes a grace period after which it tries to shut the office instance down anyway regardless of some overlong critical session or phantom session counts.

The method raises the flag and waits until the counter becomes '0', with the grace period.

After that, the method tries to close the opened documents (if any) before it tries to shut the office instance down, and that may not be absolutely necessary because the shutdown will try to close the documents anyway, but the method also disposes the close-vetoed documents and lets us know what the opened documents were.

Chloe
What is that "UnoExtendedStringHandler"?

Special-Student-7
Ah, that is my utility class (included in the workable sample cited afterward) that gets UNO string from C++ standard string and vice versa; you do not need to use the class if you can construct UNO strings appropriately otherwise.

Chloe
Well . . .

Special-Student-7
The instance of the UNO component can be registered into or unregistered from the office instance as a singleton like this, where "l_underlyingRemoteUnoObjectsContextInXComponentContext" is a 'Reference' instance of a UNO objects context to the office instance.

@C++ Source Code
~
#include <com/sun/star/container/ElementExistException.hpp>
~
#include <com/sun/star/container/XNameContainer.hpp>
~
#include <com/sun/star/uno/Any.hxx>
~
#include <com/sun/star/uno/Reference.hxx>
~
using namespace ::com::sun::star::container;
~
using namespace ::com::sun::star::uno;
~
namespace theBiasPlanet {
	namespace unoUtilitiesTests {
		namespace officeDaemonTest1 {
			~
			int Test1Test::main (int const & a_argumentsNumber, char const * const a_argumentsArray []) {
						~
						Reference <XNameContainer> l_underlyingRemoteUnoObjectsContextInXNameContainer (l_underlyingRemoteUnoObjectsContextInXComponentContext, UNO_QUERY);
						~
						Reference <OfficeInstance> l_underlyingOfficeInstance (new OfficeInstance (l_underlyingRemoteUnoObjectsContextInXComponentContext));
						Reference <XOfficeInstance> l_underlyingOfficeInstanceInXOfficeInstance (l_underlyingOfficeInstance, UNO_QUERY);
						if (! (l_underlyingRemoteUnoObjectsContextInXComponentContext->getValueByName (UnoExtendedStringHandler::getOustring (OfficeInstance::c_officeInstanceSingletonUrl)).hasValue ())) {
							try {
								l_underlyingRemoteUnoObjectsContextInXNameContainer->insertByName (UnoExtendedStringHandler::getOustring (OfficeInstance::c_officeInstanceSingletonUrl), Any (l_underlyingOfficeInstanceInXOfficeInstance));
							}
							catch (ElementExistException const & l_exception) {
							}
						}
						~
							~
							try {
								l_underlyingRemoteUnoObjectsContextInXNameContainer->removeByName (UnoExtendedStringHandler::getOustring (OfficeInstance::c_officeInstanceSingletonUrl));
							}
							catch (NoSuchElementException const & l_exception) {
							}
							~
			}
		}
	}
}


Chloe
I see.

Special-Student-7
This is the office-instance-termination-attempts listener.

theBiasPlanet/unoUtilitiesTests/officeDaemonTest1/OfficeTerminationQueryListener.hpp

@C++ Source Code
#ifndef __theBiasPlanet_unoUtilitiesTests_officeDaemonTest1_OfficeTerminationQueryListener_hpp__
	#define __theBiasPlanet_unoUtilitiesTests_officeDaemonTest1_OfficeTerminationQueryListener_hpp__
	
	#include <com/sun/star/frame/XTerminateListener.hpp>
	#include <com/sun/star/lang/EventObject.hpp>
	#include <com/sun/star/uno/Reference.hxx>
	#include "theBiasPlanet/unoDatumTypes/unoUtilities/officeInstance/XOfficeInstance.hpp"
	#include "theBiasPlanet/unoUtilitiesTests/localUnoObjectsTest1/UnoComponentBase.hpp"
	
	using namespace ::std;
	using namespace ::com::sun::star::frame;
	using namespace ::com::sun::star::uno;
	using namespace ::theBiasPlanet::unoDatumTypes::unoUtilities::officeInstance;
	
	namespace theBiasPlanet {
		namespace unoUtilitiesTests {
			namespace officeDaemonTest1 {
				class OfficeTerminationQueryListener: public ImplInheritanceHelper1 <::theBiasPlanet::unoUtilitiesTests::localUnoObjectsTest1::UnoComponentBase, XTerminateListener> {
					private:
						Reference <XOfficeInstance> & i_underlyingOfficeInstanceInXOfficeInstance;
					public:
						OfficeTerminationQueryListener (Reference <XOfficeInstance> & a_underlyingOfficeInstanceInXOfficeInstance);
						virtual ~OfficeTerminationQueryListener ();
						virtual void SAL_CALL queryTermination (::com::sun::star::lang::EventObject const & a_event) override;
						virtual void SAL_CALL notifyTermination (::com::sun::star::lang::EventObject const & a_event) override;
						virtual void SAL_CALL disposing (::com::sun::star::lang::EventObject const & a_event) override;
				};
			}
		}
	}
#endif


theBiasPlanet/unoUtilitiesTests/officeDaemonTest1/OfficeTerminationQueryListener.cpp

@C++ Source Code
#include "theBiasPlanet/unoUtilitiesTests/officeDaemonTest1/OfficeTerminationQueryListener.hpp"
#include <iostream>
#include <com/sun/star/frame/TerminationVetoException.hpp>

using namespace ::com::sun::star::frame;

namespace theBiasPlanet {
	namespace unoUtilitiesTests {
		namespace officeDaemonTest1 {
			OfficeTerminationQueryListener::OfficeTerminationQueryListener (Reference <XOfficeInstance> & a_underlyingOfficeInstanceInXOfficeInstance): ImplInheritanceHelper1 <::theBiasPlanet::unoUtilitiesTests::localUnoObjectsTest1::UnoComponentBase, XTerminateListener> (string ("::theBiasPlanet::unoUtilitiesTests::officeDaemonTest1::OfficeTerminationQueryListener")), i_underlyingOfficeInstanceInXOfficeInstance (a_underlyingOfficeInstanceInXOfficeInstance) {
			}
			
			OfficeTerminationQueryListener::~OfficeTerminationQueryListener () {
			}
			
			void SAL_CALL OfficeTerminationQueryListener::queryTermination (::com::sun::star::lang::EventObject const & a_event) {
				cout << string ("### The termination of the office instance is queried.") << endl << flush;
				if (! (i_underlyingOfficeInstanceInXOfficeInstance->serverIsPlannedToBeTerminated ()) || i_underlyingOfficeInstanceInXOfficeInstance->getCriticalSessionCount () > 0) {
					cout << string("### And vetoed.") << endl << flush;
					throw TerminationVetoException ();
				}
				else {
					cout << string ("### And accepted.") << endl << flush;
				}
			}
			
			void SAL_CALL OfficeTerminationQueryListener::notifyTermination (::com::sun::star::lang::EventObject const & a_event) {
				cout << string ("### The termination of the office instance is notified.") << endl << flush;
			}
			
			void SAL_CALL OfficeTerminationQueryListener::disposing (::com::sun::star::lang::EventObject const & a_event) {
			}
		}
	}
}


It vetoes any shutdown while the flag is down or the counter is plus.

The instance of it can be registered into or unregistered from the office instance like this.

@C++ Source Code
~
#include <com/sun/star/frame/XDesktop.hpp>
~
#include <com/sun/star/frame/XTerminateListener.hpp>
~
#include <com/sun/star/uno/Reference.hxx>
~
#include <com/sun/star/uno/XInterface.hpp>
~
#include "theBiasPlanet/unoUtilitiesTests/officeDaemonTest1/OfficeTerminationQueryListener.hpp"

~
using namespace ::com::sun::star::frame;
using namespace ::com::sun::star::uno;
~

namespace theBiasPlanet {
	namespace unoUtilitiesTests {
		namespace officeDaemonTest1 {
			int Test1Test::main (int const & a_argumentsNumber, char const * const a_argumentsArray []) {
						~
						string l_com_sun_star_frame_theDesktopSingletonUrl ("/singletons/com.sun.star.frame.theDesktop");
						~
						Reference <XDesktop> l_underlyingUnoDesktopInXDesktop (* ( (Reference <XInterface> *) (l_underlyingRemoteUnoObjectsContextInXComponentContext->getValueByName (UnoExtendedStringHandler::getOustring (l_com_sun_star_frame_theDesktopSingletonUrl)).getValue ())), UNO_QUERY);
						if (l_underlyingUnoDesktopInXDesktop.is ()) {
							Reference <XTerminateListener> l_underlyingOfficeTerminationQueryListener (new OfficeTerminationQueryListener (l_underlyingOfficeInstanceInXOfficeInstance));
							l_underlyingUnoDesktopInXDesktop->addTerminateListener (l_underlyingOfficeTerminationQueryListener);
							~
							l_underlyingUnoDesktopInXDesktop->removeTerminateListener (l_underlyingOfficeTerminationQueryListener);
						}
						~
			}
		}
	}
}


Chloe
Well, well, well, that is how it vetoes.

Special-Student-7
As a whole, the office daemon starts an office instance up, connects to the office instance, registers an office-instance-termination-attempts listener instance into the office instance, registers a singleton UNO component instance into the office instance as a singleton, waits for a 'SIGTERM' signal, and calls the 'terminate' method.

Morris
Um? Will the singleton live in the daemon, not in the office instance?

Special-Student-7
That is my plan. You can have it live in the office instance instead, but you will have to create a UNO service in that case (how to create any Java UNO service is explained in an article, if you are interested).


4: Example UNO Client Critical Blocks


Special-Student-7
This scheme of safe attentive office daemon does not work without the UNO clients' behaving themselves.

Any UNO clients has to have each critical session like this, where "l_underlyingRemoteUnoObjectsContextInXComponentContext" is a UNO objects context to the office instance.

@Java Source Code
package theBiasPlanet.unoUtilitiesTests.criticalSessionsAwareExternalUnoClientTest1;

import com.sun.star.uno.UnoRuntime;
~
import theBiasPlanet.unoDatumTypes.unoUtilities.officeInstance.XOfficeInstance;
~

public class Test1Test {
	public static void main (String [] a_argumentsArray) {
		~
				String l_officeInstanceSingletonUrl = "/singletons/theBiasPlanet.unoUtilitiesTests.officeDaemonTest1.OfficeInstance";
				Object l_underlyingOfficeInstanceInObject = l_underlyingRemoteUnoObjectsContextInXComponentContext.getValueByName (l_officeInstanceSingletonUrl);
				XOfficeInstance l_underlyingOfficeInstanceInXOfficeInstance = null;
				if (l_underlyingOfficeInstanceInObject instanceof com.sun.star.uno.Any) {
					l_underlyingOfficeInstanceInXOfficeInstance = UnoRuntime.queryInterface (XOfficeInstance.class, ( (com.sun.star.uno.Any) l_underlyingOfficeInstanceInObject).getObject ());
				}
				else if (l_underlyingOfficeInstanceInObject instanceof XInterface) {
					l_underlyingOfficeInstanceInXOfficeInstance = UnoRuntime.queryInterface (XOfficeInstance.class, l_underlyingOfficeInstanceInObject);
				}
				if (l_underlyingOfficeInstanceInXOfficeInstance == null) {
					System.out.println ("### the singleton is not there.");
					System.out.flush ();
				}
				else {
					String l_unoClientIdentity = "Java client A";
					while (true) {
						if (l_underlyingOfficeInstanceInXOfficeInstance.incrementCriticalSessionCount (l_unoClientIdentity)) {
							try {
								System.out.println ("### a critical session Start");
								System.out.flush ();
								Thread.sleep (15000);
							}
							finally {
								l_underlyingOfficeInstanceInXOfficeInstance.decrementCriticalSessionCount (l_unoClientIdentity);
								System.out.println ("### a critical session End");
								System.out.flush ();
							}
						}
						else {
							System.out.println ("### the UNO server seems to have been planned to be terminated.");
							System.out.flush ();
							break;
						}
					}
				}
		~
	}
}


@C++ Source Code
#include <iostream>
~
#include <string>
~
#include <thread>
~
#include <com/sun/star/uno/Any.hxx>
~
#include <com/sun/star/uno/Reference.hxx>
~
#include <com/sun/star/uno/XInterface.hpp>
~
#include "theBiasPlanet/unoDatumTypes/unoUtilities/officeInstance/XOfficeInstance.hpp"

using namespace ::std;
~
using namespace ::com::sun::star::uno;
~
using namespace ::theBiasPlanet::unoDatumTypes::unoUtilities::officeInstance;
~

namespace theBiasPlanet {
	namespace unoUtilitiesTests {
		namespace criticalSessionsAwareExternalUnoClientTest1 {
			Test1Test::CriticalSessionFinally::CriticalSessionFinally (string const & a_unoClientIdentity, Reference <XOfficeInstance> & a_underlyingOfficeInstanceInXOfficeInstance): i_unoClientIdentity (a_unoClientIdentity), i_underlyingOfficeInstanceInXOfficeInstance (a_underlyingOfficeInstanceInXOfficeInstance) {
			}
			
			Test1Test::CriticalSessionFinally::~CriticalSessionFinally () noexcept (false) {
				i_underlyingOfficeInstanceInXOfficeInstance->decrementCriticalSessionCount (UnoExtendedStringHandler::getOustring (i_unoClientIdentity));
				cout << string ("### a critical session End") << endl << flush;
			}
			
			int Test1Test::main (int const & a_argumentsNumber, char const * const a_argumentsArray []) {
					~
						string l_officeInstanceSingletonUrl ("/singletons/theBiasPlanet.unoUtilitiesTests.officeDaemonTest1.OfficeInstance");
						Any l_underlyingOfficeInstanceInAny (l_underlyingRemoteUnoObjectsContextInXComponentContext->getValueByName (UnoExtendedStringHandler::getOustring (l_officeInstanceSingletonUrl)));
						Reference <XOfficeInstance> l_underlyingOfficeInstanceInXOfficeInstance (* ( (Reference <XInterface> *) l_underlyingOfficeInstanceInAny.getValue ()), UNO_QUERY);
						if (! (l_underlyingOfficeInstanceInXOfficeInstance.is ())) {
							cout << string ("### the singleton is not there.") << endl << flush;
						}
						else {
							string l_unoClientIdentity ("C++ client A");
							while (true) {
								if (l_underlyingOfficeInstanceInXOfficeInstance->incrementCriticalSessionCount (UnoExtendedStringHandler::getOustring (l_unoClientIdentity))) {
									{
										CriticalSessionFinally l_criticalSessionFinally (l_unoClientIdentity, l_underlyingOfficeInstanceInXOfficeInstance);
										cout << string ("### a critical session Start") << endl << flush;
										this_thread::sleep_for (milliseconds (15000));
									}
								}
								else {
									cout << string ("### the UNO server seems to have been planned to be terminated.") << endl << flush;
									break;
								}
							}
						}
						~
			}
		}
	}
}


@C# Source Code
namespace theBiasPlanet {
	namespace unoUtilitiesTests {
		namespace criticalSessionsAwareExternalUnoClientTest1 {
			using System;
			~
			using System.Threading;
			~
			using unoidl.theBiasPlanet.unoDatumTypes.unoUtilities.officeInstance;
			~
			public class Test1Test {
				public void main (String [] a_argumentsArray) {
					~
								String l_officeInstanceSingletonUrl = "/singletons/theBiasPlanet.unoUtilitiesTests.officeDaemonTest1.OfficeInstance";
								uno.Any l_underlyingOfficeInstanceInAny = l_underlyingRemoteUnoObjectsContextInXComponentContext.getValueByName (l_officeInstanceSingletonUrl);
								XOfficeInstance l_underlyingOfficeInstanceInXOfficeInstance = null;
								if (l_underlyingOfficeInstanceInAny.hasValue ()) {
									l_underlyingOfficeInstanceInXOfficeInstance = (XOfficeInstance) l_underlyingOfficeInstanceInAny.Value;
								}
								if (l_underlyingOfficeInstanceInXOfficeInstance == null) {
									Console.Out.WriteLine ("### the singleton is not there.");
									Console.Out.Flush ();
								}
								else {
									String l_unoClientIdentity = "C# client A";
									while (true) {
										if (l_underlyingOfficeInstanceInXOfficeInstance.incrementCriticalSessionCount (l_unoClientIdentity)) {
											try {
												Console.Out.WriteLine ("### a critical session Start");
												Console.Out.Flush ();
												Thread.Sleep (15000);
											}
											finally {
												l_underlyingOfficeInstanceInXOfficeInstance.decrementCriticalSessionCount (l_unoClientIdentity);
												Console.Out.WriteLine ("### a critical session End");
												Console.Out.Flush ();
											}
										}
										else {
											Console.Out.WriteLine ("### the UNO server seems to have been planned to be terminated.");
											Console.Out.Flush ();
											break;
										}
									}
								}
				~
			}
		}
	}
}


@Python Source Code
from typing import Any
from typing import List
from typing import cast
~
import sys
import time
import uno
~
from uno import Any as UnoAny
~
XOfficeInstance: Any = uno.getClass ("theBiasPlanet.unoDatumTypes.unoUtilities.officeInstance.XOfficeInstance")

class Test1Test:
	@staticmethod
	def main (a_arguments: List [str]) -> None:
		~
				l_officeInstanceSingletonUrl: str = "/singletons/theBiasPlanet.unoUtilitiesTests.officeDaemonTest1.OfficeInstance"
				l_underlyingOfficeInstanceInObject: object = l_underlyingRemoteUnoObjectsContextInXComponentContext.getValueByName (l_officeInstanceSingletonUrl)
				l_underlyingOfficeInstanceInXOfficeInstance: "XOfficeInstance" = None
				if isinstance (l_underlyingOfficeInstanceInObject, UnoAny):
					l_underlyingOfficeInstanceInXOfficeInstance = cast (XOfficeInstance, cast (UnoAny, l_underlyingOfficeInstanceInObject).value)
				elif l_underlyingOfficeInstanceInObject is not None:
					l_underlyingOfficeInstanceInXOfficeInstance = cast (XOfficeInstance, l_underlyingOfficeInstanceInObject)
				if l_underlyingOfficeInstanceInXOfficeInstance is None:
					sys.stdout.write ("### the singleton is not there.\n")
					sys.stdout.flush ()
				else:
					l_unoClientIdentity: str = "Python client A"
					while True:
						if l_underlyingOfficeInstanceInXOfficeInstance.incrementCriticalSessionCount (l_unoClientIdentity):
							try:
								sys.stdout.write ("### a critical session Start\n")
								sys.stdout.flush ()
								time.sleep (15)
							finally:
								l_underlyingOfficeInstanceInXOfficeInstance.decrementCriticalSessionCount (l_unoClientIdentity)
								sys.stdout.write ("### a critical session End\n")
								sys.stdout.flush ()
						else:
							sys.stdout.write ("### the UNO server seems to have been planned to be terminated.\n")
							sys.stdout.flush ()
							break


Chloe
Ah, of course, a client doesn't have to be in C++.

But is that all? I mean, don't the other programming languages need some preparations?

Special-Student-7
All what is needed in each of the other programming languages is the mapping image of the UNO interface in the programming language, except, of course, being a UNO client to the office instance.

Chloe
Hmm.

Special-Student-7
If you are wondering whether you need to have a singleton UNO component code or need to have an office-instance-termination-attempts listener code in another programming language, the singleton lives only in C++, so no necessity for any singleton UNO component in another programming language, and the office daemon can solely veto shutdowns, so no necessity for any office-instance-termination-attempts listener in another programming language.

Anyway, any UNO client should enter any critical block only after the "incrementCriticalSessionCount" method succeeds and should unfailingly call the "decrementCriticalSessionCount" method whenever it leaves any critical block.


5: A Workable Sample


Special-Student-7
Here is a workable sample.

'officeDaemon' is the main project, here is how to build any project cited in this site, and this article includes some additional information for building and registering a daemon.

This is my '.service' file, where '%~%'s have to be replaced with the actual values.

'/etc/systemd/system/officeDaemon.service'

@systemd.service Source Code
[Unit]
Description=The office daemon service
After=network.target
StartLimitBurst=5
StartLimitIntervalSec=10

[Service]
Type=simple
Restart=always
RestartSec=1
User=%the operating system user name%
Environment="LD_LIBRARY_PATH=%the office directory path%/sdk/lib:%the office directory path%/program:%the development directory path%/coreUtilitiesToBeDisclosed/target:%the development directory path%/unoAdditionalDatumTypes/target:%the development directory path%/unoUtilitiesToBeDisclosed/target"
Environment="URE_BOOTSTRAP=file://%the office directory path%/program/fundamentalrc"
ExecStart=%the development directory path%/officeDaemon/target/theBiasPlanet.officeDaemon.exe "%the office directory path%/program" "socket,host=localhost,port=%the port number%,tcpNoDelay=1;urp;StarOffice.ComponentContext" "false"
KillMode=process

[Install]
WantedBy=multi-user.target

The specified port has to be opened as described in a previous article, of course.

How to enable and control the daemon is described in an article.

In fact, the archive file includes also a sample critical-sessions-aware UNO client in Java, C++, C#, or Python.

They can be executed like these with the current directory at 'criticalSessionsAwareExternalUnoClients'.

For Java:

@bash or CMD Source Code
gradle i_executeJarTask -Pi_mainClassName="theBiasPlanet.criticalSessionsAwareExternalUnoClients.programs.CriticalSessionsAwareExternalUnoClientConsoleProgram" -Pi_commandLineArguments="\"socket,host=localhost,port=2002,tcpNoDelay=1;urp;StarOffice.ComponentContext\""

For C++:

@bash or CMD Source Code
gradle i_executeCplusplusExecutableFileTask -Pi_commandLineArguments="\"socket,host=localhost,port=2002,tcpNoDelay=1;urp;StarOffice.ComponentContext\""

For C#:

@bash or CMD Source Code
gradle i_executeCsharpExecutableFileTask -Pi_commandLineArguments="\socket,host=localhost,port=2002,tcpNoDelay=1;urp;StarOffice.ComponentContext\"

For Python:

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

Note that the samples are from my code base, so they use my utility classes in my utility projects, which include much code that is not directly related with those samples; someone may not be happy about such unrelated code included, but please understand that it is not so easy to excise only such unrelated code from the interwoven whole.

The workable sample code is not written as flatly as the code cited in this article (which is written that flat for explanation's sake), but what are done are the same.


References


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