2021年12月5日日曜日

65: コネクションアウェアなリモートにも置けるPython外部UNOクライアント

<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>

LibreOffice/OpenOfficeの機能をあなたの自立した(非マクロ)プログラム(Webアプリケーションのような長時間動作するものでもよい)に組み込むために

話題


About: UNO (Universal Network Objects)
About: LibreOffice
About: Apache OpenOffice
About: Pythonプログラミング言語

この記事の目次


開始コンテキスト



ターゲットコンテキスト



  • 読者は、自分の外部Pythonプログラムを、リモートコンピュータにあってもよい任意のUNOサーバー(通常は、LibreOfficeまたはApache OpenOfficeインスタンス)へ、コネクションアウェアに接続する方法を知る。

オリエンテーション


LibreOfficeまたはApache OpenOfficeデーモンを作成する方法の記事およびLibreOfficeまたはApache OpenOffice Microsoft Windowsサービスを作成する方法の記事があります。

LibreOfficeまたはApache OpenOfficeをファイルコンバーターとして最適に利用することについての記事があります。

Pythonにて任意のUNOディスパッチコマンドを実行して実行の全情報を取得することについての記事があります。

Pythonにて任意のUNOコンポーネントを作成することについての記事があります。

LibreOfficeまたはOpenOfficeに外部Python記事を使用することについての記事があります。

コネクションアウェアな外部UNOクライアントを他のプログラミング言語で作成することについての記事が公開される予定ですC#)。


本体

ト書き
Special-Student-7、Objector 28A、Objector 28Bがコンピュータの前にいる。


1: 目標の明確化


Special-Student-7
本記事の目標は、自分の自立Pythonプログラムを任意のUNOサーバーに、scrupulous(良心的な、用意周到な)に接続することです。

「UNOサーバー」は通常LibreOfficeまたはApache OpenOfficeインスタンスであり、「自立」は、プログラムがLibreOfficeまたはApache OpenOfficeインスタンス内マクロでないことを意味します。

Objector 28A
「scrupulous(良心的な)に」?unscrupulousな(非良心的な)方法なんてものがあるということか?

Special-Student-7
サー、最も広く宣伝されている方法は怠惰な方法です、特にそれを「unscrupulous(非良心的な)」とは私は呼びませんが。

ここでは私たちはその怠惰な方法をとらないと、私は言ったのです。

Objector 28A
「その怠惰な方法」はどう怠惰なのか?

Special-Student-7
それは、UNOサーバーはローカルコンピュータにいると決め込んでおり、確立されたコネクションが確立後にまだ生きているか否かに気をとめません。

私たちのscrupulousな方法は、任意のリモートUNOサーバーに接続することができ、コネクションの任意の切断を検知できます。

Objector 28A
それは重要なのか?

Special-Student-7
私たちは、自立プログラムはWebアプリケーションのような長時間動作するものであるかもしれず、それはアプリケーションサーバーにいるかもしれず、UNOサーバーはリモートデータサーバーにいるかもしれず、コネクションはある理由によって切断されるかもしれず、その長時間動作するプログラムは通常、コネクションが切断されたら、それを知りたいだろうと想定しています。

Objector 28A
しかし、コネクション切断はどのみち検出されるだろう、処理が失敗したときに、だって、それは失敗するだろう?

Special-Student-7
それはいずれは失敗するでしょう、もしも状況が放置されれば、しかし、ユーザーたちは、状況を、自分たちのリクエストが失敗するまで放置してほしくないでしょう。

Objector 28A
かもしれない、しかし、どのみち、被害が全く出ないという保証はないだろう?

Special-Student-7
保証はありません、しかし、被害者は、画面上で大量のデータをインプットしてボタンを押し、自分の努力が徒労であったと知る前に知らせてほしいと思うでしょう。

Objector 28A
. . . それで、プログラムは何をするんだね、切断を検知して?ただユーザーたちに徒労なことをしないように警告するだけか、それともコネクションを再確立するのか?

Special-Student-7
コネクションを再確立するよう試みることはできますが、勿論、問題の原因がまず取り除かれなければなりません、その試みが成功するためには。

例えば、もしも、UNOサーバーの逝去が原因であれば、新たなUNOサーバーを自動的に開始させることができるかもしれませんが、もしも、誰かがあるケーブルを引っこ抜いたのであれば、そのケーブルが元に戻される必要があるでしょう、そのような行為についてその人物が諌められる必要とともに。

Objector 28A
諌めるのは君が心配することじゃない。

Special-Student-7
でも、私は心配します。さもなければ、その方はまたやるかもしれません。

いずれにせよ、コネクション切断を迅速に検知すること、そしてそれを整然と行なうことは良いことです。

Objector 28A
「整然と」とはどういう意味だ?

Special-Student-7
切断は、イベントリスナーによって集中的に検知されます、散らばったモジュールたちによって個々に検知されるのではなく。

Objector 28B
あなたは「「UNOサーバー」は通常LibreOfficeまたはApache OpenOfficeインスタンスであり」と言っているけど、UNOサーバーは常にLibreOfficeまたはApache OpenOfficeインスタンスではないの?

Special-Student-7
マダム、私がそう申したのは、あるUNOサーバーはLibreOfficeまたはApache OpenOfficeインスタンスではないかもしれないからです。

Objector 28B
それじゃあ、それは何かもしれないわけ?

Special-Student-7
それは、あなたが作成されたUNOサーバーかもしれません。

Objector 28B
私は作成してません。

Special-Student-7
ほとんどの人は自らのUNOサーバーを作成することなど想像したこともないことを存じていますが、その可能性を私は除外したくなかったのです。

実のところ、それは結構容易です。

しかし、それはとにかく、お望みであれば、「UNOサーバー」はLibreOfficeまたはApache OpenOfficeインスタンスであるとご自由にお考えください、あなたの目的のためには。

しかし、当該LibreOfficeまたはApache OpenOfficeインスタンスは、UNOサーバーにされないといけないことにご注意ください、UNOサーバーであるためには。 

UNOサーバーはリモートコンピューターにいてもいいですが、その場合には、UNOサーバーはTCP/IPソケットプロトコルを使わなければなりません。


2: いくつかの注意事項


Special-Student-7
ここで紹介されるコードは、Pythonバージョンが3.6以上であると想定しています。

その理由は、コードは変数タイプアノテーション機能を使っていることです。

Objector 28A
その機能はPython UNOのために必要なのか?

Special-Student-7
全然そうではありません、私にとってはほとんど絶対的に必要です。いずれにせよ、3.6は昨今ではアンリーズナブルに高いハードルでは全くないはずです。

Pythonバージョンについて言えば、恣意的なバージョンを使えるわけではないことをご承知されるべきです。

Objector 28A
でも、UNOタイプにスタティックタイプチェックなんてどのみちできないだろう、UNOモジュール群のスタブを持ってないんだから?

Special-Student-7
ああ、少なくとも本記事のために不可欠なスタブ群は、私の後ほど紹介されるアーカイブファイル内に含まれています

Objector 28A
えーと . . .

Special-Student-7
また、 適切な開発環境が準備できているものと想定されています(Linux用またはMicrosoft Windows用の記事があります)。


3: コード


Special-Student-7
あれこれ言うのはこれぐらいにして、以下がコードです。

theBiasPlanet/unoUtilitiesTests/connectingFromExternalProgramTest1/Test1Test.py

@Python ソースコード
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 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_unoConnectionConnector: XConnector = cast (XConnector, l_localUnoObjectsContext.getServiceManager ().createInstanceWithContext ("com.sun.star.connection.Connector", 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 ()
						try:
							l_unoConnection: XConnection = 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 'Test1Test.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.bridge.BridgeFactory", 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 'Test1Test.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 'Test1Test.s_unoConnectionMaintainingThreadsCondition' is not protected here), but can safely wait, because anyway, the waking call is not dispatched yet at least (because 'Test1Test.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
. . . えーと、結構、長々しいコードよね。私は怠惰な方法のほうが好きかもしれない。

Special-Student-7
お分かりでしょうが、コードの長さはあなたにとって実際にはたいした問題ではないはずです: それは、ひとかたまりの定型コードであって、もしもよろしければ、あなたはただそれをコピーすればよいだけです: 1行をコピーするのも20行をコピーするのも、あなたの労力にとってたいして違いはありません。

Objector 28B
200行じゃないの!

Special-Student-7
20行であろうが200行であろうが、あなたのコピーする労力はそんなに違わないでしょう。

コードの長さに怖じ気づくのは、何のリーズナブルな理由にも基づかない、単に情緒的な反応です。


4: コードを実行する


Special-Student-7
Python外部UNOクライアントを実行する際には行わなければならないいくつかのことがあります。

第1に、1つのモジュール群パス、具体的には、Linuxでは'%オフィスプロダクトディレクトリ%/program'、Microsoft Windowsでは'%オフィスプロダクトディレクトリ%\program'を含めなければなりません(実際には、それは自動的に何らかの方法で既にセットされているかもしれません、環境によっては)。

第2に、オペレーティングシステム環境変数を、具体的には、'URE_BOOTSTRAP'を、Linuxでは'%オフィスプロダクトディレクトリ%/program/fundamentalrc'の値で、Microsoft Windowsでは'%オフィスプロダクトディレクトリ%/program/fundamental.ini'の値で、渡さなければなりません(実際には、それは不必要かもしれません、環境によっては)。

実際、以下はこのLinuxでそのコードを実行するはずです。

@bash ソースコード
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」は、UNOサーバーのアドレスです。

ト書き
Special-Student-7は、彼らの前にあるLinuxマシーンでターミナルを開き、上記Linuxコマンドを実行する。 「### The UNO connection could not be established.」が3秒おきに現わ続ける。

Objector 28A
. . . それはエラーじゃないのか、もしかして?

Special-Student-7
それはエラーです、勿論。

ト書き
Special-Student-7は満足げに頷く。

Objector 28A
なんで、君はそれについてそんなに得意げなんだ?

Special-Student-7
別に得意というわけではありません。ただ、それは当然のことです、UNOサーバーがまだ立ち上がっていませんから。

UNOサーバーを開始させてください。

ト書き
Special-Student-7はLibreOfficeインスタンスを開始する。 「### A UNO connection has been established.」がコンソール上に現われる、次に、「### Doing something Start.」および「### Doing something End.」が3秒おきに現われ始める。

Objector 28B
ああ、つながった、みたいね。

Special-Student-7
UNOサーバーを殺させてください。

ト書き
Special-Student-7は、LibreOfficeインスタンスを'kill'コマンドで殺す。 「### The UNO connection has been severed.」および「### Warning: there is no UNO connection.」が現われ、次いで、「### The UNO connection could not be established.」が再び現れ始める。

Special-Student-7
ご注意いただきたいのでが、LibreOfficeインスタンスをGUI上終了させても、インスタンスを本当には止めないかもしれません、そのため、私は'kill'コマンドを使ったのです。

それはとにかく、LibreOfficeインスタンスを再開始させてください。

ト書き
Special-Student-7はLibreOfficeインスタンスを開始する。 「### A UNO connection has been established.」がコンソール上に現われ、次いで、「### Doing something Start.」および「### Doing something End.」が3秒おきに現われ始める。

Objector 28B
ああ、でも、どうしたらプログラムを止められるの?

Special-Student-7
コンソール上で'Enter'キーを押せばプログラムが停止します。

ト書き
Special-Student-7は'Enter'キーを押し、プログラムが停止する。


5: コードを理解する


Special-Student-7
コードを理解しましょう。あなたはコードをコピーできると申しましたが、あなたはコードを理解する必要はないとは申しませんでした。

Objector 28B
それじゃあ、コードの長さは重要じゃない、結局のところ!

Special-Student-7
えーと、問題は、それを何と比較しておられるかということです: 怠惰な方法を選択するのと比較すれば、理解せずにただコピーするのは少しも悪くありません、あなたはどのみち理解されないのですから。私は、コピーして理解するのがベストだと申し上げているのです。

Objector 28B
どうでもいいけど . . .

Special-Student-7
3つの常時スレッド: メインスレッド、コネクション確立スレッド、(「l_unoConnectingThread」)、ワーキングスレッド(「l_unoWorkingThread」)、および1つの散発的に起動されるスレッド: コネクション切断検出スレッドがあります。

まず最初に、プログラムはブートストラッピング(プログラムがUNOプログラムになることを意味します)を行わなければなりません。

プログラムは、UNOサーバーに接続するには、UNOプログラムにならなければならないということです、なぜなら、プログラムはUNOサーバーにUNOを手段として接続するからです。

具体的には、「uno.getComponentContext ()」がブートストラッピングを行ない、ローカルUNOオブジェクト群コンテキストを獲得します。

「ローカル」は、それが、そのプログラムのものであり、UNOサーバーのものではないことを意味します。

ローカルUNO環境はそれ自身のUNOサービス群マネージャーを持っており、そのUNOサービス群マネージャーはいくつかのUNOサービスをローカルUNO環境内にインスタンス化できます。

ローカルUNO環境には、勿論、スプレッドシートドキュメントのようなUNOコンポーネントはないが、ローカルUNO環境をUNOサーバーに接続するために必要ないくつかのUNOコンポーネントがあり、プログラムは、それらのUNOコンポーネントをローカルUNOサービス群マネージャーを介してインスタンス化します。 

実際、プログラムは「com.sun.star.connection.Connector」UNOサービスをインスタンス化します。

次に、プログラムは「com.sun.star.connection.Connector」インスタンスの「connect」メソッドをコールして、「com.sun.star.connection.XConnection」インスタンスを獲得します。

Objector 28A
じゃあ、プログラムは既にUNOサーバーに接続したわけだ。

Special-Student-7
えーと、そのメソッド名は「connect」ですが、プログラムはUNOサーバーへのブリッジを確立しなければなりません、本当に接続するためには。

その一方で、プログラムはイニシャルUNOオブジェクト群プロバイダを用意します。

Objector 28B
それ一体、何?

Special-Student-7
UNOサーバーはローカルプログラムからいくつかのUNOオブジェクトをイニシャルUNOオブジェクト群プロバイダを介して獲得できます、ブリッジが確立されたときに。

Objector 28B
なんで、UNOサーバーはそれらを獲得しないといけないわけ?

Special-Student-7
いくつかのUNOオブジェクトと申しましたが、通常は、それは、ローカルUNOオブジェクト群コンテキストであって、それは、UNOサーバーから望まれるかもしれません、通常、ローカルUNO環境が操作されるのは、ローカルUNOオブジェクト群コンテキストを介してなので。

Objector 28B
「かもしれ」ない . . .

Special-Student-7
しかし、実際には、LibreOfficeインスタンスはそれを望みません、なぜなら、そうしたUNOクライアントたちを操作しようという意図が全くないから。したがって、もしもUNOサーバーがLibreOfficeまたはApache OpenOfficeインスタンスであれば、イニシャルUNOオブジェクト群プロバイダについて心配する必要は実際にはありません。

Objector 28B
ということは . . .

Special-Student-7
あなたのプログラムは実際にイニシャルUNOオブジェクト群プロバイダを用意する必要はありません、私たちのプログラムはそうしましたが、scrupulousであるように意図されているが故に

次に、プログラムは「com.sun.star.bridge.BridgeFactory」UNOサービスをインスタンス化します。

次に、プログラムはUNOブリッジを生成します、「com.sun.star.bridge.BridgeFactory」インスタンスの「createBridge」メソッドをコールすることによって。

次に、プログラムは、リモートUNOオブジェクト群コンテキストを獲得します、「com.sun.star.bridge.XBridge」インスタンスの「getInstance」メソッドをコールすることによって。

お分かりでしょうが、そのリモートUNOオブジェクト群コンテキストがUNOサーバーへのアクセスポイントになるので、それをキープされたいでしょう。

情報として申し上げると、そのリモートUNOオブジェクト群コンテキストがUNOサーバーから取得できるのは、UNOサーバーがそれを自身のイニシャルUNOオブジェクト群プロバイダを介してオファーしているからです。

Objector 28B
それじゃあ、おめでとう?

Special-Student-7
しかし、プログラムはもう1つのことを行ないます: イベントリスナーをブリッジに登録します。

そのイベントリスナーは「com.sun.star.lang.XEventListener」UNOインターフェイス(「disposing」メソッドを持っており、そのメソッドはコネクションが切断されたときに呼ばれる)を実装しておかなければなりません。

「addEventListener」メソッドは当該イベントリスナーをUNOブリッジへ登録します。

Objector 28B
じゃあ、おめでとう!

Special-Student-7
当プログラムのマルチスレッド構造についていくらか言っておきましょう。

「l_unoConnectingThread」スレッドはコネクションがない間はアクティブであって、コネクションを確立しようとします。コネクションが確立されると、そのスレッドは無期限に待ち始めます。

イベントリスナー(呼び起こされるスレッドで走る)は、コネクションが切断されると「l_unoConnectingThread」スレッドに通知し、「l_unoConnectingThread」スレッドは再びアクティブになり、新たなコネクションを確立しようと試みます。

"l_unoWorkingThread"スレッドは、コネクションがある間はアクティブであり、何かを行ないます。コネクションが切断されると、スレッドは無期限に待ち始めます。

ワーキングスレッドはWebアプリケーションのユーザーリクエスト群に対処しているものと想像してください。もしも、ユーザーが不運だったら、ユーザーはエラーを経験するかもしれませんが、そうでなければ、ユーザーは、労苦を無益に行なう前に、Webアプリケーションは一時的に処理不能状態にあると警告されるでしょう(前出の"### Warning: there is no UNO connection."メッセージはその状況を表わしています)。

Objector 28B
えーと、コード中に長々しいコメントがいくつかあるけど。

Special-Student-7
マルチスレッドのハンドリングは通常、かなりデリケートなものですが、本ケースにおいてもそうです。

面倒を見なければならないポイントがいくつかあります。

第1に、デッドロックは避けなければなりません。

"l_unoConnectingThread"スレッド内に、"s_unoConnectionMaintainingThreadsCondition"がロックされて次にリリースされ、次に"s_unoConnectionUsingThreadsCondition"がロックされて次にリリースされ、そして次に"s_unoConnectionMaintainingThreadsCondition"がロックされて次にリリースされるという、曲折したように思われる処置がありますが、その理由は、"s_unoConnectionUsingThreadsCondition"は"s_unoConnectionMaintainingThreadsCondition"がロックされている最中にロックできないことです、それはデッドロックを起こす可能性がありますから。

Objector 28B
なんで、それはデッドロックを起こすかもしれないわけ?

Special-Student-7
"l_unoWorkingThread"スレッドは、逆の順序でロックを行わなければならず、2つのスレッドが互いに逆順にロックを行なうと、デッドロックを起こす可能性があります、なぜなら、スレッド1がオブジェクトAをロックし、スレッド2がオブジェクトBをロックし、スレッド1はオブジェクトBをロックしたいが、できない、オブジェクトBは既にスレッドによってロックされているから、その一方でスレッド2はオブジェクトAをロックしたいが、できない、オブジェクトAはすでにスレッド1によってすでにロックされているいるから、デッドロックです。

Objector 28B
なんで、"l_unoWorkingThread"はロックを逆順に行わなければならないわけ?

Special-Student-7
それは、面倒を見なければならない第2点のためです。

スレッドは、それが適切に起こされると保証されるときに限って待ちに入れます。

それは、さもなければ、そのスレッドは際限なく待ち続けることになりかねないからです。

Objector 28B
もちろん、それが、"l_unoWorkingThread"のロック順とどう関係するわけ?

Special-Student-7
そのスレッドが"s_unoConnectionUsingThreadsCondition"を対象として待つには、"s_unoConnectionUsingThreadsCondition"がロック済みでなければなりませんが、"s_unoConnectionMaintainingThreadsCondition"もロックされていなければなりません、"s_unoConnectionInformation"をチェックするためには(なぜなら、"s_unoConnectionMaintainingThreadsCondition"が"s_unoConnectionInformation"をプロテクトしているから)。

Objector 28B
なんで、そのスレッドは、"s_unoConnectionInformation"をチェックして"s_unoConnectionMaintainingThreadsCondition"をリリースし、それから"s_unoConnectionUsingThreadsCondition"をロックしないの?

Special-Student-7
それは、"s_unoConnectionInformation"は"s_unoConnectionMaintainingThreadsCondition"がリリースされた後、"s_unoConnectionUsingThreadsCondition"がロックされる前に変更されるかもしれないからです、そうすると、チェックは信頼できないものになるでしょう。

Objector 28B
. . . なんで、"s_unoConnectionInformation"はチェックされなければならないの、そもそも?

Special-Student-7
それは、もしも、待ちに入る瞬間にコネクションが本当に切断されていなかったら、スレッドは起こされないことになります、なぜなら、目覚ましコールは、新たなコネクションが確立された際にディスパッチされるものですが、コネクションが切断されていなかったので、再確立もないことになり、したがって、目覚ましコールがありません。

Objector 28B
でも、"s_unoConnectionMaintainingThreadsCondition"はスレッドが待ち始める前にどのみちリリース済みじゃない、したがって、コネクションは スレッドが待ち始める前に、既に確立済みかもしれないんじゃない?

Special-Student-7
はい、しかし、"s_unoConnectionUsingThreadsCondition"がロックされているので、目覚ましコールはまだディスパッチされているはずなく、そこがポイントです。

Objector 28B
だから、スレッドは適切に起こされると保証されているということ?

Special-Student-7
はい。

Objector 28B
なんで、"s_unoConnectionUsingThreadsCondition"は"s_unoConnectionMaintainingThreadsCondition"ロックセグメント内でロックできないの?

Special-Student-7
なぜなら、そうしたら、"s_unoConnectionMaintainingThreadsCondition"はロックされ続けることになり、"l_unoConnectingThread"スレッドをブロックし、したがって、"l_unoWorkingThread"は、新たなコネクションが確立されるのを待つ、その確立をブロックしながら、ということになるでしょう。

Objector 28B
. . . "l_unoConnectingThread"スレッドが待ち始める際、そのスレッドは適切に起こされるよう保証されているわけ?

Special-Student-7
はい。コネクションは既に切断されているかもしれませんが、切断はまだ検出されていないと保証されています(なぜなら、"s_unoConnectionMaintainingThreadsCondition"がロックされているから)、したがって、目覚ましコールはまだディスパッチされていないと保証されています。

Objector 28B
じゃあ、目覚ましコールがすでにディスパッチされた後にスレッドが待ち始めないように、あなたは注意を払ってるわけね . . .

Special-Student-7
はい。それが重要な点です、なぜなら、さもなければ、待っているスレッドが予期しているものが来ないということになるでしょう。

そして、面倒を見なければならない第3のポイントは、スレッドは、他のスレッドが走らなければならない時には、待たなければならないということです。

Objector 28B
はあ?それって正しい?マルチスレッドなんだから、別のスレッドはどのみち走るでしょう?

Special-Student-7
必ずしもそうではありません、実行時環境に依存して。複数のスレッドは本当に同時に走りはしないかもしれません: あるスレッドが走り続けるかもしれません、それが待たない限り。

"l_unoWorkingThread"スレッドは待つ必要はないと思われるかもしれませんが、それは待たなければなりません、コネクション切断検出スレッドが走ると保証されるためには。

Objector 28B
でも、"sleep"がそこにあるから . . .

Special-Student-7
ああ、"sleep"はそこにないと仮定してください: "sleep"がそこにあるのは、絶え間ないメッセージアウトプットが煩わしいというという理由からのみです。

Objector 28B
でも、もしも、プログラムがWebサーバーだったら . . .

Special-Student-7
おっしゃるとおりです: もしも、プログラムがWebサーバーだったら、それはループではなく、コネクション切断検出スレッドはユーザーリクエストとユーザーリクエストの間に呼び起こされるでしょう。


6: 動かせるサンプル


Special-Student-7
ここに動かせるサンプルがあり、それは、LibreOfficeまたはApache OpenOfficeインスタンスによってローカルに格納されたファイルは、何であろうが自動的にそのセーブファイルを作るものです(注意として、本ケースでは、LibreOfficeまたはApache OpenOfficeインスタンスはローカルであると想定されています)。

そのメインプロジェクトは'connectionAwareExternalUnoClients'であり、プロジェクトをビルドする方法は、以前の記事に記述されています。

Objector 28B
"ビルド"?Pythonプロジェクトをビルドしないといけない?

Special-Student-7
ああ、必ずビルドしなければならないわけではありませんが、ここでの"ビルド"は、スタティックタイプチェックし、スタブを作成し、コンパイルする('.pyc'ファイルを作成する)ことを意味しています、それらは特に必須というわけではありませんが。

プログラムは以下のようにして実行できます。

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

注意として、そのコードは、上記コードのようにフラットに書かれてはいませんが、行なわれていることは同じです、私のユーティリティクラスである、'theBiasPlanet.unoUtilities.processesHandling.UnoProcessEnvironment.UnoProcessEnvironment'、'theBiasPlanet.unoUtilities.connectionsHandling.UnoConnectionConnector.UnoConnectionConnector'、'theBiasPlanet.unoUtilities.connection.UnoConnection.UnoConnection'などの中にしまわれて。   .

Objector 28B
なんで、それは"フラットに"書かれていないわけ?

Special-Student-7
上記コードは、説明目的のためにのみ書かれたものです。私は通常そのようには書きません。私は通常、もっと適切に物事をクラス群として構成します。


参考資料


<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>