You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在ASP.NET Core Web API中用LibreOffice便携版转Excel为PDF遇退出码-1错误

LibreOffice便携版在IIS Express下转换PDF报错(退出码-1)解决指南

问题背景

曾使用LibreOffice便携版将Office文档转换为PDF,目前无法复现该操作,在IIS Express开发环境中运行代码时,持续触发ERROR: LibreOffice returned a non-zero exit code错误,退出码为-1。LibreOffice的soffice.exe放置在C:\inetpub\logs\LibreOfficePortable目录下,不确定是否存在权限配置问题。

相关代码

using System;
using System.Diagnostics;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;

public static class MemoryStreamExtensions
{
    // Fixed LibreOffice path for IIS
    private static readonly string LibreOfficeExePath = @"C:\inetpub\logs\LibreOfficePortable\soffice.exe";

    /// <summary>
    /// Converts an Office file (Excel, Word, etc.) in a MemoryStream to PDF using LibreOffice headless.
    /// </summary>
    /// <param name="inputStream">MemoryStream of the source file</param>
    /// <param name="originalFileName">Original file name with extension (e.g. "file.xlsx")</param>
    /// <returns>MemoryStream of generated PDF</returns>
    public static async Task<MemoryStream> ConvertToPdfAsync(
        this MemoryStream inputStream,
        string originalFileName)
    {
        if (inputStream == null)
            throw new ArgumentNullException(nameof(inputStream));

        if (string.IsNullOrWhiteSpace(originalFileName))
            throw new ArgumentException("File name must be provided", nameof(originalFileName));

        // Debug log buffer
        var log = new StringBuilder();
        log.AppendLine("=== LibreOffice PDF Conversion Debug ===");
        log.AppendLine($"Timestamp: {DateTime.UtcNow}");
        log.AppendLine($"Original Filename: {originalFileName}");
        log.AppendLine($"LibreOffice Path: {LibreOfficeExePath}");

        // Validate permissions BEFORE anything else
        try
        {
            log.AppendLine("Running ValidateLibreOfficePermissions...");
            ValidateLibreOfficePermissions(LibreOfficeExePath);
            log.AppendLine("Permission validation PASSED.");
        }
        catch (Exception ex)
        {
            log.AppendLine("Permission validation FAILED.");
            log.AppendLine(ex.ToString());
            throw new Exception(log.ToString());
        }

        // Create temp directories
        string timestamp = DateTime.UtcNow.Ticks.ToString();
        string baseTemp = Path.Combine(Path.GetTempPath(), $"LO_Convert_{timestamp}");

        string inputDir = Path.Combine(baseTemp, "input");
        string outputDir = Path.Combine(baseTemp, "output");
        string userProfile = Path.Combine(baseTemp, "profile");

        Directory.CreateDirectory(inputDir);
        Directory.CreateDirectory(outputDir);
        Directory.CreateDirectory(userProfile);

        log.AppendLine($"Temp root: {baseTemp}");

        string tempInputFile = Path.Combine(inputDir, originalFileName);
        string tempOutputFile = Path.Combine(
            outputDir,
            $"{Path.GetFileNameWithoutExtension(originalFileName)}.pdf"
        );

        // Write input file
        log.AppendLine($"Saving input file to: {tempInputFile}");
        using (var fs = new FileStream(tempInputFile, FileMode.Create, FileAccess.Write))
        {
            inputStream.Position = 0;
            await inputStream.CopyToAsync(fs);
        }

        // Build LO arguments
        string args =
            $"-env:UserInstallation=file:///{userProfile.Replace("\\", "/")}" +
            $" --headless --convert-to pdf --outdir \"{outputDir}\" \"{tempInputFile}\"";

        log.AppendLine("LO Arguments:");
        log.AppendLine(args);

        var startInfo = new ProcessStartInfo
        {
            FileName = LibreOfficeExePath,
            Arguments = args,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };

        string stdout = "";
        string stderr = "";
        int exitCode = -999;

        try
        {
            log.AppendLine("Starting LibreOffice process...");

            using var process = new Process { StartInfo = startInfo };

            try
            {
                if (!process.Start())
                    throw new Exception("Process.Start() returned false — process not launched.");
            }
            catch (Exception ex)
            {
                log.AppendLine("FAILED to start LibreOffice process.");
                log.AppendLine(ex.ToString());
                throw new Exception(log.ToString());
            }

            log.AppendLine("Process started OK.");

            var outTask = process.StandardOutput.ReadToEndAsync();
            var errTask = process.StandardError.ReadToEndAsync();

            await process.WaitForExitAsync();

            stdout = await outTask;
            stderr = await errTask;
            exitCode = process.ExitCode;

            log.AppendLine("=== STDOUT ===");
            log.AppendLine(stdout);
            log.AppendLine("=== STDERR ===");
            log.AppendLine(stderr);
            log.AppendLine($"Exit Code: {exitCode}");
        }
        catch (Exception ex)
        {
            log.AppendLine("EXCEPTION during conversion:");
            log.AppendLine(ex.ToString());
            throw new Exception(log.ToString());
        }

        // Error handling
        if (exitCode != 0)
        {
            log.AppendLine("ERROR: LibreOffice returned a non-zero exit code.");
            throw new Exception(log.ToString());
        }

        if (!File.Exists(tempOutputFile))
        {
            log.AppendLine($"ERROR: Output PDF not found at: {tempOutputFile}");
            throw new Exception(log.ToString());
        }

        log.AppendLine("SUCCESS: PDF Generated.");
        log.AppendLine($"PDF Path: {tempOutputFile}");

        byte[] pdfBytes = await File.ReadAllBytesAsync(tempOutputFile);
        return new MemoryStream(pdfBytes);
    }

    /// <summary>
    /// Converts a MemoryStream to a Base64 string.
    /// </summary>
    public static string ToBase64(this MemoryStream stream)
    {
        if (stream == null) throw new ArgumentNullException(nameof(stream));
        stream.Position = 0;
        return Convert.ToBase64String(stream.ToArray());
    }

    private static void TryDeleteFile(string path)
    {
        try
        {
            if (File.Exists(path)) File.Delete(path);
        }
        catch
        {
            // ignore cleanup errors
        }
    }

    private static void ValidateLibreOfficePermissions(string exePath)
    {
        var debug = new StringBuilder();
        debug.AppendLine("=== LibreOffice Permission Check ===");
        debug.AppendLine($"Checking executable: {exePath}");

        // 1. Does the file exist?
        if (!File.Exists(exePath))
            throw new FileNotFoundException("LibreOffice executable not found.", exePath);

        debug.AppendLine("OK: File exists.");

        // 2. Can current identity read the file?
        try
        {
            using var fs = File.Open(exePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            debug.AppendLine("OK: Read access allowed.");
        }
        catch (Exception ex)
        {
            debug.AppendLine("ERROR: Cannot read the executable.");
            debug.AppendLine(ex.ToString());
            throw new Exception(debug.ToString(), ex);
        }

        // 3. Can the user execute? (Check NTFS ACL)
        try
        {
            var fileInfo = new FileInfo(exePath);
            var acl = fileInfo.GetAccessControl();   // <-- This works in .NET 6/7/8
            var rules = acl.GetAccessRules(true, true, typeof(System.Security.Principal.NTAccount));

            var identity = WindowsIdentity.GetCurrent();
            var principal = new WindowsPrincipal(identity);

            bool hasExecute = false;

            foreach (FileSystemAccessRule rule in rules)
            {
                if (principal.IsInRole(rule.IdentityReference.Value))
                {
                    if (rule.FileSystemRights.HasFlag(FileSystemRights.ExecuteFile) &&
                        rule.AccessControlType == AccessControlType.Allow)
                    {
                        hasExecute = true;
                    }
                }
            }

            if (!hasExecute)
            {
                throw new Exception(
                    $"User '{WindowsIdentity.GetCurrent().Name}' does NOT have Execute rights on: {exePath}"
                );
            }
        }
        catch (Exception ex)
        {
            throw new Exception("Failed while checking file ACL permissions for LibreOffice.", ex);
        }

        // 4. Check if the file is blocked by Windows
        string zonePath = exePath + ":Zone.Identifier";

        if (File.Exists(zonePath))
        {
            debug.AppendLine("WARNING: Executable is BLOCKED by Windows (Zone.Identifier stream).");
            debug.AppendLine($"Path: {zonePath}");
            debug.AppendLine("Right-click > Properties > Unblock is required.");
            throw new Exception(debug.ToString());
        }

        debug.AppendLine("OK: Executable is not zone-blocked.");

        // 5. Try a safe process start (test if process creation is allowed)
        try
        {
            var psi = new ProcessStartInfo
            {
                FileName = "cmd.exe",
                Arguments = "/c echo ProcessTest",
                CreateNoWindow = true,
                UseShellExecute = false,
                RedirectStandardOutput = true
            };

            using var proc = Process.Start(psi);
            proc.WaitForExit();

            debug.AppendLine("OK: Process creation is allowed.");
        }
        catch (Exception ex)
        {
            debug.AppendLine("ERROR: Cannot start ANY process under this identity.");
            debug.AppendLine(ex.ToString());
            throw new Exception(debug.ToString(), ex);
        }

        // If all checks passed:
        debug.AppendLine("ALL PERMISSIONS OK.");
    }
}

排查与解决步骤

1. 补全权限配置

IIS Express默认运行身份为当前开发用户或IIS_IUSRS组用户,需确保:

  • 给C:\inetpub\logs\LibreOfficePortable文件夹及所有子项,分配运行用户完全控制权限
  • 系统临时目录(默认C:\Users\<用户名>\AppData\Local\Temp)也需给该用户写入权限,代码会在此创建临时工作目录

2. 修正用户配置文件URI格式

退出码-1常因UserInstallation路径格式错误导致,替换代码中参数构建逻辑:

// 用Uri类自动生成正确的file://格式路径,避免手动替换错误
string userProfileUri = new Uri(userProfile).AbsoluteUri;
string args =
    $"-env:UserInstallation={userProfileUri}" +
    $" --headless --convert-to pdf --outdir \"{outputDir}\" \"{tempInputFile}\"";

3. 清理残留LibreOffice进程

Headless模式下LibreOffice可能残留后台进程,导致新进程启动失败,在启动前添加清理逻辑:

// 在启动LibreOffice前杀死所有残留的soffice进程
foreach (var process in Process.GetProcessesByName("soffice"))
{
    try
    {
        process.Kill();
        process.WaitForExit();
    }
    catch { }
}

4. 命令行直接测试

用IIS Express运行身份打开命令提示符,执行以下命令验证LibreOffice本身是否正常:

"C:\inetpub\logs\LibreOfficePortable\soffice.exe" --headless --convert-to pdf "C:\test\sample.docx" --outdir "C:\test\output"
  • 若命令行报错:说明是LibreOffice配置或系统环境问题,与代码无关
  • 若命令行成功:回到代码排查进程启动逻辑

5. 排除杀毒软件拦截

Windows Defender或第三方杀毒软件可能拦截IIS进程启动外部程序,暂时关闭实时保护测试,若恢复正常则将LibreOffice文件夹加入白名单。


内容的提问来源于stack exchange,提问作者Qiuzman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 00:49:53