Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python Com Server unable to create instance when wrapped with py2exe - error object has no attribute

I've hit a wall with this one. I need to create a python based com server, package it as a windows exe and deploy it on windows. It has to have a "full" interface - because the consumer requires idispatch and a specific interfce to function. Now I have created the com server and have it running under the interpreter and it functions flawlessly with my picky client. However, when packaging as an EXE - its an localserver - I get an error in the log when the system tries to instantiate it (even from a vbs script). So here's everyting. I searched high and low in the itnernet and it looks like an import problem but I don't know how to import my own python object for the localserver to use.

This is python 2.7 with pywin32 extensions installed.

So first - the IDL I created for the server:

imtg.idl

// This file will be processed by the MIDL tool to
// produce the type library (imtg.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";
    [
        object,
        uuid(4fafbb23-6a38-4613-b93b-68ea66c67043),
        dual,
        helpstring("IImtGroupApp Interface"),
        pointer_default(unique)
    ]
    interface IImtGroupApp : IDispatch
    {
        [id(1), helpstring("method EchoString")] HRESULT EchoString([in] BSTR in1, [out, retval] BSTR *vals);
        [id(2), helpstring("method AddNumbers")] HRESULT AddNumbers([in] long in1, [in] long in2, [out, retval] long *vali);
    };
    [
    uuid(d665e9d0-71a9-4e23-a1b4-abe3376d5c58),
    version(1.0),
    helpstring("ImtGroup 1.0 Type Library")
]
library IMTGROUPLib
{
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");
    importlib("msado15.dll");

    [
        uuid(ced66424-93fb-4307-9062-7bee76d3d8eb),
        helpstring("ImtGroupApp Class")
    ]
    coclass ImtGroupApp {
        [default] interface IImtGroupApp;
    };
};

Next the Python code - now this gets a bit tricky because when I distribute this I don't want to create the .tlb - so I'm not distributing the .idy - just make sure you have the .tbl to register. Use an as admin cmd prompt if necessary.

imtg_server.py

import sys, os
import pythoncom
import win32com
import winerror
# importers check was old py2exe current uses frozen
if hasattr(sys, 'frozen'):
    # we are running as py2exe-packed executable
    print "is an exe"
    pythoncom.frozen = 1
else:
   print "not an exe"

class CImtg:
    _reg_clsctx_ = pythoncom.CLSCTX_LOCAL_SERVER
    #
    # COM declarations    
    #
    _reg_clsid_ = "{24c0e3fe-58e7-4485-87dc-9f9e823b85e1}"
    _reg_desc_ = "IMTGroup Python test object"
    _reg_progid_ = "ImtGroup.Test"
    if hasattr(sys, 'frozen'):
        # In the py2exe-packed version, specify the module.class
        # to use. In the python script version, python is able
        # to figure it out itself.
        _reg_class_spec_ = "__main__.CImtg"
        print "set reg_class_spec"
        print _reg_class_spec_
    ###
    ### Link to typelib - uuid matches uuid for type library in idl file
    _typelib_guid_ = '{d665e9d0-71a9-4e23-a1b4-abe3376d5c58}'
    _typelib_version_ = 1, 0
    _com_interfaces_ = ['IImtGroupApp']

    def __init__(self):
    ### initialize something here if necessary
    ### The item below is not used in this example
        self.MyProp1 = 10

    def EchoString(self,in1):
        return "Echoing " + in1

    def AddNumbers(self, in1, in2):
        return in1 + in2

def BuildTypelib():
    from distutils.dep_util import newer
    this_dir = os.path.dirname(__file__)
    idl = os.path.abspath(os.path.join(this_dir, "imtg.idl"))
    tlb=os.path.splitext(idl)[0] + '.tlb'
    if os.path.isfile(idl): 
      # test for idl - if no idl don't create tlb assume its there
      # Comment below for building exe as we will have type library
      if newer(idl, tlb):
         print "Compiling %s" % (idl,)
         rc = os.system ('midl "%s"' % (idl,))
         if rc:
             raise RuntimeError("Compiling MIDL failed!")
        # Can't work out how to prevent MIDL from generating the stubs.
        # just nuke them
         for fname in "dlldata.c imtg_i.c imtg_p.c imtg.h".split():
             os.remove(os.path.join(this_dir, fname))

    print "Registering %s" % (tlb,)
    tli=pythoncom.LoadTypeLib(tlb)
    pythoncom.RegisterTypeLib(tli,tlb)
def UnregisterTypelib():
    k = CImtg
    try:
        pythoncom.UnRegisterTypeLib(k._typelib_guid_, 
                                    k._typelib_version_[0], 
                                    k._typelib_version_[1], 
                                    0, 
                                    pythoncom.SYS_WIN32)
        print "Unregistered typelib"
    except pythoncom.error, details:
        if details[0]==winerror.TYPE_E_REGISTRYACCESS:
            pass
        else:
            raise
if __name__=='__main__':
      print "checking frozen"
      if hasattr(sys, 'frozen'):
         # running as packed executable
         if '--unregister' in sys.argv or '--register' in sys.argv:
            if '--unregister' in sys.argv:  
              # Unregister the type-libraries.
              UnregisterTypelib()
              import win32com.server.register 
              win32com.server.register.UseCommandLine(CImtg)
            else:
              # Build and register the type-libraries.
              BuildTypelib()
              import win32com.server.register 
              win32com.server.register.UseCommandLine(CImtg)
         else:
            import win32com.server
            from win32com.server import localserver
            print "starting the server"
            localserver.main()

      else:
        if '--unregister' in sys.argv:
          # Unregister the type-libraries.
          UnregisterTypelib()
          import win32com.server.register 
          win32com.server.register.UseCommandLine(CImtg)
        else:
          if '--register' in sys.argv:
            # Build and register the type-libraries.
            BuildTypelib()
            import win32com.server.register 
            win32com.server.register.UseCommandLine(CImtg)

Next the setup for py2exe

I had to add the funky import of the modulefinder because win32com.shell wasn't included in the packaged executable

setup_imtg.py

# This setup script builds a single-file Python inprocess COM server.
#
import modulefinder
import win32com, sys
for p in win32com.__path__[1:]:
    modulefinder.AddPackagePath("win32com",p)
for extra in ["win32com.shell"]:
    __import__(extra)
    m = sys.modules[extra]
    for p in m.__path__[1:]:
        modulefinder.AddPackagePath(extra, p)

from distutils.core import setup
import py2exe
import sys
# If run without args, build executables, in quiet mode.
if len(sys.argv) == 1:
    sys.argv.append("py2exe")
    sys.argv.append("-q")

class Target:
    def __init__(self, **kw):
        self.__dict__.update(kw)
        # for the versioninfo resources
        self.name = "IMTG Server"


################################################################
# pywin32 COM pulls in a lot of stuff which we don't want or need.

CImtg = Target(
    description = "Sample COM server",
    # what to build.  For COM servers, the module name (not the
    # filename) must be specified!
    modules = ["imtg_server"],
    # we only want the inproc server.
    )

excludes = ["pywin", "pywin.debugger", "pywin.debugger.dbgcon",
            "pywin.dialogs", "pywin.dialogs.list"]

options = {
    "bundle_files": 1, # create singlefile exe
    "compressed": 1, # compress the library archive
    "excludes": excludes,
    "dll_excludes": ["w9xpopen.exe"] # we don't need this
    }

setup(
    options = {"py2exe": options},
    zipfile = None, # append zip-archive to the executable.
    com_server = [CImtg]
    )

When you run the generated EXE you can register with

imtg_server --register

but you won't see abou output

--unregister unregisters

You can use this vbs file to test it.

t.vbs

dim MD
set MD = CreateObject("ImtGroup.Test")
dim response
response = MD.EchoString("Really")
MsgBox(response)

When you run there will be a .log created that looks like this:

pythoncom error: ERROR: server.policy could not create an instance.

Traceback (most recent call last):
  File "win32com\server\policy.pyc", line 136, in CreateInstance
  File "win32com\server\policy.pyc", line 194, in _CreateInstance_
  File "win32com\server\policy.pyc", line 727, in call_func
  File "win32com\server\policy.pyc", line 717, in resolve_func
AttributeError: 'module' object has no attribute 'CImtg'
pythoncom error: Unexpected gateway error

Traceback (most recent call last):
  File "win32com\server\policy.pyc", line 136, in CreateInstance
  File "win32com\server\policy.pyc", line 194, in _CreateInstance_
  File "win32com\server\policy.pyc", line 727, in call_func
  File "win32com\server\policy.pyc", line 717, in resolve_func
AttributeError: 'module' object has no attribute 'CImtg'
pythoncom error: CPyFactory::CreateInstance failed to create instance. (80004005)

So what I need is to get around this error. This class is certainly in my object. I'm concerned that the value I have specified in my server as:

_reg_class_spec_ = "__main__.CImtg"

Is incorrect. The main may refer to the wrapped exe and not my own server which has no main specified. I did try to create main as well with no better results. I just don't know how py2exe proxies the classes. I tried using my file name imtg_server.CImtg and that fails with module not found. I tried just CImtg and that fails. I tried using variations of win32com and pythoncom - but it just doesn't do it. What I have seems "right" so maybe I need an additional reg tag or something? Any help is greatly appreciated. Thank you.

like image 790
user2709214 Avatar asked Nov 09 '22 19:11

user2709214


1 Answers

I finally figured this out. I'm hoping it will help someone else struggling with this.

This is a problem with the import. The way py2exe packages this it just can't find the right pieces when running as the com server. What I'm posting below works.

So the first piece is that the setup needs to look like this. NOTICE the addition of "packages" in the options, where we include my com server. This is important because we are going to change the __reg_class_spec__ to point to this explicitly.

So the full revised setup_imtg.py

# This setup script builds a single-file Python inprocess COM server.
#
import modulefinder
import win32com, sys
for p in win32com.__path__[1:]:
    modulefinder.AddPackagePath("win32com",p)
for extra in ["win32com.shell"]:
    __import__(extra)
    m = sys.modules[extra]
    for p in m.__path__[1:]:
        modulefinder.AddPackagePath(extra, p)

from distutils.core import setup
import py2exe
import sys
# If run without args, build executables, in quiet mode.
if len(sys.argv) == 1:
    sys.argv.append("py2exe")
    sys.argv.append("-q")

class Target:
    def __init__(self, **kw):
        self.__dict__.update(kw)
        # for the versioninfo resources
        self.name = "Python TestServer"


################################################################
# pywin32 COM pulls in a lot of stuff which we don't want or need.

CImtg = Target(
    description = "Sample COM Server",
    # what to build.  For COM servers, the module name (not the
    # filename) must be specified!
    modules = ["imtg_server"],
    # we only want the inproc server.
    )

excludes = ["pywin", "pywin.debugger", "pywin.debugger.dbgcon",
            "pywin.dialogs", "pywin.dialogs.list"]

options = {
    "bundle_files": 1, # create singlefile exe
    "packages": ["imtg_server"],
    "compressed": 1, # compress the library archive
    "excludes": excludes,
    "dll_excludes": ["w9xpopen.exe"] # we don't need this
    }

setup(
    options = {"py2exe": options},
    zipfile = None, # append zip-archive to the executable.
    com_server = [CImtg]
    )

Now then I changed the server code to specify the _reg_class_spec_ to point to imtg_server.CImtg.

But that's not all! We were doing this from the code to start the com server. We don't need to do that! The initial sample I started with used py2exe and built a CONSOLE application and not a com_server, but py2exe DOES build a com server so we do not need to start it again - so this code gets removed:

    import win32com.server
    from win32com.server import localserver
    print "starting the server"
    localserver.main()

But as they say - that's not ALL! Since we included the package itself and thats what we are executing the "name" isn't __main__ instead it's imtg_server!

So there's a check for that in here in addition to main in case you are developing but not deploying. MOST of the code now is to determine which way you are running and to check and run appropriately because there's ONE MORE THING! Getting the directory where we are running for building the typelib from the IDL or for registering the typelib appends the name of the exe to the path - where when running the interpreter does not! So I had to strip that off.

Remember when you run the exe the print statements are suppressed so don't worry when you register and nothing is displayed. If there's a problem a .log file will be created.

Here's the working code for imtg_server.py -- enjoy

import sys, os
import pythoncom
import win32com
import winerror
# importers check was old py2exe current uses frozen
if hasattr(sys, 'frozen'):
    # we are running as py2exe-packed executable
    print "is an exe"
    pythoncom.frozen = 1
else:
   print "not an exe"

class CImtg:
    _reg_clsctx_ = pythoncom.CLSCTX_LOCAL_SERVER
    #
    # COM declarations    
    #
    _reg_clsid_ = "{24c0e3fe-58e7-4485-87dc-9f9e823b85e1}"
    _reg_desc_ = "IMTGroup Python test object"
    _reg_progid_ = "ImtGroup.Test"
    if hasattr(sys, 'frozen'):
        # In the py2exe-packed version, specify the module.class
        # to use. In the python script version, python is able
        # to figure it out itself.
        _reg_class_spec_ = "imtg_server.CImtg"
        print "set reg_class_spec"
        print _reg_class_spec_
    ###
    ### Link to typelib - uuid matches uuid for type library in idl file
    _typelib_guid_ = '{d665e9d0-71a9-4e23-a1b4-abe3376d5c58}'
    _typelib_version_ = 1, 0
    _com_interfaces_ = ['IImtGroupApp']

    def __init__(self):
    ### initialize something here if necessary
    ### The item below is not used in this example
        self.MyProp1 = 10

    def EchoString(self,in1):
        return "Echoing " + in1

    def AddNumbers(self, in1, in2):
        return in1 + in2

def BuildTypelib():
    from distutils.dep_util import newer
    this_dir = os.path.dirname(__file__)
    # when running as a exe this directory includes the exe name
    if this_dir.endswith('imtg_server.exe'):
       this_dir = this_dir[:-15]
    idl = os.path.abspath(os.path.join(this_dir, "imtg.idl"))
    tlb=os.path.splitext(idl)[0] + '.tlb'
    if os.path.isfile(idl): 
      # test for idl - if no idl don't create tlb assume its there
      # Comment below for building exe as we will have type library
      if newer(idl, tlb):
         print "Compiling %s" % (idl,)
         rc = os.system ('midl "%s"' % (idl,))
         if rc:
             raise RuntimeError("Compiling MIDL failed!")
        # Can't work out how to prevent MIDL from generating the stubs.
        # just nuke them
         for fname in "dlldata.c imtg_i.c imtg_p.c imtg.h".split():
             os.remove(os.path.join(this_dir, fname))

    print "Registering %s" % (tlb,)
    tli=pythoncom.LoadTypeLib(tlb)
    pythoncom.RegisterTypeLib(tli,tlb)
def UnregisterTypelib():
    k = CImtg
    try:
        pythoncom.UnRegisterTypeLib(k._typelib_guid_, 
                                    k._typelib_version_[0], 
                                    k._typelib_version_[1], 
                                    0, 
                                    pythoncom.SYS_WIN32)
        print "Unregistered typelib"
    except pythoncom.error, details:
        if details[0]==winerror.TYPE_E_REGISTRYACCESS:
            pass
        else:
            raise
if __name__=='__main__' or __name__ =='imtg_server':
      print "checking frozen"

      if hasattr(sys, 'frozen'):
         # running as packed executable

         if '--unregister' in sys.argv or '--register' in sys.argv:
            if '--unregister' in sys.argv:  
              # Unregister the type-libraries.
              UnregisterTypelib()
              import win32com.server.register 
              win32com.server.register.UseCommandLine(CImtg)
            else:
              # Build and register the type-libraries.
              BuildTypelib()
              import win32com.server.register 
              win32com.server.register.UseCommandLine(CImtg)

      else:
        if '--unregister' in sys.argv:
          # Unregister the type-libraries.
          UnregisterTypelib()
          import win32com.server.register 
          win32com.server.register.UseCommandLine(CImtg)
        else:
          if '--register' in sys.argv:
            # Build and register the type-libraries.
            BuildTypelib()
            import win32com.server.register 
            win32com.server.register.UseCommandLine(CImtg)
like image 185
user2709214 Avatar answered Nov 14 '22 21:11

user2709214