| #************************************************************************* |
| # |
| # $RCSfile: oood.py,v $ |
| # |
| # $Revision: 1.1 $ |
| # |
| # last change: $Author: jbu $ $Date: 2004/10/03 17:41:40 $ |
| # |
| # The Contents of this file are made available subject to the terms of |
| # either of the following licenses |
| # |
| # - GNU Lesser General Public License Version 2.1 |
| # - Sun Industry Standards Source License Version 1.1 |
| # |
| # Sun Microsystems Inc., October, 2000 |
| # |
| # GNU Lesser General Public License Version 2.1 |
| # ============================================= |
| # Copyright 2000 by Sun Microsystems, Inc. |
| # 901 San Antonio Road, Palo Alto, CA 94303, USA |
| # |
| # This library is free software; you can redistribute it and/or |
| # modify it under the terms of the GNU Lesser General Public |
| # License version 2.1, as published by the Free Software Foundation. |
| # |
| # This library is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| # Lesser General Public License for more details. |
| # |
| # You should have received a copy of the GNU Lesser General Public |
| # License along with this library; if not, write to the Free Software |
| # Foundation, Inc., 59 Temple Place, Suite 330, Boston, |
| # MA 02111-1307 USA |
| # |
| # |
| # Sun Industry Standards Source License Version 1.1 |
| # ================================================= |
| # The contents of this file are subject to the Sun Industry Standards |
| # Source License Version 1.1 (the "License"); You may not use this file |
| # except in compliance with the License. You may obtain a copy of the |
| # License at http://www.openoffice.org/license.html. |
| # |
| # Software provided under this License is provided on an "AS IS" basis, |
| # WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, |
| # WITHOUT LIMITATION, WARRANTIES THAT THE SOFTWARE IS FREE OF DEFECTS, |
| # MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE, OR NON-INFRINGING. |
| # See the License for the specific provisions governing your rights and |
| # obligations concerning the Software. |
| # |
| # The Initial Developer of the Original Code is: Joerg Budischewski |
| # |
| # Copyright: 2000 by Sun Microsystems, Inc. |
| # |
| # All Rights Reserved. |
| # |
| # Contributor(s): Joerg Budischewski |
| # |
| #************************************************************************* |
| import uno |
| import unohelper |
| import os |
| import time |
| import sys |
| import signal |
| import threading |
| import random |
| |
| from com.sun.star.bridge import XInstanceProvider |
| from com.sun.star.connection import NoConnectException, ConnectionSetupException |
| from com.sun.star.io import XStreamListener |
| from com.sun.star.lang import IllegalArgumentException |
| from com.sun.star.uno import RuntimeException |
| from com.sun.star.xml.sax import XDocumentHandler, InputSource |
| from com.sun.star.io import XInputStream |
| from com.sun.star.container import XNameAccess |
| from com.sun.star.beans import NamedValue |
| |
| SERIOUS = 0 |
| INFO = 1 |
| DETAIL = 2 |
| LEVEL2STRING = { SERIOUS : "SERIOUS", INFO : "INFO " , DETAIL : "DETAIL " } |
| COMMANDS = ( "run", "stop" , "status" ) |
| cmd = None |
| configfile = None |
| |
| def usage(): |
| print "usage: oood.py -c config-file run|stop|status" |
| print " daemon for openoffice" |
| |
| |
| i = 1 |
| while i < len(sys.argv): |
| if sys.argv[i] == "-c": |
| i = i + 1 |
| configfile = sys.argv[i] |
| elif sys.argv[i] in COMMANDS: |
| cmd = sys.argv[i] |
| else: |
| usage() |
| os._exit(1) |
| i = i + 1 |
| |
| if cmd == None or configfile == None: |
| usage() |
| os._exit(1) |
| |
| |
| class FileInputStream( XInputStream, unohelper.Base ): |
| def __init__( self, path ): |
| self.f = file( path ) |
| |
| def closeInput( self): |
| self.f.close() |
| |
| def skipBytes( self, nByteCount ): |
| self.f.read( nByteCount ) |
| |
| def readBytes( self, retSeq, nByteCount ): |
| s = self.f.read( nByteCount ) |
| return len( s ) , uno.ByteSequence( s ) |
| |
| def readSomeBytes( self, retSeq , nByteCount ): |
| #as we never block ! |
| return self.readBytes( retSeq, nByteCount ) |
| |
| def available( self ): |
| return 0 |
| |
| class Config: |
| def __init__( self ): |
| self.acceptor = "" |
| self.userInstallation = () |
| self.toleratedStartupTimePerInstance = 180 |
| self.maxUsageCountPerInstance = 10 |
| self.randomUsageCountPerInstance = 3 |
| self.loglevel = INFO |
| |
| def __str__(self): |
| return "acceptor="+self.acceptor+", userInstallation="+str(self.userInstallation) + \ |
| ", toleratedStartupTimePerInstance=" + str( \ |
| self.toleratedStartupTimePerInstance ) + ", maxUsageCountPerInstance="+ \ |
| str( self.maxUsageCountPerInstance ) + ", randomUsageCountPerInstance=" + \ |
| str( self.randomUsageCountPerInstance ) + ",loglevel= "+ str(self.loglevel) |
| |
| class ConfigHandler( XDocumentHandler, unohelper.Base ): |
| def __init__( self ): |
| pass |
| |
| def startDocument( self ): |
| self.config = Config() |
| |
| def endDocument( self ): |
| pass |
| |
| def startElement( self , name, attlist): |
| if name == "acceptor": |
| self.config.acceptor = attlist.getValueByIndex(0 ) |
| elif name == "admin-acceptor": |
| self.config.adminAcceptor = attlist.getValueByIndex(0 ) |
| elif name == "user-installation": |
| self.config.userInstallation = self.config.userInstallation + ( |
| attlist.getValueByName( "url" ), ) |
| elif name == "tolerated-startuptime-per-instance": |
| self.config.toleratedStartupTimePerInstance = int( |
| attlist.getValueByName( "value" ) ) |
| elif name == "usage-count-per-instance": |
| self.config.maxUsageCountPerInstance = int( |
| attlist.getValueByName( "max" ) ) |
| self.config.randomUsageCountPerInstance = int( |
| attlist.getValueByName( "random" ) ) |
| elif name == "logging": |
| l = attlist.getValueByName( "level" ) |
| if l == "info": |
| self.config.loglevel = INFO |
| elif l == "serious": |
| self.config.loglevel = SERIOUS |
| elif l == "detail": |
| self.config.loglevel = DETAIL |
| else: |
| raise RuntimeException( "Unknown loglevel " + l , None ) |
| |
| def endElement( self, name ): |
| pass |
| |
| def characters ( self, chars ): |
| pass |
| |
| def ignoreableWhitespace( self, chars ): |
| pass |
| |
| def setDocumentLocator( self, locator ): |
| pass |
| |
| def readConfiguration( path, parser ): |
| h = ConfigHandler() |
| parser.setDocumentHandler( h ) |
| parser.parseStream( |
| InputSource( FileInputStream( path ) , "", path, path ) ) |
| return h.config |
| |
| def namedValueTupleToMap( t ): |
| m = {} |
| for i in t: |
| m[ i.Name ] = i.Value |
| return m |
| |
| ctx = uno.getComponentContext() |
| config = readConfiguration( |
| configfile,ctx.ServiceManager.createInstance("com.sun.star.xml.sax.Parser")) |
| |
| if cmd == "stop": |
| uno.getComponentContext().ServiceManager.createInstance( |
| "com.sun.star.bridge.UnoUrlResolver").resolve( |
| "uno:"+config.adminAcceptor+";oood.Shutdown" ) |
| os._exit(0) |
| elif cmd == "status": |
| status = uno.getComponentContext().ServiceManager.createInstance( |
| "com.sun.star.bridge.UnoUrlResolver").resolve( |
| "uno:"+config.adminAcceptor+";oood.Status" ) |
| |
| if status == None: |
| print "Couldn't resolve status object" |
| print "Instances in daemon (free/total): " +str(status.getByName( "available" )) + \ |
| "/" + str( status.getByName( "poolsize" ) ) |
| |
| workers = status.getByName( "workers" ) |
| print "Worker\tpid\tin use\tusages\tduration\tuser-directory" |
| for i in workers: |
| out = "" |
| m = namedValueTupleToMap( i ) |
| inuse = " " |
| duration = " \t" |
| if m["usage-time"] > 0: |
| inuse = "x" |
| duration = str( round(m["usage-time" ],2) ) + "s \t" |
| |
| print str( m["index"]) + "\t" + \ |
| str( m["pid"] ) + " \t" + \ |
| inuse +"\t" + \ |
| str( m["usage"] ) + "\t" + \ |
| duration +\ |
| str( m["user-dir" ] ) |
| os._exit(0) |
| |
| NULL_DEVICE = "/dev/null" |
| processPool = None |
| |
| |
| class PoolAdderThread( threading.Thread ): |
| def __init__( self, process ): |
| threading.Thread.__init__( self ) |
| self.process = process |
| |
| def run( self ): |
| try: |
| if not self.process.restartWhenNecessary(): |
| logger.log( SERIOUS, "FATAL: could not restart worker " + |
| str(self.process) + ", terminating now !" ) |
| os._exit(1) |
| processPool.append( self.process ) |
| logger.log( INFO, processPool.getStateString()+" <- "+str(self.process)+ |
| " reenters pool" ) |
| except Exception,e: |
| logger.log( SERIOUS, str(e) ) |
| |
| |
| class Status( unohelper.Base, XNameAccess ): |
| def __init__( self, processList ): |
| self.map = {} |
| self.map[ "poolsize" ] = len( processList ) |
| available = 0 |
| workers = [] |
| for i in processList: |
| v = None |
| if i.timestamp == None: |
| v = NamedValue( "usage-time" , 0 ) |
| available = available + 1 |
| else: |
| v = NamedValue( "usage-time", time.time() - i.timestamp ) |
| |
| t = NamedValue( "pid", i.pid ), \ |
| NamedValue( "usage", i.usage ), \ |
| v, \ |
| NamedValue( "user-dir" , i.userid ), \ |
| NamedValue( "index", i.index ) |
| workers.append( t ) |
| self.map[ "workers" ] = tuple( workers ) |
| self.map[ "available" ] = available |
| |
| def getByName( self, name ): |
| if self.map.has_key( name ): |
| return self.map[ name ] |
| raise NoSuchElementException( "unknown element " + name, self ) |
| |
| def getElementNames( self ): |
| return tuple( self.map.keys() ) |
| |
| def hasByName( self , name ): |
| return self.map.has_key( name ) |
| |
| def getElementType( self ): |
| return Type() |
| |
| def hasElements( self ): |
| return True |
| |
| class ProcessPool: |
| def __init__( self ): |
| self.lst = [] |
| self.mutex = threading.Lock() |
| |
| def append( self , item ): |
| self.mutex.acquire() |
| self.lst.append( item ) |
| self.mutex.release() |
| |
| def initializationFinished( self ): |
| self.all = tuple( self.lst ) |
| |
| def size( self ): |
| return len( self.lst ) |
| |
| def terminate( self ): |
| for i in self.all: |
| i.terminate() |
| |
| def pop( self ): |
| ret = None |
| while ret == None: |
| self.mutex.acquire() |
| if len(self.lst) == 0: |
| self.mutex.release() |
| break |
| ret = self.lst.pop(0) |
| self.mutex.release() |
| if not ret.isResponsive(): |
| # process has died inbetween |
| PoolAdderThread( ret ).start() |
| ret = None |
| return ret |
| |
| def waitTillReady( self ): |
| for i in self.lst: |
| if not i.waitTillReady( |
| self.size() * config.toleratedStartupTimePerInstance ): |
| os._exit(1) |
| def getStateString( self): |
| global config |
| return "{" + str(self.size()) + "/" + str(len(config.userInstallation)) + "}" |
| |
| class Logger: |
| def __init__( self , out, level ): |
| self.out = out |
| self.level = level |
| |
| def log( self, level , text ): |
| if level <= self.level: |
| self.out.write( |
| time.asctime() + " ["+ |
| LEVEL2STRING[level] +"]: " + text + "\n") |
| |
| processPool = ProcessPool() |
| random.seed() |
| logger = Logger( sys.stdout, config.loglevel ) |
| shutdownThread = None |
| |
| acceptor = ctx.ServiceManager.createInstance( |
| "com.sun.star.connection.Acceptor" ) |
| bridgefactory = ctx.ServiceManager.createInstance( |
| "com.sun.star.bridge.BridgeFactory" ) |
| connector = ctx.ServiceManager.createInstance( |
| "com.sun.star.connection.Connector" ) |
| |
| def getConnectString( index ): |
| return "pipe,name=oood-instance-" + str(index) |
| |
| class AdminInstanceProvider( unohelper.Base, XInstanceProvider ): |
| def getInstance( self, name ): |
| object = None |
| if name == "oood.Shutdown": |
| global shutdownThread |
| shutdownThread = threading.Timer( 1.0, shutdown , (0,processPool) ) |
| shutdownThread.start() |
| elif name == "oood.Status": |
| object = Status( processPool.all ) |
| else: |
| logger.log( DETAIL, "AdminInstanceProvider: Unknown object " +name ) |
| return object |
| |
| class AdminAcceptorThread( threading.Thread ): |
| def __init__( self , ctx, acceptString ): |
| threading.Thread.__init__(self) |
| self.ctx = ctx |
| self.acceptString = acceptString |
| self.acceptor = self.ctx.ServiceManager.createInstance( |
| "com.sun.star.connection.Acceptor") |
| |
| def run( self ): |
| logger.log( INFO, "Admin thread started" ) |
| while True: |
| c = self.acceptor.accept( self.acceptString ) |
| if c == None: |
| break |
| logger.log( DETAIL, "Accepted admin connection from "+ |
| extractContactInfo(c.getDescription())) |
| bridgefactory.createBridge( "", "urp", c, AdminInstanceProvider() ) |
| |
| logger.log( INFO, "Admin thread terminating" ) |
| |
| def cancel( self ): |
| self.acceptor.stopAccepting( ) |
| |
| class TerminateThread( threading.Thread ): |
| def __init__( self, ctx ): |
| threading.Thread.__init__( self ) |
| self.ctx = ctx |
| |
| def run( self ): |
| try: |
| self.ctx.ServiceManager.createInstance( "com.sun.star.frame.Desktop").terminate() |
| except Exception: |
| pass |
| |
| class ResponsivenessChecker( threading.Thread ): |
| def __init__( self, process ): |
| threading.Thread.__init__(self) |
| self.process = process |
| self.responsive = False |
| |
| def isResponsive( self ): |
| self.join( 4 ) |
| return self.responsive |
| |
| def run( self ): |
| try: |
| # still alive ? |
| smgr = self.process.ctx.ServiceManager |
| desktop = smgr.createInstance( "com.sun.star.frame.Desktop" ) |
| |
| # check for typical solar-mutex deadlock |
| desktop.getCurrentComponent() |
| |
| # more checks may be added |
| self.responsive = True |
| |
| except Exception,e: |
| logger.log( SERIOUS, "responsiveness-check for " + str( self.process) + |
| " failed: " + str(e) ) |
| |
| def calculateMaxUsageCount(): |
| return config.maxUsageCountPerInstance + \ |
| ( 1. - 2*random.random()) * config.randomUsageCountPerInstance |
| |
| def shutdown( returncode , pool ): |
| acceptor.stopAccepting() |
| pool.terminate() |
| |
| class OfficeProcess: |
| def __init__( self , userid, index): |
| self.userid = userid |
| self.index = index |
| self.pid = None |
| self.usage = 0 |
| self.timestamp = None |
| self.bridge = None |
| self.ctx = None |
| |
| def start(self): |
| self.pid = os.spawnlp( |
| os.P_NOWAIT, |
| "soffice", |
| "" , # what is this for a string ? |
| "-env:UserInstallation="+self.userid , |
| "-headless", |
| "-norestore", |
| "-invisible", |
| "-accept="+getConnectString(self.index)+ ";urp;" ) |
| |
| def kill( self ): |
| if self.pid: |
| os.kill( self.pid, signal.SIGKILL ) |
| logger.log( INFO, str( self ) + " killed" ) |
| |
| def isAlive( self ): |
| return os.system( "ps -p " + str( self.pid ) + " >" + NULL_DEVICE ) == 0 |
| |
| def terminate( self ): |
| if self.ctx: |
| t = TerminateThread( self.ctx ) |
| logger.log( INFO, "terminating " +str( self ) ) |
| t.start() |
| t.join( 4 ) |
| if t.isAlive(): |
| logger.log( |
| SERIOUS, repr( self ) + " did not react on terminate, killing instance" ) |
| self.kill() |
| else: |
| logger.log( INFO, str( self ) + " terminated" ) |
| self.ctx = None |
| |
| def terminateAndRestart( self ): |
| self.terminate() |
| time.sleep( 4 ) |
| self.start() |
| if not self.waitTillReady( config.toleratedStartupTimePerInstance ): |
| logger.log( SERIOUS, "could not restart instance "+str(self)+", terminating" ) |
| return False |
| self.usage = 0 |
| return True |
| |
| def tryConnect( self ): |
| try: |
| con = connector.connect( getConnectString( self.index ) ) |
| self.bridge = bridgefactory.createBridge( "", "urp" , con, None ) |
| self.ctx = self.bridge.getInstance( "StarOffice.ComponentContext" ) |
| return self.ctx != None |
| except NoConnectException,e: |
| logger.log( DETAIL, str(self)+ " not yet responsive" ) |
| except Exception,e: |
| logger.log( SERIOUS , "couldn't connect to instance ("+str(e)+")" ) |
| return False |
| |
| def waitTillReady( self , timeout ): |
| start = time.time() |
| while not self.tryConnect( ) and time.time()-start < timeout: |
| time.sleep( 4 ) |
| |
| if time.time() - start > timeout: |
| return False |
| return True |
| |
| def __str__(self): |
| return "Worker-" + str(self.index) + "("+str(self.usage)+" uses)" |
| |
| def __repr__(self): |
| return "<oood.OfficeProcess %s;pid=%d;connectStr=%s,usage=%d>" % \ |
| (self.userid,self.pid,getConnectString(self.index),self.usage) |
| |
| def startUsage( self ): |
| self.usage = self.usage + 1 |
| self.timestamp = time.time() |
| |
| def getUsageDuration( self ): |
| return time.time() - self.timestamp |
| |
| def endUsage( self ): |
| self.timestamp = None |
| |
| def isResponsive( self ): |
| t = ResponsivenessChecker( self ) |
| t.start() |
| return t.isResponsive() |
| |
| def restartWhenNecessary( self ): |
| if not self.isResponsive(): |
| logger.log( INFO, "process " + str(self) +\ |
| " not responsive anymore, restarting" ) |
| self.usage = 0 |
| return self.terminateAndRestart() |
| |
| if self.usage >= calculateMaxUsageCount(): |
| logger.log( INFO, "max usage count for instance " + str(self) +\ |
| " reached, restarting" ) |
| return self.terminateAndRestart() |
| return True |
| |
| |
| |
| class ConnectionListener( unohelper.Base, XStreamListener ): |
| def __init__( self , officeProcess, conDesc ): |
| self.officeProcess = officeProcess |
| self.conDesc = conDesc |
| |
| def clear( self ): |
| if self.officeProcess != None: |
| logger.log( INFO, self.conDesc + " disconnects from " + |
| str( self.officeProcess ) + " (used for "+ |
| str(round(self.officeProcess.getUsageDuration(),1)) +"s) " ) |
| self.officeProcess.endUsage( ) |
| PoolAdderThread( self.officeProcess ).start() |
| self.officeProcess = None |
| |
| def started( self ): |
| pass |
| |
| def closed( self ): |
| self.clear() |
| def terminated( self ): |
| self.clear() |
| def error( self , exception ): |
| self.clear() |
| |
| |
| class OfficeInstanceProvider( unohelper.Base, XInstanceProvider ): |
| def __init__( self, office ): |
| self.office = office |
| |
| def getInstance( self, name ): |
| logger.log( DETAIL, "resolving name " +name ) |
| object = self.office.bridge.getInstance( name ) |
| return object |
| |
| class EmptyPoolInstanceProvider( unohelper.Base, XInstanceProvider ): |
| def getInstance( self, name ): |
| return None |
| # raise RuntimeException( "No office instance available, try later" , None ) |
| |
| def extractContactInfo( namevalue ): |
| lst = namevalue.split( "," ) |
| host = "" |
| port = "" |
| for i in lst: |
| if i.startswith( "peerHost" ): |
| host = i.split("=")[1] |
| elif i.startswith( "peerPort" ): |
| port = i.split("=")[1] |
| return host + ":" + port |
| |
| |
| logger.log( SERIOUS, "Started on pid " + str( os.getpid() ) ) |
| |
| |
| index = 0 |
| logger.log( INFO, "Starting office workers ..." ) |
| |
| for i in config.userInstallation: |
| office = OfficeProcess( i , index ) |
| office.start() |
| logger.log( INFO, "Worker-" +str(index) + ":" + repr(office) +" started") |
| processPool.append( office ) |
| index = index + 1 |
| |
| processPool.waitTillReady() |
| processPool.initializationFinished() |
| |
| adminThread = AdminAcceptorThread( ctx, config.adminAcceptor ) |
| adminThread.start() |
| |
| logger.log( INFO , processPool.getStateString()+ " WorkerAll instances started" ) |
| logger.log( SERIOUS, "Accepting on " + config.acceptor ) |
| |
| while True: |
| con = acceptor.accept( config.acceptor ) |
| if con == None: |
| break |
| |
| conDesc = extractContactInfo(con.getDescription()) |
| logger.log( INFO , "Incoming request for a worker from " + conDesc ) |
| process = processPool.pop() |
| if process == None: |
| logger.log( SERIOUS, processPool.getStateString()+" " + conDesc + |
| " rejected, all workers are busy" ) |
| bridgefactory.createBridge( |
| "", "urp", con , EmptyPoolInstanceProvider( ) ) |
| else: |
| process.startUsage() |
| logger.log( INFO, processPool.getStateString()+" -> " + |
| str(process) +" serves "+conDesc) |
| |
| con.addStreamListener( ConnectionListener(process,conDesc) ) |
| bridgefactory.createBridge( |
| "", "urp", con , OfficeInstanceProvider( process ) ) |
| |
| logger.log( SERIOUS, "Accepting on " + config.acceptor + |
| " stopped, waiting for shutdownthread") |
| |
| adminThread.cancel() |
| |
| if shutdownThread != None: |
| shutdownThread.join() |
| |
| if adminThread != None: |
| adminThread.join() |
| |
| logger.log( SERIOUS, "Terminating normally") |
| |
| |