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

Posted

技术标签:

【中文标题】在 C# 中执行邮件合并时 LibreOffice 崩溃【英文标题】:LibreOffice crash when perform a Mail Merge in C# 【发布时间】:2017-02-10 23:15:36 【问题描述】:

我正在开发一个使用 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
  

【问题讨论】:

代码中似乎缺少关闭文档的命令。例如xCloseable.close(true); 在这里:wiki.openoffice.org/wiki/Documentation/DevGuide/OfficeDev/…。 @JimK 谢谢,但我已经看到了那个链接,并且我已经在使用 xCloseable 来关闭由邮件合并创建的文档。我在 github 上创建了一个存储库,其中包含我使用的代码的更完整示例。邮件合并有效,但总是发生我提到的崩溃。 【参考方案1】:

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

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

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

结果:没有崩溃! :)

【讨论】:

我试着照你说的做,但在临时文件夹中仍然没有删除用作邮件合并基础的模型。

以上是关于在 C# 中执行邮件合并时 LibreOffice 崩溃的主要内容,如果未能解决你的问题,请参考以下文章

在输出前拼合邮件合并的文档

从 C# ASP.NET 中的数据集进行邮件合并

用于创建连接到现有电子表格“数据库”的 Libreoffice 数据库文件的 BASH 脚本

LibreOffice 停止工作,同时使用 C# 应用程序将 XLS 转换为 XHTML 文件

创建 libreoffice 基于文本的数据源并使用 java 设置设置

如何在 libreoffice calc 中编写 python 宏以在插入外部数据时处理合并的单元格