enthought / comtypes

A pure Python, lightweight COM client and server framework, based on the ctypes Python FFI package.
Other
290 stars 97 forks source link

Getting "NULL COM pointer access" when script calls from windows service #633

Open parsasaei opened 1 day ago

parsasaei commented 1 day ago

I'm trying to make a RPC service for calling my script which used comtypes package to create a pdf file from docx. When I call my script on the command line prompt I have no any problem, but when I make calling from windows service I get this error from the output: This is my script:

  import sys
  import json
  import subprocess
  from pathlib import Path
  import os
  # from ctypes import windll

  # SYS Import Local Packages Installed
  path = os.path.realpath(os.path.abspath(__file__))
  sys.path.insert(0, os.path.dirname(os.path.dirname(path)))
  sys.path.append(os.path.join(os.path.dirname(__file__),"."))
  sys.path.append(os.path.join(os.path.dirname(__file__),"packages"))
  # from packages import comtypes
  from packages.comtypes import client
  # COINIT_MULTITHREADED = 0x0

  try:
      # 3.8+
      from importlib.metadata import version
  except ImportError:
      from importlib_metadata import version

  def windows(paths, keep_active):
      # word = win32com.client.Dispatch("Word.Application")
      # windll.ole32.CoInitializeEx(None, COINIT_MULTITHREADED) 
      word = client.CreateObject("Word.Application")
      wdFormatPDF = 17

      if paths["batch"]:
          docx_files = sorted(Path(paths["input"]).glob("*.docx") or Path(paths["input"]).glob("*.doc"))
          for i, docx_filepath in enumerate(docx_files):
              pdf_filepath = Path(paths["output"]) / (str(docx_filepath.stem) + ".pdf")
              print(f"Converting {docx_filepath} to {pdf_filepath} ({i+1}/{len(docx_files)})")
              doc = word.Documents.Open(str(docx_filepath))
              doc.SaveAs(str(pdf_filepath), FileFormat=wdFormatPDF)
              doc.Close(0)
      else:
          docx_filepath = Path(paths["input"]).resolve()
          pdf_filepath = Path(paths["output"]).resolve()
          print(f"Converting {docx_filepath} to {pdf_filepath}")
          doc = word.Documents.Open(str(docx_filepath))
          doc.SaveAs(str(pdf_filepath), FileFormat=wdFormatPDF)
          doc.Close(0)

      if not keep_active:
          word.Quit()
          # comtypes.CoUninitialize()

  def resolve_paths(input_path, output_path):
      input_path = Path(input_path).resolve()
      output_path = Path(output_path).resolve() if output_path else None
      output = {} 
      if input_path.is_dir():
          output["batch"] = True
          output["input"] = str(input_path)
          if output_path:
              assert output_path.is_dir()
          else:
              output_path = str(input_path)
          output["output"] = output_path
      else:
          output["batch"] = False
          assert str(input_path).endswith(".docx") or str(input_path).endswith(".doc")
          output["input"] = str(input_path)
          if output_path and output_path.is_dir():
              output_path = str(output_path / (str(input_path.stem) + ".pdf"))
          elif output_path:
              assert str(output_path).endswith(".pdf")
          else:
              output_path = str(input_path.parent / (str(input_path.stem) + ".pdf"))
          output["output"] = output_path
      return output

  def convert(input_path, output_path=None, keep_active=False):
      paths = resolve_paths(input_path, output_path)
      windows(paths, keep_active)

  def cli():
      import textwrap
      import argparse

      description = textwrap.dedent(
          """
      Example Usage:

      Convert single docx file in-place from myfile.docx to myfile.pdf:
          docx2pdf myfile.docx

      Batch convert docx folder in-place. Output PDFs will go in the same folder:
          docx2pdf myfolder/

      Convert single docx file with explicit output filepath:
          docx2pdf input.docx output.docx

      Convert single docx file and output to a different explicit folder:
          docx2pdf input.docx output_dir/

      Batch convert docx folder. Output PDFs will go to a different explicit folder:
          docx2pdf input_dir/ output_dir/
      """
      )

      formatter_class = lambda prog: argparse.RawDescriptionHelpFormatter(
          prog, max_help_position=32
      )
      parser = argparse.ArgumentParser(
          description=description, formatter_class=formatter_class
      )
      parser.add_argument(
          "input",
          help="input file or folder. batch converts entire folder or convert single file",
      )
      parser.add_argument("output", nargs="?", help="output file or folder")
      parser.add_argument(
          "--keep-active",
          action="store_true",
          default=False,
          help="prevent closing word after conversion",
      )

      if len(sys.argv) == 1:
          parser.print_help()
          sys.exit(0)
      else:
          args = parser.parse_args()

      convert(args.input, args.output, args.keep_active)

  if __name__ == "__main__":
      cli()

I call my script with this: python .\\w2PDF .\\word.docx .\\word.pdf

I get this on windows service output:

Traceback (most recent call last):\r\n File \"<frozen runpy>\", line 198, in _run_module_as_main\r\n File \"<frozen runpy>\", line 88, in _run_code\r\n File \"D:\\Word2PDF-Service\\WindowsService1\\bin\\Debug\\Word2PDF\\__main__.py\", line 3, in <module>\r\n File \"D:\\Word2PDF-Service\\WindowsService1\\bin\\Debug\\Word2PDF\\Word2PDF_Python.py\", line 127, in cli\r\n File \"D:\\Word2PDF-Service\\WindowsService1\\bin\\Debug\\Word2PDF\\Word2PDF_Python.py\", line 76, in convert\r\n File \"D:\\Word2PDF-Service\\WindowsService1\\bin\\Debug\\Word2PDF\\Word2PDF_Python.py\", line 41, in windows\r\n File \"D:\\Word2PDF-Service\\WindowsService1\\bin\\Debug\\Word2PDF\\packages\\comtypes\\_meta.py\", line 14, in _wrap_coclass\r\n File \"D:\\Word2PDF-Service\\WindowsService1\\bin\\Debug\\Word2PDF\\packages\\comtypes\\_post_coinit\\unknwn.py\", line 520, in QueryInterface\r\nValueError: NULL COM pointer access

I couldn't find the problem, When I change the user log on to Network Service or Local Service above error changes to access is denied :

Traceback (most recent call last):\r\n File \"<frozen runpy>\", line 198, in _run_module_as_main\r\n File \"<frozen runpy>\", line 88, in _run_code\r\n File \"D:\\Word2PDF-Service\\WindowsService1\\bin\\Debug\\Word2PDF\\__main__.py\", line 3, in <module>\r\n File \"D:\\Word2PDF-Service\\WindowsService1\\bin\\Debug\\Word2PDF\\Word2PDF_Python.py\", line 127, in cli\r\n File \"D:\\Word2PDF-Service\\WindowsService1\\bin\\Debug\\Word2PDF\\Word2PDF_Python.py\", line 76, in convert\r\n File \"D:\\Word2PDF-Service\\WindowsService1\\bin\\Debug\\Word2PDF\\Word2PDF_Python.py\", line 26, in windows\r\n File \"D:\\Word2PDF-Service\\WindowsService1\\bin\\Debug\\Word2PDF\\packages\\comtypes\\client\\__init__.py\", line 273, in CreateObject\r\n File \"D:\\Word2PDF-Service\\WindowsService1\\bin\\Debug\\Word2PDF\\packages\\comtypes\\_post_coinit\\misc.py\", line 149, in CoCreateInstance\r\n File \"_ctypes/callproc.c\", line 1008, in GetResult\r\nPermissionError: [WinError -2147024891] Access is denied

junkmd commented 1 day ago

Hi,

First of all, it should be mentioned that MS Office is generally not suitable for server-side automation.

I am particularly concerned about whether Word is installed in that Windows environment. Additionally, I think it might be necessary to pass the machine argument to CreateObject.

parsasaei commented 1 day ago

@junkmd HI, Word is installed. About passing machine to CreateObject what change I can do on the code?

parsasaei commented 1 day ago

I passed machine parameter, but still get that error.

junkmd commented 1 day ago

I think this is more of a technical issue related to COM rather than comtypes or Python.

Do you have any references for what you’re trying to do?

parsasaei commented 1 day ago

I don't sure I got it right about reference you said, but the sample process for making remote procedure call I used is like: https://www.c-sharpcorner.com/article/getting-started-with-remote-procedure-call/ and after get the request from client on the server I calls my method to run script python with c#:

public Word2PDFResult word2pdfConvert(string arguments)
{
    var file = "Word2PDF";
    //var file = "Word2PDFS";
    var fileName = System.IO.Directory.GetFiles(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location), "*"+ file + "", System.IO.SearchOption.AllDirectories).FirstOrDefault();
    var pythonPath = "C:\\Python\\Python312\\Python";

    var output = new StringBuilder();
    var error = new StringBuilder();
    using (var process = new Process())
    {
        try 
        {
            process.StartInfo.FileName = pythonPath;
            process.StartInfo.Arguments = fileName + " " + arguments;
            //process.StartInfo.FileName = fileName;
            //process.StartInfo.Arguments = " " + arguments;
            process.StartInfo.UseShellExecute = false;
            process.StartInfo.RedirectStandardOutput = true;
            process.StartInfo.RedirectStandardError = true;
            process.StartInfo.CreateNoWindow = true;
            //process.StartInfo.Verb = "runas";
            //process.StartInfo.WorkingDirectory = Path.GetDirectoryName(fileName);

            using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false))
            using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false))
            {
                process.OutputDataReceived += (s, e) =>
                {
                    if (e.Data == null)
                    {
                        outputWaitHandle.Set();
                    }
                    else
                    {
                        output.AppendLine(e.Data);
                    }
                };
                process.ErrorDataReceived += (s, e) =>
                {
                    if (e.Data == null)
                    {
                        errorWaitHandle.Set();
                    }
                    else
                    {
                        error.AppendLine(e.Data);
                    }
                };
                process.Start();
                process.BeginOutputReadLine();
                process.BeginErrorReadLine();
                process.WaitForExit();
                process.Close();
            }
        }
        catch(Exception ex) { process.Kill(); }
    }

    return new Word2PDFResult { Error = error.ToString(), Output = output.ToString()  };
}
parsasaei commented 1 day ago

Before I make the procedure like RPC service which I mentioned, I called the process with iis directly and when user is not log on on remote the server, I got this error:

Traceback (most recent call last): File "", line 198, in _run_module_as_main File "", line 88, in _run_code File "D:\Word2PDF_Python\Word2PDFS\__main__.py", line 3, in File "D:\Word2PDF_Python\Word2PDFS\Word2PDF_Python.py", line 123, in cli File "D:\Word2PDF_Python\Word2PDFS\Word2PDF_Python.py", line 72, in convert File "D:\Word2PDF_Python\Word2PDFS\Word2PDF_Python.py", line 23, in windows File "D:\Word2PDF_Python\Word2PDFS\packages\comtypes\client\__init__.py", line 273, in CreateObject File "D:\Word2PDF_Python\Word2PDFS\packages\comtypes\_post_coinit\misc.py", line 149, in CoCreateInstance File "_ctypes/callproc.c", line 1008, in GetResult OSError: [WinError -2147467238] The server process could not be started because the configured identity is incorrect. Check the username and password

When I was connecting to the remote server, the client request was successful, which means it needed to admin user be present on remote server to can client user on web can pass the request.

junkmd commented 1 day ago

To confess, I currently have no detail knowledge of RPC.

Recently, I had the opportunity to read some old COM technical documents, but I haven't come up with a modern way to use RPC.

Since this is a topic that requires help from the community, I have added the appropriate label. Please feel free to comment on this issue.