tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
WindowsNetworkPromptReaper.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.ComponentModel;
4using System.Linq;
5using System.Runtime.InteropServices;
6using System.Text;
7using System.Threading;
8using System.Threading.Tasks;
9
10using Microsoft.Extensions.Hosting;
11using Microsoft.Extensions.Logging;
12
14
16{
19 {
23 const int SendMessageCount = 5;
24
28 const int RecheckDelayMs = 250;
29
34
38 readonly ILogger<WindowsNetworkPromptReaper> logger;
39
43 readonly List<IProcess> registeredProcesses;
44
51 static bool EnumWindow(IntPtr hWnd, IntPtr lParam)
52 {
53 var gcChildhandlesList = GCHandle.FromIntPtr(lParam);
54
55 if (gcChildhandlesList.Target == null)
56 return false;
57
58 var childHandles = (List<IntPtr>)gcChildhandlesList.Target;
59 childHandles.Add(hWnd);
60
61 return true;
62 }
63
69 static List<IntPtr> GetAllChildHandles(IntPtr mainWindow)
70 {
71 var childHandles = new List<IntPtr>();
72 var gcChildhandlesList = GCHandle.Alloc(childHandles);
73 try
74 {
75 var pointerChildHandlesList = GCHandle.ToIntPtr(gcChildhandlesList);
76 NativeMethods.EnumWindowProc childProc = new(EnumWindow);
77 NativeMethods.EnumChildWindows(mainWindow, childProc, pointerChildHandlesList);
78 }
79 finally
80 {
81 gcChildhandlesList.Free();
82 }
83
84 return childHandles;
85 }
86
92 public WindowsNetworkPromptReaper(IAsyncDelayer asyncDelayer, ILogger<WindowsNetworkPromptReaper> logger)
93 {
94 this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
95 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
96
97 registeredProcesses = new List<IProcess>();
98 }
99
101 public void RegisterProcess(IProcess process)
102 {
103 ArgumentNullException.ThrowIfNull(process);
104
106 {
107 if (registeredProcesses.Contains(process))
108 throw new InvalidOperationException("This process has already been registered for network prompt reaping!");
109 logger.LogTrace("Registering process {pid}...", process.Id);
110 registeredProcesses.Add(process);
111 }
112
113 process.Lifetime.ContinueWith(
114 x =>
115 {
116 logger.LogTrace("Unregistering process {pid}...", process.Id);
118 registeredProcesses.Remove(process);
119 },
120 TaskScheduler.Current);
121 }
122
124 protected override async Task ExecuteAsync(CancellationToken cancellationToken)
125 {
126 logger.LogDebug("Starting network prompt reaper...");
127 try
128 {
129 while (!cancellationToken.IsCancellationRequested)
130 {
131 await asyncDelayer.Delay(TimeSpan.FromMilliseconds(RecheckDelayMs), cancellationToken);
132
133 IntPtr window;
134 int processId;
136 {
137 if (registeredProcesses.Count == 0)
138 continue;
139
140 window = NativeMethods.FindWindow(null, "Network Accessibility");
141 if (window == IntPtr.Zero)
142 continue;
143
144 // found a bitch
145 var threadId = NativeMethods.GetWindowThreadProcessId(window, out processId);
146 if (!registeredProcesses.Any(x => x.Id == processId))
147 continue; // not our bitch
148 }
149
150 logger.LogTrace("Identified \"Network Accessibility\" window in owned process {pid}", processId);
151
152 var found = false;
153 foreach (var childHandle in GetAllChildHandles(window))
154 {
155 const int MaxLength = 10;
156 var stringBuilder = new StringBuilder(MaxLength + 1);
157
158 if (NativeMethods.GetWindowText(childHandle, stringBuilder, MaxLength) == 0)
159 {
160 logger.LogWarning(new Win32Exception(), "Error calling GetWindowText!");
161 continue;
162 }
163
164 var windowText = stringBuilder.ToString();
165 if (windowText == "Yes")
166 {
167 // smash_button_meme.jpg
168 logger.LogTrace("Sending \"Yes\" button clicks...");
169 for (var iteration = 0; iteration < SendMessageCount; ++iteration)
170 {
171 const int BM_CLICK = 0x00F5;
172 var result = NativeMethods.SendMessage(childHandle, BM_CLICK, IntPtr.Zero, IntPtr.Zero);
173 }
174
175 found = true;
176 break;
177 }
178 }
179
180 if (!found)
181 logger.LogDebug("Unable to find \"Yes\" button for \"Network Accessibility\" window in owned process {pid}!", processId);
182 }
183 }
184 catch (OperationCanceledException ex)
185 {
186 logger.LogTrace(ex, "Cancelled!");
187 }
188 finally
189 {
190 registeredProcesses.Clear();
191 logger.LogTrace("Exiting network prompt reaper...");
192 }
193 }
194 }
195}
Native methods used by the code.
static int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId)
See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getwindowthreadprocessid.
static bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam)
See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-enumchildwindows.
static IntPtr FindWindow(string? lpClassName, string lpWindowName)
See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-findwindoww.
static int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam)
See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-sendmessage.
static int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount)
See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getwindowtextw.
readonly ILogger< WindowsNetworkPromptReaper > logger
The ILogger for the WindowsNetworkPromptReaper.
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the WindowsNetworkPromptReaper.
void RegisterProcess(IProcess process)
Register a given process for network prompt reaping.
WindowsNetworkPromptReaper(IAsyncDelayer asyncDelayer, ILogger< WindowsNetworkPromptReaper > logger)
Initializes a new instance of the WindowsNetworkPromptReaper class.
const int SendMessageCount
Number of times to send the button click message. Should be at least 2 or it may fail to focus the wi...
static bool EnumWindow(IntPtr hWnd, IntPtr lParam)
Callback for NativeMethods.EnumChildWindows(IntPtr, NativeMethods.EnumWindowProc, IntPtr).
const int RecheckDelayMs
Check for prompts each time this amount of milliseconds pass.
static List< IntPtr > GetAllChildHandles(IntPtr mainWindow)
Get all the child windows handles of a given mainWindow .
override async Task ExecuteAsync(CancellationToken cancellationToken)
readonly List< IProcess > registeredProcesses
The list of IProcesss registered.
On Windows, DreamDaemon will show an unskippable prompt when using /world/proc/OpenPort()....
Task< int?> Lifetime
The Task<TResult> resulting in the exit code of the process or null if the process was detached.
Abstraction over a global::System.Diagnostics.Process.
Definition IProcess.cs:11
ValueTask Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan .