.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
相关产品推荐
相关产品推荐

