// desktop-wake.exe — N1 UIA 客户端 + N2 注入/钩子编排(须 x86 以匹配 WXWork) // 用法: desktop-wake.exe [--mode auto|n1|n2] // 退出码: 0暖成功 1参数 2窗无效 3UIA异常 4仍稀疏 5注入失败 using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Windows.Automation; namespace JiangchangDesktopWake { internal static class Program { private const int WM_GETOBJECT = 0x003D; private const int WM_NULL = 0x0000; private const uint OBJID_CLIENT = 0xFFFFFFFC; private const uint OBJID_WINDOW = 0x00000000; private const int SW_RESTORE = 9; private const int SW_MAXIMIZE = 3; private const int WH_CALLWNDPROC = 4; private const uint PROCESS_CREATE_THREAD = 0x0002; private const uint PROCESS_QUERY_INFORMATION = 0x0400; private const uint PROCESS_VM_OPERATION = 0x0008; private const uint PROCESS_VM_WRITE = 0x0020; private const uint PROCESS_VM_READ = 0x0010; private const uint MEM_COMMIT = 0x1000; private const uint MEM_RESERVE = 0x2000; private const uint PAGE_READWRITE = 0x04; [DllImport("user32.dll")] private static extern bool IsWindow(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] private static extern bool IsIconic(IntPtr hWnd); [DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll")] private static extern bool PostMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll")] private static extern bool IsHungAppWindow(IntPtr hWnd); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SendMessageTimeout( IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam, uint fuFlags, uint uTimeout, out IntPtr lpdwResult); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); [DllImport("oleacc.dll")] private static extern int AccessibleObjectFromWindow( IntPtr hwnd, uint dwId, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out object ppvObject); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr hObject); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize, uint flAllocationType, uint flProtect); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, UIntPtr nSize, out UIntPtr lpNumberOfBytesWritten); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr GetModuleHandle(string lpModuleName); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] private static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, UIntPtr dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId); [DllImport("kernel32.dll", SetLastError = true)] private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode); [DllImport("user32.dll", SetLastError = true)] private static extern bool SystemParametersInfo(uint uiAction, uint uiParam, IntPtr pvParam, uint fWinIni); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize, uint dwFreeType); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetWindowsHookEx(int idHook, IntPtr lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll", SetLastError = true)] private static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr LoadLibrary(string lpFileName); private static readonly Guid IID_IAccessible = new Guid("618736E0-3C3D-11CF-810C-00AA00389B71"); private static int Main(string[] args) { if (args.Length < 2) { Console.Error.WriteLine("usage: desktop-wake.exe [--mode auto|n1|n2|n2-wake|n2-hook]"); Console.Error.WriteLine(" n1=UIA; n2=inject only; n2-wake=inject+WakeProvider(C2); n2-hook=UNSAFE"); return 1; } string profile = (args[0] ?? "").Trim().ToLowerInvariant(); string mode = "auto"; for (int i = 2; i < args.Length; i++) { if (args[i] == "--mode" && i + 1 < args.Length) { mode = (args[i + 1] ?? "auto").Trim().ToLowerInvariant(); i++; } } IntPtr hwnd; try { hwnd = ParseHwnd(args[1]); } catch { Console.Error.WriteLine("invalid hwnd: " + args[1]); return 1; } if (!IsWindow(hwnd)) { WriteJson(false, false, false, profile, hwnd, 0, 0, new string[0], "window_invalid", mode, null); return 2; } int minCount = profile == "wecom" ? 80 : 30; string[] markers = profile == "wecom" ? new[] { "con_op", "searchedit" } : new string[0]; string method = "n1_uia_client"; string detailExtra = ""; bool n2InjectOk = false; try { Activate(hwnd); // n1/auto:进程外 UIA // n2:仅注入探针 // n2-wake:注入 + 远程 WakeProvider(C2,无钩子) // n2-hook:UNSAFE bool doN2 = (mode == "n2" || mode == "n2-wake" || mode == "n2-hook") && profile == "wecom"; bool doN2Hook = (mode == "n2-hook") && profile == "wecom"; bool doWakeProvider = (mode == "n2-wake") && profile == "wecom"; // n2-wake:不在 exe 内做 UIA 深扫(易跟着目标进程一起挂);暖树由 Python probe 判定 bool doN1 = (mode == "n1" || mode == "auto" || mode == "n2-hook"); // n2-wake 不再 SPI_SETSCREENREADER:会触发 UiaRoot 查询,测试机曾崩 uiautomationcore if (!doWakeProvider) EnableScreenReaderFlag(); if (doN2) { string n2Detail; IntPtr remoteMod; n2InjectOk = TryN2(hwnd, doN2Hook, doWakeProvider, out n2Detail, out remoteMod); detailExtra = n2Detail; method = n2InjectOk ? (doWakeProvider ? "n2_wake" : "n2_inject") : "n2_inject_failed"; bool responsive = CheckUiResponsive(hwnd); detailExtra = detailExtra + ";ui_responsive=" + responsive; if (mode == "n2" || mode == "n2-wake") { // ok=注入(+provider)成功且 UI 仍响应;是否暖树由调用方 probe WriteJson( n2InjectOk && responsive, n2InjectOk, responsive, profile, hwnd, 0, 0, new string[0], (n2InjectOk ? (doWakeProvider ? "wake_provider_ok" : "inject_ok") : "inject_failed") + "|" + detailExtra, mode, method); if (!n2InjectOk) return 5; if (!responsive) return 6; return 0; } if (!n2InjectOk) { WriteJson(false, false, responsive, profile, hwnd, 0, 0, new string[0], n2Detail, mode, method); return 5; } if (!responsive) { WriteJson(false, true, false, profile, hwnd, 0, 0, new string[0], "ui_hung_after_inject|" + detailExtra, mode, method); return 6; } Thread.Sleep(400); } if (doN1) { SendGetObject(hwnd); TryMsaa(hwnd); var root = AutomationElement.FromHandle(hwnd); if (root == null) { WriteJson(false, n2InjectOk, CheckUiResponsive(hwnd), profile, hwnd, 0, 0, new string[0], "uia_from_handle_null|" + detailExtra, mode, method); return 3; } try { root.SetFocus(); } catch { } int walk1 = WalkCount(root, 64); int findAll = FindAllCount(root); Thread.Sleep(200); SendGetObject(hwnd); int walk2 = WalkCount(root, 64); int walk = Math.Max(walk1, Math.Max(walk2, findAll)); var foundMarkers = FindMarkers(root, markers); bool warm = walk >= minCount && (markers.Length == 0 || foundMarkers.Count >= markers.Length); bool responsive = CheckUiResponsive(hwnd); string detail = warm ? "warm" : "still_sparse"; if (!string.IsNullOrEmpty(detailExtra)) detail = detail + "|" + detailExtra; if (mode == "n2-wake") method = warm ? "n2_wake" : "n2_wake_still_sparse"; else if (mode == "n2-hook") method = n2InjectOk ? (warm ? "n2_hook" : "n2_hook_still_sparse") : method; WriteJson(warm, n2InjectOk, responsive, profile, hwnd, walk, findAll, foundMarkers.ToArray(), detail, mode, method); return warm ? 0 : 4; } WriteJson(false, false, false, profile, hwnd, 0, 0, new string[0], "no_mode", mode, method); return 1; } catch (Exception ex) { WriteJson(false, n2InjectOk, false, profile, hwnd, 0, 0, new string[0], "exception:" + ex.GetType().Name + ":" + ex.Message + "|" + detailExtra, mode, method); return 3; } } private static bool CheckUiResponsive(IntPtr hwnd) { try { if (IsHungAppWindow(hwnd)) return false; IntPtr result; IntPtr ok = SendMessageTimeout(hwnd, WM_NULL, IntPtr.Zero, IntPtr.Zero, 0x0002, 2000, out result); return ok != IntPtr.Zero; } catch { return false; } } private static void EnableScreenReaderFlag() { try { // SPI_SETSCREENREADER=0x46 SystemParametersInfo(0x0046, 1, IntPtr.Zero, 0x0003); } catch { } } private static bool TryN2(IntPtr hwnd, bool enableHook, bool callWakeProvider, out string detail, out IntPtr remoteModule) { remoteModule = IntPtr.Zero; string dll = FindPayloadDll(); if (dll == null) { detail = "n2_dll_missing"; return false; } uint pid; uint tid = GetWindowThreadProcessId(hwnd, out pid); if (pid == 0 || tid == 0) { detail = "n2_pid_tid_failed"; return false; } string targetFile = Path.Combine(Path.GetTempPath(), "jiangchang_desktop_wake_target.txt"); File.WriteAllText( targetFile, "hwnd=0x" + hwnd.ToInt64().ToString("x") + "\r\npid=" + pid + "\r\n", Encoding.ASCII); string loadedFile = Path.Combine(Path.GetTempPath(), "jiangchang_desktop_wake_loaded.txt"); string subclassFile = Path.Combine(Path.GetTempPath(), "jiangchang_desktop_wake_subclass.txt"); string hookFile = Path.Combine(Path.GetTempPath(), "jiangchang_desktop_wake_hook.txt"); string providerFile = Path.Combine(Path.GetTempPath(), "jiangchang_desktop_wake_provider.txt"); try { if (File.Exists(loadedFile)) File.Delete(loadedFile); } catch { } try { if (File.Exists(subclassFile)) File.Delete(subclassFile); } catch { } try { if (File.Exists(hookFile)) File.Delete(hookFile); } catch { } try { if (File.Exists(providerFile)) File.Delete(providerFile); } catch { } string injErr; bool injected = InjectLoadLibrary(pid, dll, out injErr, out remoteModule); string hookErr = enableHook ? "" : "skipped_unsafe_hook"; bool hooked = false; if (enableHook) { hooked = InstallCallWndHook(dll, tid, hwnd, out hookErr); PostMessage(hwnd, WM_NULL, IntPtr.Zero, IntPtr.Zero); Thread.Sleep(200); } string wakeErr = "skipped"; bool wakeCalled = false; if (callWakeProvider && injected && remoteModule != IntPtr.Zero) { wakeCalled = CallRemoteWakeProvider(pid, dll, remoteModule, hwnd, out wakeErr); // STA UIA 可能写完 marker 略晚;等 done=1 for (int i = 0; i < 40; i++) { if (File.Exists(providerFile)) { try { string pt = File.ReadAllText(providerFile); if (pt.IndexOf("done=1", StringComparison.Ordinal) >= 0) { wakeCalled = true; break; } } catch { } } Thread.Sleep(250); } if (!wakeCalled && File.Exists(providerFile)) wakeCalled = true; } bool loadedMarker = false; if (File.Exists(loadedFile)) { try { string text = File.ReadAllText(loadedFile); loadedMarker = text.IndexOf("pid=" + pid, StringComparison.Ordinal) >= 0 || text.IndexOf("match=1", StringComparison.Ordinal) >= 0; } catch { loadedMarker = File.Exists(loadedFile); } } bool subclassMarker = File.Exists(subclassFile); bool hookMarker = File.Exists(hookFile); bool providerMarker = File.Exists(providerFile); string providerText = ""; if (providerMarker) { try { providerText = File.ReadAllText(providerFile).Replace("\r", " ").Replace("\n", " ").Trim(); } catch { } } detail = string.Format( "pid={0};tid={1};inject={2};hook={3};wake={4};loaded_marker={5};provider_marker={6};provider={7};subclass_marker={8};hook_marker={9};injErr={10};hookErr={11};wakeErr={12};dll={13};remoteMod=0x{14}", pid, tid, injected, hooked, wakeCalled, loadedMarker, providerMarker, providerText, subclassMarker, hookMarker, injErr, hookErr, wakeErr, Path.GetFileName(dll), remoteModule.ToInt64().ToString("x")); // DLL 已在目标进程时再次 LoadLibrary 不会重跑 DllMain,故 remoteMod!=0 也算注入成功 bool injectOk = injected && (loadedMarker || remoteModule != IntPtr.Zero); // WakeProvider 为 best-effort:注入成功即可,暖树由外层 probe 判定 return injectOk || (hooked && (hookMarker || subclassMarker)); } private static string FindPayloadDll() { try { string exe = Process.GetCurrentProcess().MainModule.FileName; var dir = new DirectoryInfo(Path.GetDirectoryName(exe)); for (int i = 0; i < 6 && dir != null; i++) { string newest = null; DateTime best = DateTime.MinValue; string[] searchDirs = new string[] { dir.FullName, Path.Combine(dir.FullName, "native") }; foreach (var sd in searchDirs) { if (!Directory.Exists(sd)) continue; // 优先正式名 wecom_wake.dll(发版包) string primary = Path.Combine(sd, "wecom_wake.dll"); if (File.Exists(primary)) return Path.GetFullPath(primary); foreach (var f in Directory.GetFiles(sd, "wecom_wake*.dll")) { DateTime t = File.GetLastWriteTimeUtc(f); if (t >= best) { best = t; newest = f; } } } if (newest != null) return Path.GetFullPath(newest); dir = dir.Parent; } } catch { } return null; } private static bool InjectLoadLibrary(uint pid, string dllPath, out string err, out IntPtr remoteModule) { err = ""; remoteModule = IntPtr.Zero; IntPtr hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, false, pid); if (hProcess == IntPtr.Zero) { err = "OpenProcess:" + Marshal.GetLastWin32Error(); return false; } try { byte[] pathBytes = Encoding.Unicode.GetBytes(dllPath + "\0"); IntPtr remote = VirtualAllocEx(hProcess, IntPtr.Zero, (UIntPtr)pathBytes.Length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (remote == IntPtr.Zero) { err = "VirtualAllocEx:" + Marshal.GetLastWin32Error(); return false; } UIntPtr written; if (!WriteProcessMemory(hProcess, remote, pathBytes, (UIntPtr)pathBytes.Length, out written)) { err = "WriteProcessMemory:" + Marshal.GetLastWin32Error(); return false; } IntPtr hKernel = GetModuleHandle("kernel32.dll"); IntPtr loadLib = GetProcAddress(hKernel, "LoadLibraryW"); if (loadLib == IntPtr.Zero) { err = "GetProcAddress LoadLibraryW failed"; return false; } IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, UIntPtr.Zero, loadLib, remote, 0, IntPtr.Zero); if (hThread == IntPtr.Zero) { err = "CreateRemoteThread:" + Marshal.GetLastWin32Error(); return false; } uint wait = WaitForSingleObject(hThread, 5000); uint exitCode = 0; GetExitCodeThread(hThread, out exitCode); CloseHandle(hThread); VirtualFreeEx(hProcess, remote, UIntPtr.Zero, 0x8000); if (wait != 0) { err = "LoadLibrary wait timeout/status=" + wait; return false; } if (exitCode == 0) { err = "LoadLibrary returned NULL"; return false; } remoteModule = new IntPtr(unchecked((int)exitCode)); // x86 HMODULE return true; } finally { CloseHandle(hProcess); } } private static bool CallRemoteWakeProvider( uint pid, string dllPath, IntPtr remoteModule, IntPtr hwnd, out string err) { err = ""; IntPtr localMod = LoadLibrary(dllPath); if (localMod == IntPtr.Zero) { err = "local LoadLibrary:" + Marshal.GetLastWin32Error(); return false; } IntPtr localProc = GetProcAddress(localMod, "WakeProvider"); if (localProc == IntPtr.Zero) localProc = GetProcAddress(localMod, "_WakeProvider@4"); if (localProc == IntPtr.Zero) { err = "WakeProvider export missing"; return false; } long offset = localProc.ToInt64() - localMod.ToInt64(); IntPtr remoteProc = new IntPtr(remoteModule.ToInt64() + offset); IntPtr hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, false, pid); if (hProcess == IntPtr.Zero) { err = "OpenProcess wake:" + Marshal.GetLastWin32Error(); return false; } try { IntPtr hThread = CreateRemoteThread( hProcess, IntPtr.Zero, UIntPtr.Zero, remoteProc, hwnd, 0, IntPtr.Zero); if (hThread == IntPtr.Zero) { err = "CreateRemoteThread WakeProvider:" + Marshal.GetLastWin32Error(); return false; } // STA + 进程内 FindAll 可能较慢 uint wait = WaitForSingleObject(hThread, 20000); CloseHandle(hThread); if (wait != 0) { err = "WakeProvider wait=" + wait; return false; } return true; } finally { CloseHandle(hProcess); } } private static bool InstallCallWndHook(string dllPath, uint tid, IntPtr hwnd, out string err) { err = ""; IntPtr hMod = LoadLibrary(dllPath); if (hMod == IntPtr.Zero) { err = "LoadLibrary:" + Marshal.GetLastWin32Error(); return false; } // x86 __stdcall 可能是 _JcCallWndProc@12;优先无修饰名(.def 导出) IntPtr proc = GetProcAddress(hMod, "JcCallWndProc"); if (proc == IntPtr.Zero) proc = GetProcAddress(hMod, "_JcCallWndProc@12"); if (proc == IntPtr.Zero) { err = "GetProcAddress JcCallWndProc:" + Marshal.GetLastWin32Error(); return false; } IntPtr hhk = SetWindowsHookEx(WH_CALLWNDPROC, proc, hMod, tid); if (hhk == IntPtr.Zero) { err = "SetWindowsHookEx:" + Marshal.GetLastWin32Error(); return false; } PostMessage(hwnd, WM_NULL, IntPtr.Zero, IntPtr.Zero); Thread.Sleep(400); // 保持钩子,避免 DLL 被立刻卸载;进程退出时清理 GC.KeepAlive(hhk); return true; } private static IntPtr ParseHwnd(string s) { s = (s ?? "").Trim(); if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) return new IntPtr(Convert.ToInt64(s, 16)); // 含 a-f 一定是 hex;纯数字优先按十进制(Python 可传 {hwnd}) bool hasHexLetter = false; for (int i = 0; i < s.Length; i++) { char c = s[i]; if ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { hasHexLetter = true; break; } } if (hasHexLetter) return new IntPtr(Convert.ToInt64(s, 16)); long asDec; if (long.TryParse(s, out asDec)) return new IntPtr(asDec); return new IntPtr(Convert.ToInt64(s, 16)); } private static void Activate(IntPtr hwnd) { try { if (IsIconic(hwnd)) ShowWindow(hwnd, SW_RESTORE); else ShowWindow(hwnd, SW_MAXIMIZE); SetForegroundWindow(hwnd); Thread.Sleep(250); } catch { } } private static void SendGetObject(IntPtr hwnd) { SendMessage(hwnd, WM_GETOBJECT, IntPtr.Zero, new IntPtr(unchecked((int)OBJID_CLIENT))); SendMessage(hwnd, WM_GETOBJECT, IntPtr.Zero, new IntPtr(unchecked((int)OBJID_WINDOW))); } private static void TryMsaa(IntPtr hwnd) { try { Guid iid = IID_IAccessible; object acc; int hr = AccessibleObjectFromWindow(hwnd, OBJID_CLIENT, ref iid, out acc); if (hr == 0 && acc != null) Marshal.FinalReleaseComObject(acc); } catch { } } private static int WalkCount(AutomationElement root, int maxDepth) { int count = 0; var walker = TreeWalker.ControlViewWalker; Walk(walker, root, 0, maxDepth, ref count); return count; } private static void Walk(TreeWalker walker, AutomationElement node, int depth, int maxDepth, ref int count) { if (node == null || depth > maxDepth) return; count++; AutomationElement child; try { child = walker.GetFirstChild(node); } catch { return; } while (child != null) { Walk(walker, child, depth + 1, maxDepth, ref count); try { child = walker.GetNextSibling(child); } catch { break; } } } private static int FindAllCount(AutomationElement root) { try { var all = root.FindAll(TreeScope.Subtree, Condition.TrueCondition); return all == null ? 0 : all.Count; } catch { return 0; } } private static List FindMarkers(AutomationElement root, string[] markers) { var found = new List(); if (markers == null || markers.Length == 0) return found; foreach (var m in markers) { try { var el = root.FindFirst(TreeScope.Subtree, new PropertyCondition(AutomationElement.NameProperty, m)); if (el != null) { found.Add(m); continue; } el = root.FindFirst(TreeScope.Subtree, new PropertyCondition(AutomationElement.AutomationIdProperty, m)); if (el != null) found.Add(m); } catch { } } return found; } private static void WriteJson( bool ok, bool injectOk, bool uiResponsive, string profile, IntPtr hwnd, int walk, int findAll, string[] markers, string detail, string mode, string method) { var sb = new StringBuilder(); sb.Append("{"); sb.Append("\"ok\":").Append(ok ? "true" : "false").Append(","); sb.Append("\"inject_ok\":").Append(injectOk ? "true" : "false").Append(","); sb.Append("\"ui_responsive\":").Append(uiResponsive ? "true" : "false").Append(","); sb.Append("\"profile\":\"").Append(Escape(profile)).Append("\","); sb.Append("\"hwnd\":\"").Append(hwnd.ToInt64().ToString("x")).Append("\","); sb.Append("\"mode\":\"").Append(Escape(mode ?? "")).Append("\","); sb.Append("\"method\":\"").Append(Escape(method ?? "")).Append("\","); sb.Append("\"walk_count\":").Append(walk).Append(","); sb.Append("\"findall_count\":").Append(findAll).Append(","); sb.Append("\"markers_found\":["); for (int i = 0; i < markers.Length; i++) { if (i > 0) sb.Append(","); sb.Append("\"").Append(Escape(markers[i])).Append("\""); } sb.Append("],"); sb.Append("\"detail\":\"").Append(Escape(detail)).Append("\""); sb.Append("}"); Console.WriteLine(sb.ToString()); } private static string Escape(string s) { if (string.IsNullOrEmpty(s)) return ""; return s.Replace("\\", "\\\\").Replace("\"", "\\\""); } } }