using System; using System.Collections.Generic; using System.Runtime.InteropServices; namespace Notify.Interop; /// /// 沿父进程上溯,跳过 shell/运行时,找到真正的调用方 App(编辑器/终端) /// internal static partial class ProcessTree { // 这些进程是 shell / 运行时 / 包装器,不是用户面对的 App,跳过继续上溯 private static readonly HashSet SkipNames = new(StringComparer.OrdinalIgnoreCase) { "cmd", "powershell", "pwsh", "bash", "sh", "zsh", "fish", "wsl", "wslhost", "conhost", "openconsole", "node", "deno", "bun", "python", "python3", "py", "uv", "uvx", "npm", "npx", "yarn", "pnpm", "claude", "dotnet", "git", "env", "busybox", "winpty", "sudo", "notify", }; public static string FindCallerExePath() { try { var parents = BuildParentMap(); var pid = GetCurrentProcessId(); for (var i = 0; i < 16; i++) { if (!parents.TryGetValue(pid, out var info)) { break; } pid = info.Parent; if (pid == 0 || !parents.TryGetValue(pid, out var anc)) { break; } var name = anc.Name; if (name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) { name = name[..^4]; } if (SkipNames.Contains(name)) { continue; } // 第一个非 shell/运行时的祖先即调用方 App return GetFullPath(pid); } } catch { // 取不到就回退默认图标 } return ""; } private static Dictionary BuildParentMap() { var map = new Dictionary(); var snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot == IntPtr.Zero || snapshot == new IntPtr(-1)) { return map; } try { var entry = default(PROCESSENTRY32W); entry.dwSize = (uint)Marshal.SizeOf(); if (Process32FirstW(snapshot, ref entry)) { do { map[entry.th32ProcessID] = (entry.th32ParentProcessID, ReadExeName(ref entry)); } while (Process32NextW(snapshot, ref entry)); } } finally { CloseHandle(snapshot); } return map; } private static unsafe string ReadExeName(ref PROCESSENTRY32W entry) { fixed (char* p = entry.szExeFile) { return new string(p); } } private static string GetFullPath(uint pid) { var h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid); if (h == IntPtr.Zero) { return ""; } try { var buf = new char[1024]; var size = (uint)buf.Length; return QueryFullProcessImageName(h, 0, ref buf[0], ref size) ? new string(buf, 0, (int)size) : ""; } finally { CloseHandle(h); } } private const uint TH32CS_SNAPPROCESS = 0x00000002; private const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000; [LibraryImport("kernel32.dll")] private static partial uint GetCurrentProcessId(); [LibraryImport("kernel32.dll")] private static partial IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID); [LibraryImport("kernel32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool Process32FirstW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe); [LibraryImport("kernel32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool Process32NextW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe); [LibraryImport("kernel32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool CloseHandle(IntPtr hObject); [LibraryImport("kernel32.dll")] private static partial IntPtr OpenProcess(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwProcessId); [LibraryImport("kernel32.dll", EntryPoint = "QueryFullProcessImageNameW", StringMarshalling = StringMarshalling.Utf16)] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool QueryFullProcessImageName(IntPtr hProcess, uint dwFlags, ref char lpExeName, ref uint lpdwSize); } [StructLayout(LayoutKind.Sequential)] internal unsafe struct PROCESSENTRY32W { public uint dwSize; public uint cntUsage; public uint th32ProcessID; public nint th32DefaultHeapID; public uint th32ModuleID; public uint cntThreads; public uint th32ParentProcessID; public int pcPriClassBase; public uint dwFlags; public fixed char szExeFile[260]; }