Repository navigation
Expand file tree
/
Copy pathWindowsRadio.Power.cs
More file actions
315 lines (289 loc) · 11.7 KB
/
Copy pathWindowsRadio.Power.cs
File metadata and controls
315 lines (289 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Windows.Devices.Radios;
using Microsoft.Win32;
namespace WindowsDeviceControl;
public static partial class WindowsRadio
{
private static readonly TimeSpan RadioCacheTtl = TimeSpan.FromSeconds(30);
private static readonly object RadioCacheLock = new();
private static (long Taken, Radio[] Radios)? _radioCache;
/// <summary>Reads the combined power state of every adapter of one kind.</summary>
/// <param name="kind">Which radio family to read.</param>
/// <returns>
/// The highest-priority state: On, Disabled, Off, Unknown, then Absent when enumeration is empty.
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="kind" /> is not a defined
/// <see cref="RadioKind" /> value.
/// </exception>
/// <remarks>
/// Adapter membership is cached for up to 30 seconds; power states are read on each call.
/// WinRT enumeration or state-read failures propagate to the caller.
/// </remarks>
public static Power GetPower(RadioKind kind)
{
ValidateRadioKind(kind);
var radios = GetRadios(kind, out var states);
return AggregatePower(radios.Select((radio, index) => MapPower(states?[index] ?? radio.State)));
}
/// <summary>Asks Windows whether this process may change radio power.</summary>
/// <returns>Whether radio control is permitted, and if not, why.</returns>
/// <remarks>
/// May request user consent. Blocks for the WinRT answer and propagates native failures.
/// <see cref="SetPower" /> calls this before accessing adapters.
/// </remarks>
public static Access RequestAccess()
{
return MapAccess(
Radio.RequestAccessAsync().WaitWinRt());
}
/// <summary>Turns every adapter of one kind on or off.</summary>
/// <param name="kind">Which radio family to change.</param>
/// <param name="on">True to turn the radios on, false to turn them off.</param>
/// <returns>
/// The access decision and each adapter's result. Denied access returns no adapter entries.
/// Once allowed, each adapter is attempted once; name-read or write exceptions become that
/// entry's HRESULT. An accepted write is not read back, and earlier writes are not rolled back.
/// </returns>
/// <exception cref="InvalidOperationException">The machine has no adapter of this kind.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="kind" /> is not a defined
/// <see cref="RadioKind" /> value.
/// </exception>
/// <remarks>Access-request and enumeration failures propagate; only per-adapter failures become results.</remarks>
public static RadioPowerResult SetPower(RadioKind kind, bool on)
{
ValidateRadioKind(kind);
var access = RequestAccess();
if (access != Access.Allowed)
{
return new RadioPowerResult(access, []);
}
var radios = GetRadios(kind, out _);
if (radios.Count == 0)
{
throw new InvalidOperationException("Windows reported no radio of the requested kind.");
}
var adapters = new RadioAdapterResult[radios.Count];
for (var index = 0; index < radios.Count; index++)
{
var name = string.Empty;
try
{
name = radios[index].Name ?? string.Empty;
var result = MapAccess(radios[index].SetStateAsync(on ? RadioState.On : RadioState.Off)
.WaitWinRt());
adapters[index] = new RadioAdapterResult(name, result, 0);
}
catch (Exception ex)
{
adapters[index] = new RadioAdapterResult(name, null, ex.HResult);
}
}
return new RadioPowerResult(CombineAdapterAccess(adapters), adapters);
}
/// <summary>
/// Folds adapter answers into one: allowed only when every adapter allowed, otherwise the
/// first refusal, or <see cref="Access.Unspecified" /> when an adapter failed without one.
/// </summary>
/// <param name="adapters">Adapter results in attempted order.</param>
/// <returns>The first refusal, otherwise Unspecified for an exception, otherwise Allowed (including empty input).</returns>
internal static Access CombineAdapterAccess(IReadOnlyList<RadioAdapterResult> adapters)
{
var failed = false;
foreach (var adapter in adapters)
{
if (adapter.Access is not { } answer)
{
failed = true;
}
else if (answer != Access.Allowed)
{
return answer;
}
}
return failed ? Access.Unspecified : Access.Allowed;
}
/// <summary>Reads the privacy consent recorded for a capability.</summary>
/// <param name="capability">
/// The capability name, as the privacy store spells it — for example
/// <c>location</c> or <c>radios</c>.
/// </param>
/// <returns>The user-scope and machine-scope consent values.</returns>
/// <exception cref="ArgumentException">
/// <paramref name="capability" /> is null, empty or white space, which would read the
/// consent store's root instead of a capability.
/// </exception>
/// <remarks>
/// Diagnostic only: the owning API remains the authority on what is permitted, and this can
/// disagree with it. Location access can block Wi-Fi scan-derived information; a WLAN
/// refusal is reported as a Win32Exception, including ERROR_ACCESS_DENIED on Windows 11
/// 24H2. An empty network list by itself does not establish that consent was denied.
/// </remarks>
public static (Consent User, Consent Machine) GetConsent(string capability)
{
ArgumentException.ThrowIfNullOrWhiteSpace(capability);
return (
ReadConsent(Registry.CurrentUser, capability),
ReadConsent(Registry.LocalMachine, capability));
}
/// <summary>The adapters of one kind, from a briefly cached enumeration.</summary>
/// <param name="kind">Which radio family to return.</param>
/// <param name="states">
/// When the cached list was reused, the state each returned adapter
/// reported while the cache was checked; null after a fresh enumeration.
/// </param>
private static IReadOnlyList<Radio> GetRadios(RadioKind kind, out RadioState[]? states)
{
Radio[]? all = null;
RadioState[]? observed = null;
lock (RadioCacheLock)
{
if (_radioCache is { } cached
&& Stopwatch.GetElapsedTime(cached.Taken) < RadioCacheTtl
&& TryReadStates(cached.Radios, out observed))
{
all = cached.Radios;
}
}
if (all is null)
{
// A stalled native enumeration must not block readers that can still use the cache.
all = Radio.GetRadiosAsync().WaitWinRt().ToArray();
lock (RadioCacheLock)
{
_radioCache = (Stopwatch.GetTimestamp(), all);
}
}
var expected = kind == RadioKind.WiFi
? Windows.Devices.Radios.RadioKind.WiFi
: Windows.Devices.Radios.RadioKind.Bluetooth;
List<Radio> radios = [];
List<RadioState>? kindStates = observed is null ? null : [];
for (var index = 0; index < all.Length; index++)
{
if (all[index].Kind == expected)
{
radios.Add(all[index]);
kindStates?.Add(observed![index]);
}
}
states = kindStates?.ToArray();
return radios;
}
/// <summary>Refuses an undefined radio kind. Every public member checks it before enumerating.</summary>
/// <param name="kind">Caller-provided adapter family.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="kind" /> is neither WiFi nor Bluetooth.</exception>
internal static void ValidateRadioKind(RadioKind kind)
{
if (kind is not RadioKind.WiFi and not RadioKind.Bluetooth)
{
throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unknown radio kind.");
}
}
/// <summary>Reads every cached adapter's state. One unreadable adapter means the cache is stale.</summary>
private static bool TryReadStates(Radio[] radios, out RadioState[]? states)
{
var read = new RadioState[radios.Length];
for (var index = 0; index < radios.Length; index++)
{
try
{
read[index] = radios[index].State;
}
catch
{
states = null;
return false;
}
}
states = read;
return true;
}
/// <summary>Reduces several adapters' power states to the one a caller should act on.</summary>
/// <param name="states">The individual adapter states.</param>
/// <returns>
/// The first present state in priority order: On, Disabled, Off, Unknown, Absent. Empty input
/// and undefined enum values contribute Absent. This method does not query Windows.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="states" /> is null.</exception>
public static Power AggregatePower(IEnumerable<Power> states)
{
ArgumentNullException.ThrowIfNull(states);
var power = Power.Absent;
foreach (var state in states)
{
power = Prefer(power, state);
}
return power;
}
/// <summary>
/// The state that represents both: On, then Disabled, Off and Unknown. Any other value
/// counts as no adapter.
/// </summary>
private static Power Prefer(Power current, Power candidate)
{
return Rank(candidate) < Rank(current) ? candidate : current;
}
private static int Rank(Power state)
{
return state switch
{
Power.On => 0,
Power.Disabled => 1,
Power.Off => 2,
Power.Unknown => 3,
_ => 4
};
}
private static Power MapPower(RadioState state)
{
return state switch
{
RadioState.On => Power.On,
RadioState.Off => Power.Off,
RadioState.Disabled => Power.Disabled,
_ => Power.Unknown
};
}
private static Access MapAccess(RadioAccessStatus status)
{
return status switch
{
RadioAccessStatus.Allowed => Access.Allowed,
RadioAccessStatus.DeniedByUser => Access.DeniedByUser,
RadioAccessStatus.DeniedBySystem => Access.DeniedBySystem,
_ => Access.Unspecified
};
}
private static Consent ReadConsent(RegistryKey root, string capability)
{
try
{
using var key = root.OpenSubKey(
$"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\CapabilityAccessManager\\ConsentStore\\{capability}",
false);
if (key is null)
{
return Consent.Unset;
}
return key.GetValue("Value") switch
{
string value when value.Trim().Equals("Allow", StringComparison.OrdinalIgnoreCase)
=> Consent.Allow,
string value when value.Trim().Equals("Deny", StringComparison.OrdinalIgnoreCase)
=> Consent.Deny,
string value when string.IsNullOrWhiteSpace(value) => Consent.Unset,
null => Consent.Unset,
_ => Consent.Unknown
};
}
catch
{
return Consent.Unknown;
}
}
}