5ce2c8a982
为 Claude Code 提供原生 Windows toast 通知:点击跳回原窗口、切回 Windows Terminal 标签、跨虚拟桌面、调用方图标、非阻塞投递;NativeAOT 单文件分发。
168 lines
5.0 KiB
C#
168 lines
5.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace Notify.Interop;
|
|
|
|
/// <summary>
|
|
/// 沿父进程上溯,跳过 shell/运行时,找到真正的调用方 App(编辑器/终端)
|
|
/// </summary>
|
|
internal static partial class ProcessTree
|
|
{
|
|
// 这些进程是 shell / 运行时 / 包装器,不是用户面对的 App,跳过继续上溯
|
|
private static readonly HashSet<string> 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<uint, (uint Parent, string Name)> BuildParentMap()
|
|
{
|
|
var map = new Dictionary<uint, (uint, string)>();
|
|
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<PROCESSENTRY32W>();
|
|
|
|
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];
|
|
}
|