tdf#116156 tdf#157162 tdf#159528 Fix glitches in officehelper.py

- MacOs & GNU/Linux distributions are supported, next to Windows
- Connection attempts are customisable
- Reporting to console can be configured
- Python source doc. added
- service memory cleanup suggestion examples intended for QA as well as inclusion in wiki pages

officehelper documentation:
https://wiki.documentfoundation.org/Documentation/DevGuide/First_Steps#First_Contact

Change-Id: I6a7c19429e78a1736e4a1479c4bbc1950228d93f
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/164118
Tested-by: Jenkins
Reviewed-by: Noel Grandin <noel.grandin@collabora.co.uk>
diff --git a/pyuno/source/officehelper.py b/pyuno/source/officehelper.py
index 53bd594..5c89ef7 100644
--- a/pyuno/source/officehelper.py
+++ b/pyuno/source/officehelper.py
@@ -17,14 +17,56 @@
#   the License at http://www.apache.org/licenses/LICENSE-2.0 .
#

#
# Translated to python from "Bootstrap.java" by Kim Kulak
#
""" Bootstrap OOo and PyUNO Runtime.

import os
import random
from sys import platform
from time import sleep
The soffice process is started opening a named pipe of random name, then
the local context is used to access the pipe. This function directly
returns the remote component context, from whereon you can get the
ServiceManager by calling getServiceManager() on the returned object.

This module supports the following environments:
-   Windows 
-   GNU/Linux derivatives
-   MacOS X

A configurable time-out allows to wait for LibO process to be completed.
Multiple attempts can be set in order to connect to LibO as a service. 

Specific versions of the office suite can be started.

Instructions:

1.  Include one of the below examples in your Python macro
2.  Run your LibreOffice script from your preferred IDE

Imports:
    os, random, subprocess, sys - `bootstrap`
    itertools, time - `retry` decorator

Exceptions:
    BootstrapException - in `bootstrap`
    NoConnectException - in `bootstrap`

Functions:
    `bootstrap`
    `retry` decorator

Acknowledgments:

  - Kim Kulak for original officehelper.py Python translation from bootstrap.java
  - ActiveState, for `retry` python decorator

warning:: Tested platforms are Linux, MacOS X & Windows
    AppImage, Flatpak, Snap and the like have not be validated

:References:
.. _ActiveState retry Python decorator: http://code.activestate.com/recipes/580745-retry-decorator-in-python/

"""

import os, random, subprocess  # in bootstrap()
from sys import platform  # in bootstrap()
import itertools, time  # in retry() decorator

import uno
from com.sun.star.connection import NoConnectException
@@ -34,54 +76,146 @@ from com.sun.star.uno import Exception as UnoException
class BootstrapException(UnoException):
    pass

def bootstrap():

def retry(delays=(0, 1, 5, 30, 180, 600, 3600),
          exception=Exception,
          report=lambda *args: None):
    """retry Python decorator
    Credit:
    http://code.activestate.com/recipes/580745-retry-decorator-in-python/
    """
    def wrapper(function):
        def wrapped(*args, **kwargs):
            problems = []
            for delay in itertools.chain(delays, [None]):
                try:
                    return function(*args, **kwargs)
                except exception as problem:
                    problems.append(problem)
                    if delay is None:
                        report("retryable failed definitely:", problems)
                        # raise
                    else:
                        report("retryable failed:", problem,
                               "-- delaying for %ds" % delay)
                        time.sleep(delay)
            return None
        return wrapped
    return wrapper

def bootstrap(soffice=None,delays=(1, 3, 5, 7), report=lambda *args: None):
    # 4 connection attempts; sleeping 1, 3, 5 and 7 seconds
    # no report to console
    """Bootstrap OOo and PyUNO Runtime.
    The soffice process is started opening a named pipe of random name, then the local context is used
	to access the pipe. This function directly returns the remote component context, from whereon you can
	get the ServiceManager by calling getServiceManager() on the returned object.
	"""
    try:
       # soffice script used on *ix, Mac; soffice.exe used on Win
    The soffice process is started opening a named pipe of random name,
    then the local context is used to access the pipe. This function
    directly returns the remote component context, from whereon you can
    get the ServiceManager by calling getServiceManager() on the
    returned object.
    
    Examples:

    i.  Start LO as a service, get its remote component context
    
        import officehelper
        ctx = officehelper.bootstrap()
        # your code goes here
    
    ii. Wait longer for LO to start, request context multiples times
      + Report processing in console
    
        import officehelper as oh
        ctx = oh.bootstrap(delays=(5,10,15,20),report=print)  # every 5 sec.
        # your code goes here
    
    iii. Use a specific LibreOffice copy 
    
        from officehelper import bootstrap
        ctx = bootstrap(soffice='USB:\PortableApps\libO-7.6\App\libreoffice\program\soffice.exe'))
        # your code goes here

    """
    if soffice:  # soffice fully qualified path as parm
        sOffice = soffice
    else:  # soffice script used on *ix, Mac; soffice.exe used on Win
        if "UNO_PATH" in os.environ:
            sOffice = os.environ["UNO_PATH"]
        else:
            sOffice = "" # lets hope for the best
            sOffice = ""  # let's hope for the best
        sOffice = os.path.join(sOffice, "soffice")
        if platform.startswith("win"):
            sOffice += ".exe"
            sOffice = '"' + sOffice + '"'  # accommodate ' ' spaces in filename
        elif platform=="darwin":  # any other un-hardcoded suggestion?
            sOffice = "/Applications/LibreOffice.App/Contents/MacOS/soffice"
    #print(sOffice)

        # Generate a random pipe name.
        random.seed()
        sPipeName = "uno" + str(random.random())[2:]
    # Generate a random pipe name.
    random.seed()
    sPipeName = "uno" + str(random.random())[2:]

        # Start the office process, don't check for exit status since an exception is caught anyway if the office terminates unexpectedly.
        cmdArray = (sOffice, "--nologo", "--nodefault", "".join(["--accept=pipe,name=", sPipeName, ";urp;"]))
        os.spawnv(os.P_NOWAIT, sOffice, cmdArray)
    # Start the office process

        # ---------
    connect_string = ''.join(['pipe,name=', sPipeName, ';urp;'])
    command = ' '.join([sOffice, '--nologo', '--nodefault', '--accept="' + connect_string + '"'])
    #print(command)
    subprocess.Popen(command, shell=True)

        xLocalContext = uno.getComponentContext()
        resolver = xLocalContext.ServiceManager.createInstanceWithContext(
            "com.sun.star.bridge.UnoUrlResolver", xLocalContext)
        sConnect = "".join(["uno:pipe,name=", sPipeName, ";urp;StarOffice.ComponentContext"])
    # Connect to a started office instance

        # Wait until an office is started, but loop only nLoop times (can we do this better???)
        nLoop = 20
        while True:
            try:
                xContext = resolver.resolve(sConnect)
                break
            except NoConnectException:
                nLoop -= 1
                if nLoop <= 0:
                    raise BootstrapException("Cannot connect to soffice server.", None)
                sleep(0.5)  # Sleep 1/2 second.
    xLocalContext = uno.getComponentContext()
    resolver = xLocalContext.ServiceManager.createInstanceWithContext(
        "com.sun.star.bridge.UnoUrlResolver", xLocalContext)
    sConnect = "".join(['uno:', connect_string, 'StarOffice.ComponentContext'])

    except BootstrapException:
        raise
    except Exception as e:  # Any other exception
        raise BootstrapException("Caught exception " + str(e), None)
    @retry(delays=delays,
           exception=NoConnectException,
           report=report)
    def resolve():
        return resolver.resolve(sConnect)  # may raise NoConnectException

    return xContext
    ctx = resolve()
    if not ctx:
        raise BootstrapException
    return ctx

# vim: set shiftwidth=4 softtabstop=4 expandtab:

# ============
# Unit Testing
# ============

if __name__ == "__main__":

    # ~ dir(__name__)
    # ~ help(__name__)
    # ~ help(bootstrap)
    # ~ exit()

    #from officehelper import bootstrap, BootstrapException
    #from sys import platform
    #import subprocess, time

    try:
        ctx = bootstrap()
        # use delays=[0,] to raise BootstrapException

    except BootstrapException:  # stop soffice as a service
        if platform.startswith("win"):
            subprocess.Popen(['taskkill', '/f', '/t', '/im', 'soffice.exe'])
        elif platform == "linux":
            time.sleep(5)  
            subprocess.Popen(['killall', "soffice.bin"])
        elif platform == "darwin":
            time.sleep(15)
            subprocess.Popen(['pkill', '-f', 'LibreOffice'])
        raise BootstrapException()

    # your code goes here

    time.sleep(10)
    if ctx:  # stop soffice as a service
        smgr = ctx.getServiceManager()
        desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
        desktop.terminate()

# vim: set shiftwidth=4 softtabstop=4 expandtab
\ No newline at end of file