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

标签 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 错误创建的临时文件的目录。

程序.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
  }
}

最佳答案

如果没有崩溃,我无法让它工作,并且根据 this discussion ,其他人也遇到过同样的问题。

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

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

结果:没有崩溃! :)

关于c# - 在 C# 中执行邮件合并时 LibreOffice 崩溃,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42170214/

相关文章:

javascript - 是否可以使用 HTML、CSS 和 Javascript 构建 LibreOffice 侧边栏插件

c# - 在 WinForms 应用程序中编译 ASPX

c# - Moq 中的 SetupSequence

c# - Silverlight 以编程方式选择文本

excel - 如何将两列与数据合并,如果一列的单元格为空,则相邻单元格已满?

Libreoffice : Referencing cells in another worksheet via FIND(), LOOKUP(),还是 EXACT()?

c# - 设计多个表使用相同模型的 SQL 数据库

javascript - Dynamics CRM web api FetchXml 返回命名为 '2_x002e_' 的链接实体,两者之间类似

vba - 使用唯一名称保存生成的 Word 文件 (mailmerge)

c# Interop.Word 在不保存到磁盘的情况下连接两个 WordDocument