using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.IO.Pipes; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using LANCommander.Interposer.Events; namespace LANCommander.Interposer { /// /// Single entry point for LANCommander Interposer: DLL injection, plugin API /// (logging, config, virtual registry, rich presence), and hook events (file, /// registry, DNS, network, identity). /// /// Launcher usage: set override properties, then call /// or /// to inject the Interposer DLL into a game process. /// /// /// In-process usage: call to register native /// event callbacks, then use the plugin API methods and subscribe to events. /// /// public class InterposerService : IDisposable { private const string DllName = "LANCommander.Interposer.dll"; // ================================================================= // P/Invoke - Plugin API // ================================================================= [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool InterposerGetUsername( [Out] StringBuilder buffer, uint bufferSize); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] private static extern void InterposerLog(string verb, string message); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool InterposerGetConfigString( string dotPath, [Out] StringBuilder buffer, uint bufferSize); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool InterposerRegisterPluginConfig( string pluginName, string yamlDefaults); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] private static extern void InterposerSetRegistryValue( string keyPath, string valueName, string value); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] private static extern uint InterposerSetRegistryValueBySuffix( string keySuffix, string valueName, string value); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerSetPresenceDetails([MarshalAs(UnmanagedType.LPUTF8Str)] string details); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerSetPresenceState([MarshalAs(UnmanagedType.LPUTF8Str)] string state); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerSetPresenceTimestamps(long start, long end); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerSetPresenceLargeImage( [MarshalAs(UnmanagedType.LPUTF8Str)] string key, [MarshalAs(UnmanagedType.LPUTF8Str)] string text); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerSetPresenceSmallImage( [MarshalAs(UnmanagedType.LPUTF8Str)] string key, [MarshalAs(UnmanagedType.LPUTF8Str)] string text); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerSetPresenceParty( [MarshalAs(UnmanagedType.LPUTF8Str)] string id, int size, int max); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerSetPresenceButton( int index, [MarshalAs(UnmanagedType.LPUTF8Str)] string text, [MarshalAs(UnmanagedType.LPUTF8Str)] string url); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerSetPresenceType(int type); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerSetPresenceName([MarshalAs(UnmanagedType.LPUTF8Str)] string name); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerUpdatePresence(); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerClearPresence(); // ================================================================= // P/Invoke - Callback registration // ================================================================= [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerRegisterFileCallback(IntPtr cb); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerRegisterRegistryCallback(IntPtr cb); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerRegisterDnsCallback(IntPtr cb); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerRegisterNetworkCallback(IntPtr cb); [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] private static extern void InterposerRegisterIdentityCallback(IntPtr cb); // ================================================================= // Native callback delegate types // ================================================================= [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Unicode)] private delegate void NativeFileCallback(IntPtr verb, IntPtr path, IntPtr secondaryPath); [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Unicode)] private delegate void NativeRegistryCallback(IntPtr verb, IntPtr keyPath, IntPtr valueName); [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Unicode)] private delegate void NativeDnsCallback(IntPtr hostname, IntPtr redirectedHostname); [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Unicode)] private delegate void NativeNetworkCallback(IntPtr address, int port); [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Unicode)] private delegate void NativeIdentityCallback(IntPtr type, IntPtr value); // ================================================================= // Instance state // ================================================================= private NativeFileCallback _fileDelegate; private NativeRegistryCallback _registryDelegate; private NativeDnsCallback _dnsDelegate; private NativeNetworkCallback _networkDelegate; private NativeIdentityCallback _identityDelegate; private bool _eventsEnabled; private IntPtr _fastDlMmf; private IntPtr _usernameMmf; private IntPtr _computerNameMmf; private NamedPipeServerStream _eventPipe; private CancellationTokenSource _eventCts; private Task _eventPipeTask; private bool _disposed; // ================================================================= // Injection properties // ================================================================= /// /// Optional FastDL base URL override. When set, a named memory-mapped file /// is created so the DLL reads this URL instead of the one in Config.yml. /// public string FastDlUrl { get; set; } /// /// Optional username override. When set, a named memory-mapped file is /// created so the DLL returns this name from GetUserNameW/A. /// public string Username { get; set; } /// /// Optional computer name override. When set, a named memory-mapped file /// is created so the DLL returns this name from GetComputerNameW/A. /// public string ComputerName { get; set; } // ================================================================= // Events // ================================================================= /// /// Fires on file operations: CreateFile, GetFileAttributes, FindFirstFile, /// Delete, Move, Copy, LoadLibrary. /// Works cross-process via named pipe when using or /// , and in-process via . /// public event EventHandler FileAccessed; /// /// Fires on registry operations: Open, Create, Query, Set, Delete, Enum. /// Works cross-process via named pipe when using or /// , and in-process via . /// public event EventHandler RegistryAccessed; /// /// Fires on DNS redirect events (when a hostname lookup matches a DnsRedirects rule). /// Works cross-process via named pipe when using or /// , and in-process via . /// public event EventHandler DnsResolved; /// /// Fires when a new network address is discovered (connect, send/recv). /// Each unique address fires only once per session. /// Works cross-process via named pipe when using or /// , and in-process via . /// public event EventHandler NetworkConnection; /// /// Fires when GetUserName or GetComputerName is called and an identity /// override is active. /// Works cross-process via named pipe when using or /// , and in-process via . /// public event EventHandler IdentityQueried; // ================================================================= // Injection - Process integration // ================================================================= /// /// Injects the Interposer DLL into an already-running process. /// /// The target process. Must not have exited. /// /// Path to the Interposer DLL. If null, searches for interposer.dll or /// LANCommander.Interposer.dll next to the application. /// /// Thrown if injection fails. public void Inject(Process process, string dllPath = null) { if (process == null) throw new ArgumentNullException(nameof(process)); Inject(process.Id, dllPath); } /// /// Injects the Interposer DLL into a process identified by PID. /// /// Target process ID. /// /// Path to the Interposer DLL. If null, searches for interposer.dll or /// LANCommander.Interposer.dll next to the application. /// /// Thrown if injection fails. public void Inject(int processId, string dllPath = null) { dllPath = ResolveDllPath(dllPath); var hProcess = NativeMethods.OpenProcess( NativeMethods.PROCESS_CREATE_THREAD | NativeMethods.PROCESS_VM_OPERATION | NativeMethods.PROCESS_VM_WRITE | NativeMethods.PROCESS_VM_READ | NativeMethods.PROCESS_QUERY_INFORMATION, false, (uint)processId); if (hProcess == IntPtr.Zero) throw new InjectionException($"OpenProcess failed for PID {processId}.", new Win32Exception()); try { CreateMappings((uint)processId); InjectDll(hProcess, dllPath); } finally { NativeMethods.CloseHandle(hProcess); } } /// /// Launches a process suspended, injects the Interposer DLL, then resumes it. /// The returned is already running with the DLL loaded. /// /// The must be set. Other properties /// like , /// , and /// are respected. /// /// /// Describes the process to launch. /// /// Path to the Interposer DLL. If null, searches for interposer.dll or /// LANCommander.Interposer.dll next to the application. /// /// The launched with the Interposer DLL loaded. /// Thrown if the process cannot be created or injection fails. public Process Start(ProcessStartInfo startInfo, string dllPath = null) { if (startInfo == null) throw new ArgumentNullException(nameof(startInfo)); dllPath = ResolveDllPath(dllPath); var exePath = Path.GetFullPath(startInfo.FileName); if (!File.Exists(exePath)) throw new InjectionException($"Executable not found: {exePath}"); var workingDirectory = string.IsNullOrEmpty(startInfo.WorkingDirectory) ? Path.GetDirectoryName(exePath) : startInfo.WorkingDirectory; // Build command line var cmdLine = new StringBuilder(); cmdLine.Append('"').Append(exePath).Append('"'); if (!string.IsNullOrEmpty(startInfo.Arguments)) { cmdLine.Append(' '); cmdLine.Append(startInfo.Arguments); } // Build environment block if custom variables are set IntPtr envBlock = IntPtr.Zero; uint creationFlags = NativeMethods.CREATE_SUSPENDED; if (startInfo.Environment != null && startInfo.Environment.Count > 0) { var envBuilder = new StringBuilder(); foreach (var kvp in startInfo.Environment) { envBuilder.Append(kvp.Key); envBuilder.Append('='); envBuilder.Append(kvp.Value); envBuilder.Append('\0'); } envBuilder.Append('\0'); envBlock = Marshal.StringToHGlobalUni(envBuilder.ToString()); creationFlags |= NativeMethods.CREATE_UNICODE_ENVIRONMENT; } var si = new NativeMethods.STARTUPINFO(); si.cb = Marshal.SizeOf(si); NativeMethods.PROCESS_INFORMATION pi; try { if (!NativeMethods.CreateProcessW( exePath, cmdLine, IntPtr.Zero, IntPtr.Zero, false, creationFlags, envBlock, workingDirectory, ref si, out pi)) { throw new InjectionException("CreateProcess failed.", new Win32Exception()); } } finally { if (envBlock != IntPtr.Zero) Marshal.FreeHGlobal(envBlock); } try { CreateMappings(pi.dwProcessId); InjectDll(pi.hProcess, dllPath); NativeMethods.ResumeThread(pi.hThread); return Process.GetProcessById((int)pi.dwProcessId); } catch { NativeMethods.TerminateProcess(pi.hProcess, 1); throw; } finally { NativeMethods.CloseHandle(pi.hThread); NativeMethods.CloseHandle(pi.hProcess); } } // ================================================================= // Events - lifecycle // ================================================================= /// /// Registers native event callbacks so that hook events fire on this /// service instance. Only callable from within a process that has the /// Interposer DLL loaded. Safe to call multiple times. /// public void EnableEvents() { if (_eventsEnabled) return; _fileDelegate = OnFileCallback; _registryDelegate = OnRegistryCallback; _dnsDelegate = OnDnsCallback; _networkDelegate = OnNetworkCallback; _identityDelegate = OnIdentityCallback; InterposerRegisterFileCallback( Marshal.GetFunctionPointerForDelegate(_fileDelegate)); InterposerRegisterRegistryCallback( Marshal.GetFunctionPointerForDelegate(_registryDelegate)); InterposerRegisterDnsCallback( Marshal.GetFunctionPointerForDelegate(_dnsDelegate)); InterposerRegisterNetworkCallback( Marshal.GetFunctionPointerForDelegate(_networkDelegate)); InterposerRegisterIdentityCallback( Marshal.GetFunctionPointerForDelegate(_identityDelegate)); _eventsEnabled = true; } /// /// Unregisters native event callbacks. No further events will fire /// until is called again. /// public void DisableEvents() { if (!_eventsEnabled) return; InterposerRegisterFileCallback(IntPtr.Zero); InterposerRegisterRegistryCallback(IntPtr.Zero); InterposerRegisterDnsCallback(IntPtr.Zero); InterposerRegisterNetworkCallback(IntPtr.Zero); InterposerRegisterIdentityCallback(IntPtr.Zero); _fileDelegate = null; _registryDelegate = null; _dnsDelegate = null; _networkDelegate = null; _identityDelegate = null; _eventsEnabled = false; } // ================================================================= // Dispose // ================================================================= /// /// Unregisters native event callbacks and releases memory-mapped file handles. /// public void Dispose() { if (_disposed) return; _disposed = true; if (_eventsEnabled) DisableEvents(); if (_eventCts != null) { _eventCts.Cancel(); _eventCts.Dispose(); _eventCts = null; } if (_eventPipe != null) { _eventPipe.Dispose(); _eventPipe = null; } CloseMmf(ref _fastDlMmf); CloseMmf(ref _usernameMmf); CloseMmf(ref _computerNameMmf); } // ================================================================= // Config & Logging (in-process only) // ================================================================= /// /// Gets the effective username (configured override or real Windows account name). /// Returns null if the call fails. In-process only. /// public string GetUsername() { var sb = new StringBuilder(256); return InterposerGetUsername(sb, (uint)sb.Capacity) ? sb.ToString() : null; } /// /// Writes a line to the Interposer session log regardless of logging flags. /// In-process only. /// /// Log verb padded to ~15 chars for alignment, e.g. "[MYPLUGIN] " /// Log message text. public void Log(string verb, string message) { InterposerLog(verb, message); } /// /// Reads a scalar value from Config.yml by dot-separated YAML path. /// Returns null if the key is missing, not scalar, or the buffer is too small. /// In-process only. /// /// Dot-separated path, e.g. "Plugins.MyPlugin.Setting" public string GetConfigString(string dotPath) { var sb = new StringBuilder(1024); return InterposerGetConfigString(dotPath, sb, (uint)sb.Capacity) ? sb.ToString() : null; } /// /// Registers default configuration for a plugin. If a Plugins.<pluginName> /// section already exists in Config.yml, the defaults are not written. /// In-process only. /// /// Key under Plugins: in Config.yml (e.g. "CDKey"). /// YAML map body with default keys/values. /// True if the section existed or defaults were written successfully. public bool RegisterPluginConfig(string pluginName, string yamlDefaults) { return InterposerRegisterPluginConfig(pluginName, yamlDefaults); } // ================================================================= // Virtual Registry (in-process only) // ================================================================= /// /// Injects a transient REG_SZ value into the virtual registry store by exact key path. /// In-process only. /// public void SetRegistryValue(string keyPath, string valueName, string value) { InterposerSetRegistryValue(keyPath, valueName, value); } /// /// Injects a transient REG_SZ value into every virtual store key whose path ends /// with the given suffix (backslash-boundary, case-insensitive match). /// In-process only. /// /// The number of keys updated. public uint SetRegistryValueBySuffix(string keySuffix, string valueName, string value) { return InterposerSetRegistryValueBySuffix(keySuffix, valueName, value); } // ================================================================= // Rich Presence (in-process only) // ================================================================= /// Sets the first detail line of the rich presence. public void SetPresenceDetails(string details) => InterposerSetPresenceDetails(details); /// Sets the second detail line (state) of the rich presence. public void SetPresenceState(string state) => InterposerSetPresenceState(state); /// Sets the start/end timestamps (Unix epoch seconds, 0 = not set). public void SetPresenceTimestamps(long start, long end) => InterposerSetPresenceTimestamps(start, end); /// Sets the large image asset key and tooltip text. public void SetPresenceLargeImage(string key, string text) => InterposerSetPresenceLargeImage(key, text); /// Sets the small image asset key and tooltip text. public void SetPresenceSmallImage(string key, string text) => InterposerSetPresenceSmallImage(key, text); /// Sets the party info (id, current size, max size). public void SetPresenceParty(string id, int size, int max) => InterposerSetPresenceParty(id, size, max); /// Sets a presence button (index 0 or 1, label, URL). public void SetPresenceButton(int index, string text, string url) => InterposerSetPresenceButton(index, text, url); /// Sets the activity type (0=Playing, 1=Streaming, 2=Listening, 3=Watching, 5=Competing). public void SetPresenceType(int type) => InterposerSetPresenceType(type); /// Sets the display name shown after the activity verb. public void SetPresenceName(string name) => InterposerSetPresenceName(name); /// Pushes the current presence state to all connected backends (e.g. Discord). public void UpdatePresence() => InterposerUpdatePresence(); /// Disconnects all backends and clears the presence. public void ClearPresence() => InterposerClearPresence(); // ================================================================= // Private - injection helpers // ================================================================= private static string ResolveDllPath(string dllPath) { if (!string.IsNullOrEmpty(dllPath)) { dllPath = Path.GetFullPath(dllPath); if (!File.Exists(dllPath)) throw new InjectionException($"DLL not found: {dllPath}"); return dllPath; } var baseDir = AppDomain.CurrentDomain.BaseDirectory; var candidates = new[] { "interposer.dll", "LANCommander.Interposer.dll" }; foreach (var candidate in candidates) { var path = Path.Combine(baseDir, candidate); if (File.Exists(path)) return path; } throw new InjectionException( "No DLL path specified and neither interposer.dll nor " + "LANCommander.Interposer.dll was found next to the application."); } private void CreateMappings(uint pid) { if (!string.IsNullOrEmpty(FastDlUrl)) _fastDlMmf = CreateUtf8Mmf($"Local\\InterposerFastDL_{pid}", FastDlUrl, 2048); if (!string.IsNullOrEmpty(Username)) _usernameMmf = CreateUtf8Mmf($"Local\\InterposerUsername_{pid}", Username, 512); if (!string.IsNullOrEmpty(ComputerName)) _computerNameMmf = CreateUtf8Mmf($"Local\\InterposerComputerName_{pid}", ComputerName, 512); // Create a named pipe for the injected DLL to send events back. // The pipe must be in a listening state before the DLL is injected // so it can connect during DLL_PROCESS_ATTACH. var pipeName = $"InterposerEvents_{pid}"; _eventPipe = new NamedPipeServerStream( pipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); _eventCts = new CancellationTokenSource(); // Start WaitForConnectionAsync BEFORE returning so the pipe is in // listening state when InjectDll triggers DllMain → ConnectEventPipe. // The task continues reading in the background once a client connects. _eventPipeTask = ReadEventPipeAsync(_eventCts.Token); } private static IntPtr CreateUtf8Mmf(string name, string value, uint size) { var hMmf = NativeMethods.CreateFileMappingW( new IntPtr(-1), IntPtr.Zero, NativeMethods.PAGE_READWRITE, 0, size, name); if (hMmf == IntPtr.Zero) throw new InjectionException($"CreateFileMapping failed for '{name}'.", new Win32Exception()); var view = NativeMethods.MapViewOfFile(hMmf, NativeMethods.FILE_MAP_WRITE, 0, 0, (UIntPtr)size); if (view == IntPtr.Zero) { NativeMethods.CloseHandle(hMmf); throw new InjectionException($"MapViewOfFile failed for '{name}'.", new Win32Exception()); } try { var utf8 = Encoding.UTF8.GetBytes(value + '\0'); if (utf8.Length <= size) Marshal.Copy(utf8, 0, view, utf8.Length); } finally { NativeMethods.UnmapViewOfFile(view); } return hMmf; } private static void InjectDll(IntPtr hProcess, string dllPath) { var pathBytes = Encoding.Unicode.GetBytes(dllPath + '\0'); var pathSize = (UIntPtr)pathBytes.Length; var remoteMem = NativeMethods.VirtualAllocEx( hProcess, IntPtr.Zero, pathSize, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_READWRITE); if (remoteMem == IntPtr.Zero) throw new InjectionException("VirtualAllocEx failed.", new Win32Exception()); try { UIntPtr written; if (!NativeMethods.WriteProcessMemory(hProcess, remoteMem, pathBytes, pathSize, out written) || written != pathSize) { throw new InjectionException("WriteProcessMemory failed.", new Win32Exception()); } var hKernel32 = NativeMethods.GetModuleHandleW("kernel32.dll"); var loadLibrary = NativeMethods.GetProcAddress(hKernel32, "LoadLibraryW"); if (loadLibrary == IntPtr.Zero) throw new InjectionException("Could not resolve LoadLibraryW."); var hThread = NativeMethods.CreateRemoteThread( hProcess, IntPtr.Zero, UIntPtr.Zero, loadLibrary, remoteMem, 0, IntPtr.Zero); if (hThread == IntPtr.Zero) throw new InjectionException( "CreateRemoteThread failed. Try running as Administrator.", new Win32Exception()); NativeMethods.WaitForSingleObject(hThread, 0xFFFFFFFF); uint exitCode; NativeMethods.GetExitCodeThread(hThread, out exitCode); NativeMethods.CloseHandle(hThread); if (exitCode == 0) throw new InjectionException("Injection failed: LoadLibraryW returned NULL in the target process."); } finally { NativeMethods.VirtualFreeEx(hProcess, remoteMem, UIntPtr.Zero, NativeMethods.MEM_RELEASE); } } private static void CloseMmf(ref IntPtr handle) { if (handle != IntPtr.Zero) { NativeMethods.CloseHandle(handle); handle = IntPtr.Zero; } } // ================================================================= // Private - named pipe event reader // ================================================================= /// /// Fires when the pipe reader encounters a diagnostic condition (connection, disconnect, error). /// Subscribe before calling to observe pipe lifecycle. /// public event EventHandler PipeDiagnostic; private async Task ReadEventPipeAsync(CancellationToken ct) { try { PipeDiagnostic?.Invoke(this, "Pipe: waiting for connection..."); await _eventPipe.WaitForConnectionAsync(ct).ConfigureAwait(false); PipeDiagnostic?.Invoke(this, "Pipe: client connected."); // Buffer for reading from the pipe. Messages are variable-length // binary: int32 eventType, then 3x (int32 len + wchar[len]), then int32. var intBuf = new byte[4]; while (!ct.IsCancellationRequested && _eventPipe.IsConnected) { if (!await ReadExactAsync(_eventPipe, intBuf, ct).ConfigureAwait(false)) break; int eventType = BitConverter.ToInt32(intBuf, 0); string field1 = await ReadPipeStringAsync(_eventPipe, intBuf, ct).ConfigureAwait(false); string field2 = await ReadPipeStringAsync(_eventPipe, intBuf, ct).ConfigureAwait(false); string field3 = await ReadPipeStringAsync(_eventPipe, intBuf, ct).ConfigureAwait(false); if (!await ReadExactAsync(_eventPipe, intBuf, ct).ConfigureAwait(false)) break; int intField = BitConverter.ToInt32(intBuf, 0); DispatchPipeEvent(eventType, field1, field2, field3, intField); } } catch (OperationCanceledException) { PipeDiagnostic?.Invoke(this, "Pipe: cancelled."); } catch (IOException ex) { PipeDiagnostic?.Invoke(this, $"Pipe: IO error - {ex.Message}"); } catch (ObjectDisposedException) { PipeDiagnostic?.Invoke(this, "Pipe: disposed."); } } private static async Task ReadPipeStringAsync( NamedPipeServerStream pipe, byte[] intBuf, CancellationToken ct) { if (!await ReadExactAsync(pipe, intBuf, ct).ConfigureAwait(false)) return null; int charCount = BitConverter.ToInt32(intBuf, 0); if (charCount <= 0) return null; var strBuf = new byte[charCount * 2]; // wchar_t = 2 bytes if (!await ReadExactAsync(pipe, strBuf, ct).ConfigureAwait(false)) return null; return Encoding.Unicode.GetString(strBuf); } private static async Task ReadExactAsync( NamedPipeServerStream pipe, byte[] buffer, CancellationToken ct) { int offset = 0; while (offset < buffer.Length) { int read = await pipe.ReadAsync(buffer, offset, buffer.Length - offset, ct) .ConfigureAwait(false); if (read == 0) return false; // pipe closed offset += read; } return true; } private void DispatchPipeEvent(int eventType, string field1, string field2, string field3, int intField) { switch (eventType) { case 1: // FILE FileAccessed?.Invoke(this, new FileEventArgs(field1, field2, field3)); break; case 2: // REGISTRY RegistryAccessed?.Invoke(this, new RegistryEventArgs(field1, field2, field3)); break; case 3: // DNS DnsResolved?.Invoke(this, new DnsEventArgs(field1, field2)); break; case 4: // NETWORK NetworkConnection?.Invoke(this, new NetworkEventArgs(field1, intField)); break; case 5: // IDENTITY IdentityQueried?.Invoke(this, new IdentityEventArgs(field1, field2)); break; } } // ================================================================= // Private - native callback handlers // ================================================================= private void OnFileCallback(IntPtr verb, IntPtr path, IntPtr secondaryPath) { FileAccessed?.Invoke(this, new FileEventArgs( Marshal.PtrToStringUni(verb), Marshal.PtrToStringUni(path), secondaryPath != IntPtr.Zero ? Marshal.PtrToStringUni(secondaryPath) : null)); } private void OnRegistryCallback(IntPtr verb, IntPtr keyPath, IntPtr valueName) { RegistryAccessed?.Invoke(this, new RegistryEventArgs( Marshal.PtrToStringUni(verb), Marshal.PtrToStringUni(keyPath), valueName != IntPtr.Zero ? Marshal.PtrToStringUni(valueName) : null)); } private void OnDnsCallback(IntPtr hostname, IntPtr redirectedHostname) { DnsResolved?.Invoke(this, new DnsEventArgs( Marshal.PtrToStringUni(hostname), Marshal.PtrToStringUni(redirectedHostname))); } private void OnNetworkCallback(IntPtr address, int port) { NetworkConnection?.Invoke(this, new NetworkEventArgs( Marshal.PtrToStringUni(address), port)); } private void OnIdentityCallback(IntPtr type, IntPtr value) { IdentityQueried?.Invoke(this, new IdentityEventArgs( Marshal.PtrToStringUni(type), Marshal.PtrToStringUni(value))); } } }