在C#中执行邮件合并时,LibreOffice崩溃

时间:2017-02-10 23:15:36

标签: c# libreoffice mailmerge

我正在使用一个使用LibreOffice执行邮件合并的C#应用​​程序 我可以执行邮件合并并将结果保存为pdf,但在调用xDesktop.terminate()后发生崩溃,并且下次打开LibreOffice时会出现崩溃报告。

每次使用com.sun.star.text.MailMerge服务并关闭LibreOffice时,用作邮件合并基础的模型都不会从临时文件夹中删除。
例如文件:
%TEMP%\lu97964g78o.tmp\lu97964g78v.tmp
%TEMP%\lu97964g78o.tmp\SwMM0.odt

似乎我没有正确关闭MailMerge服务。


重现Writer崩溃的最小代码:

// Program.cs

using System;
using System.IO;

namespace LibreOffice_MailMerge
{
  class Program
  {
    static void Main(string[] args)
    {
      // LibreOffice crash after calling xDesktop.terminate().
      // The crash reporting appear when the second itaration begins.

      int i;
      for (i = 0; i < 2; i++)
      {
        //Minimal code to reproduce the crash.
        using (var document = new TextDocument())
        {
          document.MailMerge();
        }
      }
    }
  }
}


// TextDocument.cs

using Microsoft.Win32;
using System;
using unoidl.com.sun.star.frame;
using unoidl.com.sun.star.lang;
using unoidl.com.sun.star.uno;

namespace LibreOffice_MailMerge
{
  class TextDocument : IDisposable
  {
    private XComponentContext localContext;
    private XMultiComponentFactory serviceManager;
    private XDesktop xDesktop;

    public TextDocument()
    {
      InitializeEnvironment();  // Add LibreOffice in PATH environment variable.

      localContext = uno.util.Bootstrap.bootstrap();
      serviceManager = localContext.getServiceManager();
      xDesktop = (XDesktop)serviceManager.createInstanceWithArgumentsAndContext("com.sun.star.frame.Desktop", new uno.Any[] { }, localContext);
    }

    public void MailMerge()
    {
      // #############################################
      // # No crash if these two lines are commented #
      // #############################################
      var oMailMerge = serviceManager.createInstanceWithArgumentsAndContext("com.sun.star.text.MailMerge", new uno.Any[] { }, localContext);
      ((XComponent)oMailMerge).dispose();
    }

    public void Dispose()
    {
      if (xDesktop != null)
      {
        xDesktop.terminate();
      }
    }
  }
}


操作系统:Windows 10 64位和Windows 7 32位
LibreOffice和SDK版本:5.3.0.3 x86(也测试了5.2.4.2和5.2.5.1 x86)
LibreOffice快速入门:禁用
Crashreport

Complete Visual Studio project在GitHub上。

非常感谢任何能告诉我错误的人。

编辑:更新代码并提交错误报告。

编辑2 :希望做一些有用的事情,我发布了上述问题的解决方法。

基本上,我通过将创建新用户配置文件的目录作为参数传递来启动LibreOffice流程。
我还将tmp环境变量的路径更改为仅LibreOffice进程指向上一个目录。

当我完成工作时,我会删除此目录,其中包含由LibreOffice API错误创建的崩溃报告和临时文件。

的Program.cs:

using System;
using System.IO;

namespace LibreOffice_MailMerge
{
  class Program
  {
    static void Main(string[] args)
    {
      // Example of mail merge.
      using (var document = new WriterDocument())
      {
        var modelPath = Path.Combine(Environment.CurrentDirectory, "Files", "Test.odt");
        var csvPath = Path.Combine(Environment.CurrentDirectory, "Files", "Test.csv");
        var outputPath = Path.Combine(Path.GetTempPath(), "MailMerge.pdf");

        document.MailMerge(modelPath, csvPath);
        document.ExportToPdf(outputPath);
      }
    }
  }
}

LibreOffice.cs:

using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.IO;
using unoidl.com.sun.star.beans;
using unoidl.com.sun.star.bridge;
using unoidl.com.sun.star.frame;
using unoidl.com.sun.star.lang;
using unoidl.com.sun.star.uno;

namespace LibreOffice_MailMerge
{
  class LibreOffice : IDisposable
  {
    // LibreOffice process.
    private Process process;

    // LibreOffice user profile directory.
    public string UserProfilePath { get; private set; }

    public XComponentContext Context { get; private set; }
    public XMultiComponentFactory ServiceManager { get; private set; }
    public XDesktop2 Desktop { get; private set; }

    public LibreOffice()
    {
      const string name = "MyProjectName";

      UserProfilePath = Path.Combine(Path.GetTempPath(), name);
      CleanUserProfile();

      InitializeEnvironment();

      var arguments = $"-env:UserInstallation={new Uri(UserProfilePath)} --accept=pipe,name={name};urp --headless --nodefault --nofirststartwizard --nologo --nolockcheck";

      process = new Process();
      process.StartInfo.UseShellExecute = false;
      process.StartInfo.FileName = "soffice";
      process.StartInfo.Arguments = arguments;
      process.StartInfo.CreateNoWindow = true;

      process.StartInfo.EnvironmentVariables["tmp"] = UserProfilePath;

      process.Start();
      var xLocalContext = uno.util.Bootstrap.defaultBootstrap_InitialComponentContext();
      var xLocalServiceManager = xLocalContext.getServiceManager();
      var xUnoUrlResolver = (XUnoUrlResolver)xLocalServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", xLocalContext);

      for (int i = 0; i <= 10; i++)
      {
        try
        {
          ServiceManager = (XMultiComponentFactory)xUnoUrlResolver.resolve($"uno:pipe,name={name};urp;StarOffice.ServiceManager");
          break;
        }
        catch (unoidl.com.sun.star.connection.NoConnectException)
        {
          System.Threading.Thread.Sleep(1000);
          if (Equals(i, 10))
          {
            throw;
          }
        }
      }

      Context = (XComponentContext)((XPropertySet)ServiceManager).getPropertyValue("DefaultContext").Value;
      Desktop = (XDesktop2)ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", Context);
    }

    /// <summary>
    /// Set up the environment variables for the process.
    /// </summary>
    private void InitializeEnvironment()
    {
      var nodes = new RegistryHive[] { RegistryHive.CurrentUser, RegistryHive.LocalMachine };

      foreach (var node in nodes)
      {
        var key = RegistryKey.OpenBaseKey(node, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\LibreOffice\UNO\InstallPath");

        if (key != null && key.ValueCount > 0)
        {
          var unoPath = key.GetValue(key.GetValueNames()[key.ValueCount - 1]).ToString();

          Environment.SetEnvironmentVariable("PATH", $"{unoPath};{Environment.GetEnvironmentVariable("PATH")}", EnvironmentVariableTarget.Process);
          Environment.SetEnvironmentVariable("URE_BOOTSTRAP", new Uri(Path.Combine(unoPath, "fundamental.ini")).ToString(), EnvironmentVariableTarget.Process);
          return;
        }
      }

      throw new System.Exception("LibreOffice not found.");
    }

    /// <summary>
    /// Delete LibreOffice user profile directory.
    /// </summary>
    private void CleanUserProfile()
    {
      if (Directory.Exists(UserProfilePath))
      {
        Directory.Delete(UserProfilePath, true);
      }
    }

    #region IDisposable Support

    private bool disposed = false;

    protected virtual void Dispose(bool disposing)
    {
      if (!disposed)
      {
        if (disposing)
        {

        }

        if (Desktop != null)
        {
          Desktop.terminate();
          Desktop = null;
          ServiceManager = null;
          Context = null;
        }

        if (process != null)
        {
          // Wait LibreOffice process.
          if (!process.WaitForExit(5000))
          {
            process.Kill();
          }

          process.Dispose();
        }

        CleanUserProfile();

        disposed = true;
      }
    }

    ~LibreOffice()
    {
      Dispose(false);
    }

    public void Dispose()
    {
      Dispose(true);
      GC.Collect();
      GC.SuppressFinalize(this);
    }

    #endregion
  }
}

WriterDocument.cs:

using System;
using System.IO;
using unoidl.com.sun.star.beans;
using unoidl.com.sun.star.frame;
using unoidl.com.sun.star.lang;
using unoidl.com.sun.star.sdb;
using unoidl.com.sun.star.task;
using unoidl.com.sun.star.text;
using unoidl.com.sun.star.util;

namespace LibreOffice_MailMerge
{
  class WriterDocument : LibreOffice
  {
    private XTextDocument xTextDocument = null;
    private XDatabaseContext xDatabaseContext;

    public WriterDocument()
    {
      xDatabaseContext = (XDatabaseContext)ServiceManager.createInstanceWithContext("com.sun.star.sdb.DatabaseContext", Context);
    }

    /// <summary>
    /// Execute a mail merge.
    /// </summary>
    /// <param name="modelPath">Full path of model.</param>
    /// <param name="csvPath">>Full path of CSV file.</param>
    public void MailMerge(string modelPath, string csvPath)
    {
      const string dataSourceName = "Test";

      var dataSourcePath = Path.Combine(UserProfilePath, $"{dataSourceName}.csv");
      var databasePath = Path.Combine(UserProfilePath, $"{dataSourceName}.odb");

      File.Copy(csvPath, dataSourcePath);

      CreateDataSource(databasePath, dataSourceName, dataSourcePath);

      // Set up the mail merge properties.
      var oMailMerge = ServiceManager.createInstanceWithContext("com.sun.star.text.MailMerge", Context);

      var properties = (XPropertySet)oMailMerge;
      properties.setPropertyValue("DataSourceName", new uno.Any(typeof(string), dataSourceName));
      properties.setPropertyValue("DocumentURL", new uno.Any(typeof(string), new Uri(modelPath).AbsoluteUri));
      properties.setPropertyValue("Command", new uno.Any(typeof(string), dataSourceName));
      properties.setPropertyValue("CommandType", new uno.Any(typeof(int), CommandType.TABLE));
      properties.setPropertyValue("OutputType", new uno.Any(typeof(short), MailMergeType.SHELL));
      properties.setPropertyValue("SaveAsSingleFile", new uno.Any(typeof(bool), true));

      // Execute the mail merge.
      var job = (XJob)oMailMerge;
      xTextDocument = (XTextDocument)job.execute(new NamedValue[0]).Value;

      var model = ((XPropertySet)oMailMerge).getPropertyValue("Model").Value;
      CloseDocument(model);

      DeleteDataSource(dataSourceName);

      ((XComponent)oMailMerge).dispose();
    }

    /// <summary>
    /// Export the document as PDF.
    /// </summary>
    /// <param name="outputPath">Full path of the PDF file</param>
    public void ExportToPdf(string outputPath)
    {
      if (xTextDocument == null)
      {
        throw new System.Exception("You must first perform a mail merge.");
      }

      var xStorable = (XStorable)xTextDocument;

      var propertyValues = new PropertyValue[2];
      propertyValues[0] = new PropertyValue() { Name = "Overwrite", Value = new uno.Any(typeof(bool), true) };
      propertyValues[1] = new PropertyValue() { Name = "FilterName", Value = new uno.Any(typeof(string), "writer_pdf_Export") };

      var pdfPath = new Uri(outputPath).AbsoluteUri;
      xStorable.storeToURL(pdfPath, propertyValues);
    }

    private void CloseDocument(Object document)
    {
      if (document is XModel xModel && xModel != null)
      {
        ((XModifiable)xModel).setModified(false);

        if (xModel is XCloseable xCloseable && xCloseable != null)
        {
          try
          {
            xCloseable.close(true);
          }
          catch (CloseVetoException) { }
        }
        else
        {
          try
          {
            xModel.dispose();
          }
          catch (PropertyVetoException) { }
        }
      }
    }

    /// <summary>
    /// Register a new data source.
    /// </summary>
    /// <param name="databasePath">Full path of database.</param>
    /// <param name="datasourceName">The name by which register the database.</param>
    /// <param name="dataSourcePath">Full path of CSV file.</param>
    private void CreateDataSource(string databasePath, string dataSourceName, string dataSourcePath)
    {
      DeleteDataSource(dataSourceName);

      var oDataSource = xDatabaseContext.createInstance();
      var XPropertySet = (XPropertySet)oDataSource;

      // http://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1sdb_1_1XOfficeDatabaseDocument.html
      var xOfficeDatabaseDocument = ((XDocumentDataSource)oDataSource).DatabaseDocument;
      var xModel = (XModel)xOfficeDatabaseDocument;
      var xStorable = (XStorable)xOfficeDatabaseDocument;

      // Set up the datasource properties.
      var properties = new PropertyValue[9];
      properties[0] = new PropertyValue() { Name = "Extension", Value = new uno.Any(typeof(string), "csv") };
      properties[1] = new PropertyValue() { Name = "HeaderLine", Value = new uno.Any(typeof(bool), true) };
      properties[2] = new PropertyValue() { Name = "FieldDelimiter", Value = new uno.Any(typeof(string), ";") };
      properties[3] = new PropertyValue() { Name = "StringDelimiter", Value = new uno.Any(typeof(string), "\"") };
      properties[4] = new PropertyValue() { Name = "DecimalDelimiter", Value = new uno.Any(typeof(string), ".") };
      properties[5] = new PropertyValue() { Name = "ThousandDelimiter", Value = new uno.Any(typeof(string), "") };
      properties[6] = new PropertyValue() { Name = "EnableSQL92Check", Value = new uno.Any(typeof(bool), false) };
      properties[7] = new PropertyValue() { Name = "PreferDosLikeLineEnds", Value = new uno.Any(typeof(bool), true) };
      properties[8] = new PropertyValue() { Name = "CharSet", Value = new uno.Any(typeof(string), "UTF-8") };

      var uri = Uri.EscapeUriString($"sdbc:flat:{dataSourcePath}".Replace(Path.DirectorySeparatorChar, '/'));

      XPropertySet.setPropertyValue("URL", new uno.Any(typeof(string), uri));
      XPropertySet.setPropertyValue("Info", new uno.Any(typeof(PropertyValue[]), properties));

      // Save the database and register the datasource.
      xStorable.storeAsURL(new Uri(databasePath).AbsoluteUri, xModel.getArgs());
      xDatabaseContext.registerObject(dataSourceName, oDataSource);

      CloseDocument(xOfficeDatabaseDocument);
      ((XComponent)oDataSource).dispose();
    }

    /// <summary>
    /// Revoke datasource.
    /// </summary>
    /// <param name="datasourceName">The name of datasource.</param>
    private void DeleteDataSource(string datasourceName)
    {
      if (xDatabaseContext.hasByName(datasourceName))
      {
        var xDocumentDataSource = (XDocumentDataSource)xDatabaseContext.getByName(datasourceName).Value;

        xDatabaseContext.revokeDatabaseLocation(datasourceName);
        CloseDocument(xDocumentDataSource);
        ((XComponent)xDocumentDataSource).dispose();
      }
    }

    #region IDisposable Support

    private bool disposed = false;

    protected override void Dispose(bool disposing)
    {
      if (!disposed)
      {
        if (disposing)
        {

        }

        if (xTextDocument != null)
        {
          CloseDocument(xTextDocument);
          xTextDocument = null;
        }

        disposed = true;
        base.Dispose(disposing);
      }
    }

    #endregion
  }
}

1 个答案:

答案 0 :(得分:0)

我无法在没有崩溃的情况下开始工作,而根据this discussion,其他人也遇到了同样的问题。

但是应该可以多次关闭并重新打开文档(而不是LibreOffice应用程序本身)而不会崩溃。

首先手动或使用PowerShell等shell脚本打开LibreOffice。然后运行您的应用程序执行多个邮件合并但不要调用xDesktop.terminate()。应用程序完成后,手动关闭LibreOffice或使用shell脚本关闭它。

结果:没有崩溃! :)