diff options
Diffstat (limited to 'pyuno/source/loader')
-rw-r--r-- | pyuno/source/loader/pythonloader.component | 26 | ||||
-rw-r--r-- | pyuno/source/loader/pythonloader.py | 163 | ||||
-rw-r--r-- | pyuno/source/loader/pyuno_loader.cxx | 278 |
3 files changed, 467 insertions, 0 deletions
diff --git a/pyuno/source/loader/pythonloader.component b/pyuno/source/loader/pythonloader.component new file mode 100644 index 000000000..4fc4a255c --- /dev/null +++ b/pyuno/source/loader/pythonloader.component @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 . + --> + +<component loader="com.sun.star.loader.SharedLibrary" environment="@CPPU_ENV@" + xmlns="http://openoffice.org/2010/uno-components"> + <implementation name="org.openoffice.comp.pyuno.Loader" + constructor="pyuno_Loader_get_implementation" single-instance="true"> + <service name="com.sun.star.loader.Python"/> + </implementation> +</component> diff --git a/pyuno/source/loader/pythonloader.py b/pyuno/source/loader/pythonloader.py new file mode 100644 index 000000000..02f901942 --- /dev/null +++ b/pyuno/source/loader/pythonloader.py @@ -0,0 +1,163 @@ +# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This file incorporates work covered by the following license notice: +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed +# with this work for additional information regarding copyright +# ownership. The ASF licenses this file to you under the Apache +# License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 . +# +import uno +import unohelper +import sys +import types +import os +from urllib.parse import unquote +from com.sun.star.uno import Exception,RuntimeException +from com.sun.star.loader import XImplementationLoader +from com.sun.star.lang import XServiceInfo + +MODULE_PROTOCOL = "vnd.openoffice.pymodule:" +DEBUG = 0 + +g_supportedServices = "com.sun.star.loader.Python", # referenced by the native C++ loader ! +g_implementationName = "org.openoffice.comp.pyuno.Loader" # referenced by the native C++ loader ! + +def splitUrl( url ): + nColon = url.find( ":" ) + if -1 == nColon: + raise RuntimeException( "PythonLoader: No protocol in url " + url, None ) + return url[0:nColon], url[nColon+1:len(url)] + +g_loadedComponents = {} +def checkForPythonPathBesideComponent( url ): + path = unohelper.fileUrlToSystemPath( url+"/pythonpath.zip" ); + if DEBUG == 1: + print(b"checking for existence of " + encfile( path )) + if 1 == os.access( encfile( path ), os.F_OK) and not path in sys.path: + if DEBUG == 1: + print(b"adding " + encfile( path ) + b" to sys.path") + sys.path.append( path ) + + path = unohelper.fileUrlToSystemPath( url+"/pythonpath" ); + if 1 == os.access( encfile( path ), os.F_OK) and not path in sys.path: + if DEBUG == 1: + print(b"adding " + encfile( path ) + b" to sys.path") + sys.path.append( path ) + +def encfile(uni): + return uni.encode( sys.getfilesystemencoding()) + +class Loader( XImplementationLoader, XServiceInfo, unohelper.Base ): + def __init__(self, ctx ): + if DEBUG: + print("pythonloader.Loader ctor") + self.ctx = ctx + + def getModuleFromUrl( self, url ): + if DEBUG: + print("pythonloader: interpreting url " + url) + protocol, dependent = splitUrl( url ) + if "vnd.sun.star.expand" == protocol: + exp = self.ctx.getValueByName( "/singletons/com.sun.star.util.theMacroExpander" ) + url = exp.expandMacros(unquote(dependent)) + protocol,dependent = splitUrl( url ) + + if DEBUG: + print("pythonloader: after expansion " + protocol + ":" + dependent) + + try: + if "file" == protocol: + # remove \..\ sequence, which may be useful e.g. in the build env + url = unohelper.absolutize( url, url ) + + # did we load the module already ? + mod = g_loadedComponents.get( url ) + if not mod: + mod = types.ModuleType("uno_component") + + # check for pythonpath.zip beside .py files + checkForPythonPathBesideComponent( url[0:url.rfind('/')] ) + + # read the file + filename = unohelper.fileUrlToSystemPath( url ) + + with open( filename, encoding='utf_8' ) as fileHandle: + src = fileHandle.read().replace("\r","") + if not src.endswith( "\n" ): + src = src + "\n" + + # compile and execute the module + codeobject = compile( src, encfile(filename), "exec" ) + mod.__file__ = filename + exec(codeobject, mod.__dict__) + g_loadedComponents[url] = mod + return mod + elif "vnd.openoffice.pymodule" == protocol: + nSlash = dependent.rfind('/') + if -1 != nSlash: + path = unohelper.fileUrlToSystemPath( dependent[0:nSlash] ) + dependent = dependent[nSlash+1:len(dependent)] + if not path in sys.path: + sys.path.append( path ) + mod = __import__( dependent ) + path_component, dot, rest = dependent.partition('.') + while dot == '.': + path_component, dot, rest = rest.partition('.') + mod = getattr(mod, path_component) + return mod + else: + if DEBUG: + print("Unknown protocol '" + protocol + "'"); + raise RuntimeException( "PythonLoader: Unknown protocol " + + protocol + " in url " +url, self ) + except Exception as e: + if DEBUG: + print ("Python import exception " + str(type(e)) + + " message " + str(e) + " args " + str(e.args)); + raise RuntimeException( "Couldn't load " + url + " for reason " + str(e), None ) + return None + + def activate( self, implementationName, dummy, locationUrl, regKey ): + if DEBUG: + print("pythonloader.Loader.activate") + + mod = self.getModuleFromUrl( locationUrl ) + implHelper = mod.__dict__.get( "g_ImplementationHelper" , None ) + if DEBUG: + print ("Fetched ImplHelper as " + str(implHelper)) + if implHelper is None: + return mod.getComponentFactory( implementationName, self.ctx.ServiceManager, regKey ) + else: + return implHelper.getComponentFactory( implementationName,regKey,self.ctx.ServiceManager) + + def writeRegistryInfo( self, regKey, dummy, locationUrl ): + if DEBUG: + print( "pythonloader.Loader.writeRegistryInfo" ) + + mod = self.getModuleFromUrl( locationUrl ) + implHelper = mod.__dict__.get( "g_ImplementationHelper" , None ) + if implHelper is None: + return mod.writeRegistryInfo( self.ctx.ServiceManager, regKey ) + else: + return implHelper.writeRegistryInfo( regKey, self.ctx.ServiceManager ) + + def getImplementationName( self ): + return g_implementationName + + def supportsService( self, ServiceName ): + return ServiceName in self.getSupportedServiceNames() + + def getSupportedServiceNames( self ): + return g_supportedServices + +# vim: set shiftwidth=4 softtabstop=4 expandtab: diff --git a/pyuno/source/loader/pyuno_loader.cxx b/pyuno/source/loader/pyuno_loader.cxx new file mode 100644 index 000000000..da0467f45 --- /dev/null +++ b/pyuno/source/loader/pyuno_loader.cxx @@ -0,0 +1,278 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 . + */ + +#include <config_features.h> +#include <config_folders.h> + +#include <pyuno.hxx> + +#include <o3tl/any.hxx> +#include <o3tl/char16_t2wchar_t.hxx> + +#include <osl/process.h> +#include <osl/file.hxx> +#include <osl/thread.h> + +#include <rtl/ustrbuf.hxx> +#include <rtl/bootstrap.hxx> + +#include <cppuhelper/factory.hxx> + +#include <com/sun/star/uno/XComponentContext.hpp> + +// apparently PATH_MAX is not standard and not defined by MSVC +#ifndef PATH_MAX +#ifdef _MAX_PATH +#define PATH_MAX _MAX_PATH +#else +#ifdef MAX_PATH +#define PATH_MAX MAX_PATH +#else +#error no PATH_MAX +#endif +#endif +#endif + +using pyuno::PyRef; +using pyuno::NOT_NULL; +using pyuno::Runtime; +using pyuno::PyThreadAttach; + +using com::sun::star::uno::Reference; +using com::sun::star::uno::XInterface; +using com::sun::star::uno::Sequence; +using com::sun::star::uno::XComponentContext; +using com::sun::star::uno::RuntimeException; + +namespace pyuno_loader +{ + +/// @throws RuntimeException +static void raiseRuntimeExceptionWhenNeeded() +{ + if( PyErr_Occurred() ) + { + PyRef excType, excValue, excTraceback; + PyErr_Fetch(reinterpret_cast<PyObject **>(&excType), reinterpret_cast<PyObject**>(&excValue), reinterpret_cast<PyObject**>(&excTraceback)); + Runtime runtime; + css::uno::Any a = runtime.extractUnoException( excType, excValue, excTraceback ); + OUStringBuffer buf; + buf.append( "python-loader:" ); + if( auto e = o3tl::tryAccess<css::uno::Exception>(a) ) + buf.append( e->Message ); + throw RuntimeException( buf.makeStringAndClear() ); + } +} + +/// @throws RuntimeException +static PyRef getLoaderModule() +{ + PyRef module( + PyImport_ImportModule( "pythonloader" ), + SAL_NO_ACQUIRE ); + raiseRuntimeExceptionWhenNeeded(); + if( !module.is() ) + { + throw RuntimeException( "pythonloader: Couldn't load pythonloader module" ); + } + return PyRef( PyModule_GetDict( module.get() )); +} + +/// @throws RuntimeException +static PyRef getObjectFromLoaderModule( const char * func ) +{ + PyRef object( PyDict_GetItemString(getLoaderModule().get(), func ) ); + if( !object.is() ) + { + throw RuntimeException( "pythonloader: couldn't find core element pythonloader." + + OUString::createFromAscii( func )); + } + return object; +} + +static void setPythonHome ( const OUString & pythonHome ) +{ + OUString systemPythonHome; + osl_getSystemPathFromFileURL( pythonHome.pData, &(systemPythonHome.pData) ); + // static because Py_SetPythonHome just copies the "wide" pointer + static wchar_t wide[PATH_MAX + 1]; +#if defined _WIN32 + const size_t len = systemPythonHome.getLength(); + if (len < std::size(wide)) + wcsncpy(wide, o3tl::toW(systemPythonHome.getStr()), len + 1); +#else + OString o = OUStringToOString(systemPythonHome, osl_getThreadTextEncoding()); + size_t len = mbstowcs(wide, o.pData->buffer, PATH_MAX + 1); + if(len == size_t(-1)) + { + PyErr_SetString(PyExc_SystemError, "invalid multibyte sequence in python home path"); + return; + } +#endif + if(len >= PATH_MAX + 1) + { + PyErr_SetString(PyExc_SystemError, "python home path is too long"); + return; + } + Py_SetPythonHome(wide); +} + +static void prependPythonPath( std::u16string_view pythonPathBootstrap ) +{ + OUStringBuffer bufPYTHONPATH( 256 ); + bool bAppendSep = false; + sal_Int32 nIndex = 0; + while( true ) + { + size_t nNew = pythonPathBootstrap.find( ' ', nIndex ); + std::u16string_view fileUrl; + if( nNew == std::u16string_view::npos ) + { + fileUrl = pythonPathBootstrap.substr(nIndex); + } + else + { + fileUrl = pythonPathBootstrap.substr(nIndex, nNew - nIndex); + } + OUString systemPath; + osl_getSystemPathFromFileURL( OUString(fileUrl).pData, &(systemPath.pData) ); + if (!systemPath.isEmpty()) + { + if (bAppendSep) + bufPYTHONPATH.append(static_cast<sal_Unicode>(SAL_PATHSEPARATOR)); + bufPYTHONPATH.append(systemPath); + bAppendSep = true; + } + if( nNew == std::u16string_view::npos ) + break; + nIndex = nNew + 1; + } + const char * oldEnv = getenv( "PYTHONPATH"); + if( oldEnv ) + { + if (bAppendSep) + bufPYTHONPATH.append( static_cast<sal_Unicode>(SAL_PATHSEPARATOR) ); + bufPYTHONPATH.append( OUString(oldEnv, strlen(oldEnv), osl_getThreadTextEncoding()) ); + } + + OUString envVar("PYTHONPATH"); + OUString envValue(bufPYTHONPATH.makeStringAndClear()); + osl_setEnvironment(envVar.pData, envValue.pData); +} + +namespace { + +void pythonInit() { + if ( Py_IsInitialized()) // may be inited by getComponentContext() already + return; + + OUString pythonPath; + OUString pythonHome; + OUString path( "$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE("pythonloader.uno" )); + rtl::Bootstrap::expandMacros(path); //TODO: detect failure + rtl::Bootstrap bootstrap(path); + + // look for pythonhome + bootstrap.getFrom( "PYUNO_LOADER_PYTHONHOME", pythonHome ); + bootstrap.getFrom( "PYUNO_LOADER_PYTHONPATH", pythonPath ); + + // pythonhome+pythonpath must be set before Py_Initialize(), otherwise there appear warning on the console + // sadly, there is no api for setting the pythonpath, we have to use the environment variable + if( !pythonHome.isEmpty() ) + setPythonHome( pythonHome ); + + if( !pythonPath.isEmpty() ) + prependPythonPath( pythonPath ); + +#ifdef _WIN32 + //extend PATH under windows to include the branddir/program so ssl libs will be found + //for use by terminal mailmerge dependency _ssl.pyd + OUString sEnvName("PATH"); + OUString sPath; + osl_getEnvironment(sEnvName.pData, &sPath.pData); + OUString sBrandLocation("$BRAND_BASE_DIR/program"); + rtl::Bootstrap::expandMacros(sBrandLocation); + osl::FileBase::getSystemPathFromFileURL(sBrandLocation, sBrandLocation); + sPath = OUStringBuffer(sPath). + append(static_cast<sal_Unicode>(SAL_PATHSEPARATOR)). + append(sBrandLocation).makeStringAndClear(); + osl_setEnvironment(sEnvName.pData, sPath.pData); +#endif + + PyImport_AppendInittab( "pyuno", PyInit_pyuno ); + +#if HAVE_FEATURE_READONLY_INSTALLSET + Py_DontWriteBytecodeFlag = 1; +#endif + + // initialize python + Py_Initialize(); +#if PY_VERSION_HEX < 0x03090000 + PyEval_InitThreads(); +#endif + + PyThreadState *tstate = PyThreadState_Get(); + PyEval_ReleaseThread( tstate ); +#if PY_VERSION_HEX < 0x030B0000 + // This tstate is never used again, so delete it here. + // This prevents an assertion in PyThreadState_Swap on the + // PyThreadAttach below. + PyThreadState_Delete(tstate); +#endif +} + +} + +extern "C" SAL_DLLPUBLIC_EXPORT css::uno::XInterface* +pyuno_Loader_get_implementation( + css::uno::XComponentContext* ctx , css::uno::Sequence<css::uno::Any> const&) +{ + // tdf#114815 init python only once, via single-instace="true" in pythonloader.component + pythonInit(); + + Reference< XInterface > ret; + + PyThreadAttach attach( PyInterpreterState_Head() ); + { + // note: this can't race against getComponentContext() because + // either (in soffice.bin) CreateInstance() must be called before + // getComponentContext() can be called, or (in python.bin) the other + // way around + if( ! Runtime::isInitialized() ) + { + Runtime::initialize( ctx ); + } + Runtime runtime; + + PyRef pyCtx = runtime.any2PyObject( + css::uno::Any( css::uno::Reference(ctx) ) ); + + PyRef clazz = getObjectFromLoaderModule( "Loader" ); + PyRef args ( PyTuple_New( 1 ), SAL_NO_ACQUIRE, NOT_NULL ); + PyTuple_SetItem( args.get(), 0 , pyCtx.getAcquired() ); + PyRef pyInstance( PyObject_CallObject( clazz.get() , args.get() ), SAL_NO_ACQUIRE ); + runtime.pyObject2Any( pyInstance ) >>= ret; + } + ret->acquire(); + return ret.get(); +} + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |