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

.NET Core 6跨平台实现程序退出时不关闭已启动默认文本编辑器

.NET Core 6 未处理异常日志打开跨平台实现方案

问题根因

  • 原实现使用UseShellExecute = true通过系统Shell打开日志文件时,Process.Start()返回的是Shell宿主进程实例,而非实际运行的文本编辑器进程:Windows下该实例对应explorer.exe的文件关联启动逻辑、Linux下对应xdg-open进程、macOS下对应open进程,这类宿主进程启动完关联程序后会立刻退出,直接调用WaitForExit()无法等待到文本编辑器关闭。
  • JetBrains Rider调试运行时会将主进程和其启动的所有子进程归入同一个进程组/作业对象,主程序提前退出时系统会自动清理同组所有进程,这就是调试环境下编辑器会被强制关闭、直接运行编译产物无此问题的核心原因。
  • 固定时长Thread.Sleep()无可靠性保证:不同设备编辑器启动速度差异大,用户阅读日志的时长也不可控,休眠时长过短会导致编辑器还未加载完成就被关闭,过长则会产生无意义的等待。

可靠实现代码

核心逻辑是分平台直接获取文本文件默认关联的编辑器可执行路径,直接启动编辑器进程并传入日志路径参数,拿到真实编辑器进程句柄后调用WaitForExit()阻塞直到用户关闭编辑器,再执行后续程序退出逻辑,全程不需要固定时长休眠。

if (logPath != null)
{
    // 写入异常日志
    var log = logPath();
    File.WriteAllText(log, ex.ToString());

    ProcessStartInfo editorStartInfo;
    if (OperatingSystem.IsWindows())
    {
        // 从注册表查询.txt格式默认打开程序
        using var extKey = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(".txt");
        var fileType = extKey?.GetValue(string.Empty)?.ToString() ?? "txtfile";
        using var cmdKey = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey($@"{fileType}\shell\open\command");
        var launchCmd = cmdKey?.GetValue(string.Empty)?.ToString() ?? @"%SystemRoot%\system32\NOTEPAD.EXE %1";
        
        // 解析带引号的可执行路径
        var exePath = launchCmd.StartsWith('"') 
            ? launchCmd[1..launchCmd.IndexOf('"', 1)] 
            : launchCmd[..launchCmd.IndexOf(' ')];
        var args = launchCmd.Replace("%1", log).Replace(exePath, string.Empty).Trim('"', ' ');
        
        editorStartInfo = new ProcessStartInfo(exePath, args)
        {
            UseShellExecute = false,
            WorkingDirectory = Path.GetDirectoryName(exePath)
        };
    }
    else if (OperatingSystem.IsLinux())
    {
        string exePath = "nano";
        string args = log;
        try
        {
            // 通过xdg-mime查询文本文件默认关联程序
            var mimeQueryProc = Process.Start(new ProcessStartInfo("xdg-mime", "query default text/plain")
            {
                UseShellExecute = false,
                RedirectStandardOutput = true,
                CreateNoWindow = true
            });
            var desktopFileName = mimeQueryProc.StandardOutput.ReadToEnd().Trim();
            mimeQueryProc.WaitForExit();

            if (!string.IsNullOrEmpty(desktopFileName))
            {
                // 查找desktop文件路径,优先用户目录后系统目录
                var userDesktopPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), 
                    ".local", "share", "applications", desktopFileName);
                var systemDesktopPath = Path.Combine("/usr/share/applications", desktopFileName);
                var desktopFilePath = File.Exists(userDesktopPath) ? userDesktopPath : systemDesktopPath;

                if (File.Exists(desktopFilePath))
                {
                    foreach (var line in File.ReadAllLines(desktopFilePath))
                    {
                        if (!line.StartsWith("Exec=")) continue;
                        var execLine = line[5..]
                            .Replace("%f", log)
                            .Replace("%F", log)
                            .Replace("%u", log)
                            .Replace("%U", log);
                        exePath = execLine.Split(' ')[0];
                        args = execLine[exePath.Length..].Trim();
                        break;
                    }
                }
            }

            // fallback:依次探测常用GUI编辑器
            var fallbackEditors = new[] { "gedit", "kate", "mousepad", "leafpad", "pluma" };
            foreach (var editor in fallbackEditors)
            {
                var checkProc = Process.Start(new ProcessStartInfo("which", editor)
                {
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    CreateNoWindow = true
                });
                checkProc.WaitForExit();
                if (checkProc.ExitCode != 0) continue;
                exePath = editor;
                args = log;
                break;
            }
        }
        catch
        {
            // 查询失败时保持默认fallback到nano
        }

        editorStartInfo = new ProcessStartInfo(exePath, args)
        {
            UseShellExecute = false
        };
    }
    else // macOS
    {
        // open -t 用默认文本编辑器打开文件,-W参数阻塞直到编辑器退出
        editorStartInfo = new ProcessStartInfo("open", $"-W -t \"{log}\"")
        {
            UseShellExecute = false
        };
    }

    // 启动编辑器并等待退出
    using var editorProcess = Process.Start(editorStartInfo);
    editorProcess?.WaitForExit();
}

兼容性说明

  • 全平台做了降级适配:Windows下注册表查询失败默认使用系统记事本,Linux下默认关联查询失败会依次探测常用GUI编辑器,最终回落到nano终端编辑器;macOS直接使用系统自带open命令,无额外依赖。
  • 阻塞等待逻辑对所有编辑器生效,用户关闭编辑器窗口后主程序才会继续执行退出流程,完全避免固定时长休眠的不可靠问题。
  • 调试环境下由于主程序会一直阻塞到编辑器关闭后才执行退出,不会出现父进程提前终止连带关闭编辑器的问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 18:54:25