3

I'm working on a C# application that performs a mail merge using LibreOffice.
I can perform the mail merge and save the result as pdf but a crash occurs after calling xDesktop.terminate() and the crash reporting appears the next time LibreOffice is opened.

Every time I use the com.sun.star.text.MailMerge service and close LibreOffice, the models used as the basis of the mail merge are not deleted from the temporary folder.
For example the files:
%TEMP%\lu97964g78o.tmp\lu97964g78v.tmp
%TEMP%\lu97964g78o.tmp\SwMM0.odt

It seems that I do not close properly the MailMerge service.


Minimal code to reproduce Writer crash:

// 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();
      }
    }
  }
}


OS: Windows 10 64bit and Windows 7 32bit
LibreOffice and SDK version: 5.3.0.3 x86 (also tested 5.2.4.2 and 5.2.5.1 x86)
LibreOffice quickstart: disabled
Crashreport

Complete Visual Studio project on GitHub.

Many thanks to anyone who can tell me where I'm wrong.

EDIT: Update code and submit a bug report.

EDIT 2: Hoping to do something useful, I publish a workaround for the problem described above.

Basically, I start the LibreOffice process by passing as a parameter a directory in which to create a new user profile.
I also change the path of the tmp environment variablile for only LibreOffice process to point to the previous directory.

When I finish the work, I delete this directory with crash reports and temporary files created by the LibreOffice API bug.

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
  }
}
Simone
  • 31
  • 3
  • It looks like the code is missing a command to close the document. For example `xCloseable.close(true);` as here: https://wiki.openoffice.org/wiki/Documentation/DevGuide/OfficeDev/Closing_Documents. – Jim K Feb 11 '17 at 01:21
  • @JimK Thanks but I had already seen that link and I'm already using xCloseable to close the document created by the mail merge. I created a repository on github with a more complete example of the code that I use. The mail merge works but always occurs the crash I mentioned. – Simone Feb 11 '17 at 09:17

1 Answers1

0

I can't get it to work without the crash, and according to this discussion, others have experienced the same problem.

However it should be possible to close and reopen documents (not the LibreOffice application itself) multiple times without crashing.

So first open LibreOffice either manually or with a shell script such as PowerShell. Then run your application. Perform multiple mail merges but do not call xDesktop.terminate(). After the application finishes, manually close LibreOffice or close it with the shell script.

The result: No crashes! :)

Jim K
  • 12,824
  • 2
  • 22
  • 51
  • I tried to do as you said but in the temporary folder still not be deleted the models used as the base of the mail merge. – Simone Feb 12 '17 at 18:25