Source code for msl.loadlib.server32

"""
Contains the base class for loading a 32-bit shared library in 32-bit Python.

The :class:`~.server32.Server32` class is used in combination with the
:class:`~.client64.Client64` class to communicate with a 32-bit shared library
from 64-bit Python.
"""
import json
import os
import re
import subprocess
import sys
import threading
import traceback
import warnings
try:
    from http.server import BaseHTTPRequestHandler
    from http.server import HTTPServer
    import pickle
except ImportError:  # then Python 2
    from BaseHTTPServer import BaseHTTPRequestHandler
    from BaseHTTPServer import HTTPServer
    import cPickle as pickle

from . import IS_WINDOWS
from . import LoadLibrary
from . import SERVER_FILENAME

METADATA = '-METADATA-'
SHUTDOWN = '-SHUTDOWN-'
OK = 200
ERROR = 500


[docs]class Server32(HTTPServer): def __init__(self, path, libtype, host, port, *args, **kwargs): """Base class for loading a 32-bit library in 32-bit Python. All modules that are to be run on the 32-bit server must contain a class that is inherited from this class and the module can import **any** of the `standard`_ python modules **except** for :mod:`distutils`, :mod:`ensurepip`, :mod:`tkinter` and :mod:`turtle`. All modules that are run on the 32-bit server must be able to run on the Python interpreter that the server is running on, see :meth:`.version` for how to determine the version of the Python interpreter. .. _standard: https://docs.python.org/3/py-modindex.html .. _JVM: https://en.wikipedia.org/wiki/Java_virtual_machine Parameters ---------- path : :class:`str` The path to the 32-bit library. See :class:`.LoadLibrary` for more details. libtype : :class:`str` The library type. See :class:`.LoadLibrary` for more details. .. note:: Since Java byte code is executed on the JVM_ it does not make sense to use :class:`Server32` for a Java ``.jar`` or ``.class`` file. host : :class:`str` The IP address of the server. port : :class:`int` The port to run the server on. *args All additional arguments are currently ignored. **kwargs All keyword arguments are passed to :class:`.LoadLibrary`. """ self._library = LoadLibrary(path, libtype=libtype, **kwargs) self._assembly = self._library.assembly self._lib = self._library.lib self._path = self._library.path super(Server32, self).__init__((host, int(port)), _RequestHandler, bind_and_activate=False) @property def assembly(self): """ Returns a reference to the `.NET Runtime Assembly <NET_>`_ object if the shared library is a .NET Framework otherwise returns :data:`None`. .. tip:: The `JetBrains dotPeek`_ program can be used to reliably decompile any .NET Assembly into the equivalent source code. .. _NET: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly .. _JetBrains dotPeek: https://www.jetbrains.com/decompiler/ """ return self._assembly @property def lib(self): """Returns the reference to the 32-bit, loaded-library object. For example, if `libtype` is * ``'cdll'`` then a :class:`~ctypes.CDLL` object * ``'windll'`` then a :class:`~ctypes.WinDLL` object * ``'oledll'`` then a :class:`~ctypes.OleDLL` object * ``'net'`` or ``'clr'`` then a :class:`~.load_library.DotNet` object * ``'com'`` or ``'activex'`` then an interface pointer to the COM_ object .. _COM: https://en.wikipedia.org/wiki/Component_Object_Model """ return self._lib @property def path(self): """:class:`str`: The path to the shared library file.""" return self._path
[docs] @staticmethod def version(): """Gets the version of the Python interpreter that the 32-bit server is running on. Returns ------- :class:`str` The result of executing ``'Python ' + sys.version`` on the 32-bit server. .. invisible-code-block: pycon >>> SKIP_IF_MACOS() Examples -------- :: >>> from msl.loadlib import Server32 >>> Server32.version() 'Python 3.11.4 ...' Note ---- This method takes about 1 second to finish because the 32-bit server needs to start in order to determine the version of the Python interpreter. """ exe = os.path.join(os.path.dirname(__file__), SERVER_FILENAME) pipe = subprocess.Popen([exe, '--version'], stdout=subprocess.PIPE) return pipe.communicate()[0].decode().strip()
[docs] @staticmethod def interactive_console(): """Start an interactive console. This method starts an interactive console, in a new terminal, with the Python interpreter on the 32-bit server. Examples -------- :: >>> from msl.loadlib import Server32 # doctest: +SKIP >>> Server32.interactive_console() # doctest: +SKIP """ exe = os.path.join(os.path.dirname(__file__), SERVER_FILENAME) if IS_WINDOWS: cmd = 'start "msl.loadlib.Server32 || interactive console" "{exe}" --interactive' else: cmd = "gnome-terminal --command='{exe} --interactive'" os.system(cmd.format(exe=exe))
@property def quiet(self): """This attribute is no longer used, it will be removed in a future release. Returns :data:`True`. """ warnings.simplefilter('once', DeprecationWarning) warnings.warn( 'The `quiet` attribute for Server32 will be removed in a future release -- always returns True', DeprecationWarning, stacklevel=2 ) return True
[docs] @staticmethod def remove_site_packages_64bit(): """Remove the site-packages directory from the 64-bit process. By default, the site-packages directory of the 64-bit process is included in :data:`sys.path` of the 32-bit process. Having the 64-bit site-packages directory available can sometimes cause issues. For example, comtypes imports numpy so if numpy is installed in the 64-bit process then comtypes will import the 64-bit version of numpy in the 32-bit process. Depending on the version of Python and/or numpy this can cause the 32-bit server to crash. .. versionadded:: 0.9 Examples -------- :: import sys from msl.loadlib import Server32 class FileSystem(Server32): def __init__(self, host, port, **kwargs): # remove the site-packages directory that was passed from 64-bit Python # before calling the super() function to load the COM library path = Server32.remove_site_packages_64bit() super(FileSystem, self).__init__('Scripting.FileSystemObject', 'com', host, port) # optional: add the site-packages directory back into sys.path sys.path.append(path) Returns ------- :class:`str` The path of the site-packages directory that was removed. Can be an empty string if the directory was not found in :data:`sys.path`. """ for index, path in enumerate(sys.path): if path.endswith('site-packages'): return sys.path.pop(index) return ''
[docs] @staticmethod def is_interpreter(): """Check if code is running on the 32-bit server. If the same module is executed by both :class:`~msl.loadlib.client64.Client64` and :class:`.Server32` then there may be only parts of the code that should be executed by the correct bitness of the Python interpreter. .. versionadded:: 0.9 Returns ------- :class:`bool` Whether the code is running on the 32-bit server. Examples -------- :: import sys from msl.loadlib import Server32 if Server32.is_interpreter(): # this only gets executed on the 32-bit server assert sys.maxsize < 2**32 """ return sys.executable.endswith(SERVER_FILENAME)
[docs] @staticmethod def examples_dir(): """Get the directory where the example libraries are located. .. versionadded:: 0.9 Returns ------- :class:`str` The directory where the example libraries are located. """ if Server32.is_interpreter(): root = os.path.dirname(sys.executable) else: root = os.path.dirname(__file__) path = os.path.join(root, os.pardir, 'examples', 'loadlib') return os.path.abspath(path)
[docs] def shutdown_handler(self): """Proxy function that is called immediately prior to the server shutting down. The intended use case is for the server to do any necessary cleanup, such as stopping locally started threads or closing file handles before it shuts down. .. versionadded:: 0.6 """ pass
class _RequestHandler(BaseHTTPRequestHandler): """Handles a request that was sent to the 32-bit server.""" def do_GET(self): """Handle a GET request.""" try: if self.path == METADATA: response = { 'path': self.server.path, 'pid': os.getpid(), 'unfrozen_dir': sys._MEIPASS, } else: with open(self.server.pickle_path, mode='rb') as f: args = pickle.load(f) kwargs = pickle.load(f) attr = getattr(self.server, self.path) if callable(attr): response = attr(*args, **kwargs) else: response = attr with open(self.server.pickle_path, mode='wb') as f: pickle.dump(response, f, protocol=self.server.pickle_protocol) self.send_response(OK) self.end_headers() except Exception as e: print('{}: {}'.format(e.__class__.__name__, e)) exc_type, exc_value, exc_traceback = sys.exc_info() tb_list = traceback.extract_tb(exc_traceback) tb = tb_list[min(len(tb_list)-1, 1)] # get the Server32 subclass exception response = {'name': exc_type.__name__, 'value': str(exc_value)} traceback_ = ' File {!r}, line {}, in {}'.format(tb[0], tb[1], tb[2]) if tb[3]: traceback_ += '\n {}'.format(tb[3]) response['traceback'] = traceback_ self.send_response(ERROR) self.end_headers() self.wfile.write(json.dumps(response).encode(encoding='utf-8', errors='ignore')) def do_POST(self): """Handle a POST request.""" if self.path == SHUTDOWN: self.server.shutdown_handler() threading.Thread(target=self.server.shutdown).start() else: # the pickle info match = re.match(r'protocol=(\d+)&path=(.*)', self.path) if match: self.server.pickle_protocol = int(match.group(1)) self.server.pickle_path = match.group(2) code = OK else: code = ERROR self.send_response(code) self.end_headers() def log_message(self, fmt, *args): """ Overrides: :meth:`~http.server.BaseHTTPRequestHandler.log_message` Ignore all log messages from being displayed in :data:`sys.stdout`. """ pass