mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 05:00:07 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8608ee6272 | |||
| f762b932a2 | |||
| 173bfc40a9 | |||
| 609d9c47af | |||
| 5496d575c1 |
+209
@@ -0,0 +1,209 @@
|
||||
#nullable enable
|
||||
using AutopilotMonitor.Agent.V2.Core.Monitoring.Interop;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for the pure netsh-output parser of the WiFi fallback path. netsh labels follow
|
||||
/// the OS UI language — the parser must extract SSID/signal/radio/channel from the label
|
||||
/// variants of the languages commonly seen in enrollments, and degrade gracefully (fields
|
||||
/// absent, never throw) on unrecognized ones.
|
||||
/// </summary>
|
||||
public sealed class NetshWifiFallbackTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_english_output_extracts_all_fields()
|
||||
{
|
||||
const string output = @"
|
||||
There is 1 interface on the system:
|
||||
|
||||
Name : Wi-Fi
|
||||
Description : Intel(R) Wi-Fi 6 AX201 160MHz
|
||||
GUID : 754b6c07-5ae9-49d5-ba6c-4fbd6e38bbf6
|
||||
State : connected
|
||||
SSID : contoso-corp
|
||||
AP BSSID : 3c:37:12:53:c8:44
|
||||
Band : 5 GHz
|
||||
Channel : 116
|
||||
Radio type : 802.11ax
|
||||
Signal : 94%
|
||||
";
|
||||
var r = NetshWifiFallback.Parse(output);
|
||||
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal("contoso-corp", r!.Ssid);
|
||||
Assert.Equal(94, r.SignalPercent);
|
||||
Assert.Equal("802.11ax", r.RadioType);
|
||||
Assert.Equal(116, r.Channel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_german_output_extracts_all_fields()
|
||||
{
|
||||
const string output = @"
|
||||
Es ist 1 Schnittstelle auf dem System vorhanden:
|
||||
|
||||
Name : WLAN
|
||||
Beschreibung : Intel(R) Wi-Fi 6 AX201 160MHz
|
||||
Status : Verbunden
|
||||
SSID : fabrikam-wlan
|
||||
Kanal : 44
|
||||
Funktyp : 802.11ac
|
||||
Signal : 87%
|
||||
";
|
||||
var r = NetshWifiFallback.Parse(output);
|
||||
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal("fabrikam-wlan", r!.Ssid);
|
||||
Assert.Equal(87, r.SignalPercent);
|
||||
Assert.Equal("802.11ac", r.RadioType);
|
||||
Assert.Equal(44, r.Channel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_spanish_output_extracts_signal_and_channel()
|
||||
{
|
||||
const string output = @"
|
||||
Nombre : Wi-Fi
|
||||
SSID : contoso-es
|
||||
Canal : 6
|
||||
Tipo de radio : 802.11n
|
||||
Señal : 72%
|
||||
";
|
||||
var r = NetshWifiFallback.Parse(output);
|
||||
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal("contoso-es", r!.Ssid);
|
||||
Assert.Equal(72, r.SignalPercent);
|
||||
Assert.Equal("802.11n", r.RadioType);
|
||||
Assert.Equal(6, r.Channel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_chinese_simplified_output_extracts_all_fields()
|
||||
{
|
||||
const string output = @"
|
||||
名称 : WLAN
|
||||
SSID : contoso-cn
|
||||
信道 : 149
|
||||
无线电类型 : 802.11ac
|
||||
信号 : 68%
|
||||
";
|
||||
var r = NetshWifiFallback.Parse(output);
|
||||
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal("contoso-cn", r!.Ssid);
|
||||
Assert.Equal(68, r.SignalPercent);
|
||||
Assert.Equal("802.11ac", r.RadioType);
|
||||
Assert.Equal(149, r.Channel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_korean_output_extracts_all_fields()
|
||||
{
|
||||
const string output = @"
|
||||
이름 : Wi-Fi
|
||||
SSID : contoso-kr
|
||||
채널 : 36
|
||||
라디오 종류 : 802.11ax
|
||||
신호 : 91%
|
||||
";
|
||||
var r = NetshWifiFallback.Parse(output);
|
||||
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal("contoso-kr", r!.Ssid);
|
||||
Assert.Equal(91, r.SignalPercent);
|
||||
Assert.Equal("802.11ax", r.RadioType);
|
||||
Assert.Equal(36, r.Channel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_signal_with_space_before_percent_is_parsed()
|
||||
{
|
||||
var r = NetshWifiFallback.Parse(" Signal : 81 %\r\n SSID : x\r\n");
|
||||
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal(81, r!.SignalPercent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ap_bssid_does_not_overwrite_ssid()
|
||||
{
|
||||
const string output = @"
|
||||
SSID : contoso-corp
|
||||
AP BSSID : aa:bb:cc:dd:ee:ff
|
||||
";
|
||||
var r = NetshWifiFallback.Parse(output);
|
||||
|
||||
Assert.Equal("contoso-corp", r!.Ssid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_finnish_output_extracts_all_fields()
|
||||
{
|
||||
const string output = @"
|
||||
Nimi : Wi-Fi
|
||||
SSID : contoso-fi
|
||||
Kanava : 100
|
||||
Radiotyyppi : 802.11ax
|
||||
Signaali : 79%
|
||||
";
|
||||
var r = NetshWifiFallback.Parse(output);
|
||||
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal("contoso-fi", r!.Ssid);
|
||||
Assert.Equal(79, r.SignalPercent);
|
||||
Assert.Equal("802.11ax", r.RadioType);
|
||||
Assert.Equal(100, r.Channel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_indonesian_output_extracts_all_fields()
|
||||
{
|
||||
const string output = @"
|
||||
Nama : Wi-Fi
|
||||
SSID : contoso-id
|
||||
Saluran : 11
|
||||
Jenis radio : 802.11n
|
||||
Sinyal : 64%
|
||||
";
|
||||
var r = NetshWifiFallback.Parse(output);
|
||||
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal("contoso-id", r!.Ssid);
|
||||
Assert.Equal(64, r.SignalPercent);
|
||||
Assert.Equal("802.11n", r.RadioType);
|
||||
Assert.Equal(11, r.Channel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_unknown_language_without_ssid_returns_null()
|
||||
{
|
||||
// Greek is not in the enrollment-geography label set -> nothing to report.
|
||||
var r = NetshWifiFallback.Parse(" Κατάσταση : συνδεδεμένο\r\n Σήμα : 90%\r\n");
|
||||
|
||||
Assert.Null(r);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_unknown_language_still_yields_ssid()
|
||||
{
|
||||
// SSID is untranslated in all locales — partial result beats none.
|
||||
var r = NetshWifiFallback.Parse(" SSID : contoso-gr\r\n Σήμα : 90%\r\n");
|
||||
|
||||
Assert.NotNull(r);
|
||||
Assert.Equal("contoso-gr", r!.Ssid);
|
||||
Assert.Null(r.SignalPercent);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData("Der WLAN-AutoKonfig-Dienst (wlansvc) wird nicht ausgeführt.")]
|
||||
public void Parse_empty_or_error_output_returns_null(string? output)
|
||||
{
|
||||
Assert.Null(NetshWifiFallback.Parse(output));
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -22,7 +22,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Interop
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
Assert.InRange(result.SignalPercent, 0, 100);
|
||||
Assert.True(result.SignalPercent.HasValue, "native API path must always carry signal quality");
|
||||
Assert.InRange(result.SignalPercent.Value, 0, 100);
|
||||
if (result.Channel.HasValue)
|
||||
Assert.True(result.Channel.Value > 0, "channel must be positive when present");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using AutopilotMonitor.Agent.V2.Core.Security;
|
||||
|
||||
namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Fallback WiFi reader via <c>netsh wlan show interfaces</c> output parsing, used ONLY
|
||||
/// when <see cref="WifiInfoReader"/> (native WLAN API) yields nothing on a WiFi-connected
|
||||
/// device. Field evidence 2026-07-28: the WLAN API can return nothing in the OOBE/SYSTEM
|
||||
/// context on builds where inbox netsh still reports the connection — until that is fully
|
||||
/// root-caused (suspect: Wi-Fi location gating, 24H2+), netsh stays as the safety net.
|
||||
/// <para>
|
||||
/// netsh labels follow the OS UI language, so the parser matches the label variants of
|
||||
/// every language implied by the enrollment geography seen to date (see the comment on
|
||||
/// the key arrays for the country→language derivation). Unknown-language labels fall
|
||||
/// through harmlessly — the affected field is simply absent, same as before. "SSID"
|
||||
/// itself is untranslated in all known locales.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class NetshWifiFallback
|
||||
{
|
||||
private const int NetshTimeoutMs = 5000;
|
||||
|
||||
// Label variants per field, by OS UI language. Matched case-insensitively against the
|
||||
// full text left of the colon, so acronym-suffixed keys ("AP BSSID") never collide.
|
||||
// Language set derived from enrollment geography (get_geographic_metrics, 365d):
|
||||
// US/GB/AU/CA/IE/SG/IN/ZA/NG/PH en · DE/AT/CH de · BE/NL nl · BE/FR/CH fr · DK da ·
|
||||
// SE sv · PL pl · TR tr · CN zh-CN · HK zh-TW · ES/MX/PE/PY es · CZ cs · SK sk ·
|
||||
// IT/CH it · ID id · MY ms · FI fi · LT lt · LV lv · HU hu · BR/PT pt · JP ja ·
|
||||
// KR ko · TH th · VN vi · RO/MD ro · BG bg · BA/HR hr · AE/SA/EG/JO ar ·
|
||||
// MN/GE/AZ usually en/ru images → ru.
|
||||
private static readonly string[] SignalKeys =
|
||||
{
|
||||
"Signal", // en, de, fr, sv, da, nb, hr
|
||||
"Señal", // es
|
||||
"Sinal", // pt
|
||||
"Segnale", // it
|
||||
"Signaal", // nl
|
||||
"Sygnał", // pl
|
||||
"Signál", // cs, sk
|
||||
"Sinyal", // tr, id
|
||||
"Isyarat", // ms
|
||||
"Signaali", // fi
|
||||
"Signalas", // lt
|
||||
"Signāls", // lv
|
||||
"Jel", // hu
|
||||
"Semnal", // ro
|
||||
"Tín hiệu", // vi
|
||||
"สัญญาณ", // th
|
||||
"الإشارة", // ar
|
||||
"إشارة", // ar (variant without article)
|
||||
"Сигнал", // ru, bg
|
||||
"シグナル", // ja
|
||||
"信号", // zh-CN
|
||||
"訊號", // zh-TW
|
||||
"신호", // ko
|
||||
};
|
||||
|
||||
private static readonly string[] RadioTypeKeys =
|
||||
{
|
||||
"Radio type", // en
|
||||
"Funktyp", // de
|
||||
"Type de radio", // fr
|
||||
"Tipo de radio", // es
|
||||
"Tipo de rádio", // pt
|
||||
"Tipo di radio", // it
|
||||
"Radiotype", // nl, da
|
||||
"Radiotyp", // sv
|
||||
"Typ radia", // pl
|
||||
"Typ rádia", // cs, sk
|
||||
"Radyo türü", // tr
|
||||
"Jenis radio", // id, ms
|
||||
"Radiotyyppi", // fi
|
||||
"Radijo tipas", // lt
|
||||
"Radio tips", // lv
|
||||
"Rádió típusa", // hu
|
||||
"Tip radio", // ro
|
||||
"Vrsta radija", // hr, bs
|
||||
"Loại radio", // vi
|
||||
"ชนิดวิทยุ", // th
|
||||
"نوع الراديو", // ar
|
||||
"Тип радио", // ru
|
||||
"Тип на радиото", // bg
|
||||
"無線の種類", // ja
|
||||
"无线电类型", // zh-CN
|
||||
"無線電類型", // zh-TW
|
||||
"라디오 종류", // ko
|
||||
"무선 종류", // ko (variant)
|
||||
};
|
||||
|
||||
private static readonly string[] ChannelKeys =
|
||||
{
|
||||
"Channel", // en
|
||||
"Kanal", // de, sv, da, nb, tr, hr
|
||||
"Canal", // fr, es, pt, ro
|
||||
"Canale", // it
|
||||
"Kanaal", // nl
|
||||
"Kanał", // pl
|
||||
"Kanál", // cs, sk
|
||||
"Saluran", // id, ms
|
||||
"Kanava", // fi
|
||||
"Kanalas", // lt
|
||||
"Kanāls", // lv
|
||||
"Csatorna", // hu
|
||||
"Kênh", // vi
|
||||
"ช่องสัญญาณ", // th
|
||||
"ช่อง", // th (variant)
|
||||
"القناة", // ar
|
||||
"قناة", // ar (variant without article)
|
||||
"Канал", // ru, bg
|
||||
"チャネル", // ja
|
||||
"信道", // zh-CN
|
||||
"通道", // zh-TW / zh-CN (variant)
|
||||
"채널", // ko
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Runs <c>netsh wlan show interfaces</c> and parses its output. Returns a
|
||||
/// <see cref="WifiConnectionInfo"/> with the fields that could be parsed, or null when
|
||||
/// nothing was found (no WLAN, netsh failure, timeout, or unrecognized localized
|
||||
/// labels). Never throws.
|
||||
/// </summary>
|
||||
internal static WifiConnectionInfo TryRead()
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = SystemPaths.Netsh,
|
||||
Arguments = "wlan show interfaces",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
string output;
|
||||
using (var process = Process.Start(psi))
|
||||
{
|
||||
output = process.StandardOutput.ReadToEnd();
|
||||
if (!process.WaitForExit(NetshTimeoutMs))
|
||||
{
|
||||
try { process.Kill(); } catch { }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return Parse(output);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure parser over the netsh output text. Internal for direct unit-testing via
|
||||
/// InternalsVisibleTo (TryRead itself spawns a process and stays untested).
|
||||
/// </summary>
|
||||
internal static WifiConnectionInfo Parse(string output)
|
||||
{
|
||||
if (string.IsNullOrEmpty(output))
|
||||
return null;
|
||||
|
||||
string ssid = null, radioType = null;
|
||||
int? signal = null, channel = null;
|
||||
|
||||
foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
var colonIndex = trimmed.IndexOf(':');
|
||||
if (colonIndex < 0) continue;
|
||||
|
||||
var key = trimmed.Substring(0, colonIndex).Trim();
|
||||
var value = trimmed.Substring(colonIndex + 1).Trim();
|
||||
|
||||
if (key.Equals("SSID", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ssid = value;
|
||||
}
|
||||
else if (MatchesAny(key, SignalKeys))
|
||||
{
|
||||
// Value formats seen: "94%", "94% " (trailing space), "94 %".
|
||||
if (int.TryParse(value.TrimEnd('%', ' '), out var sig))
|
||||
signal = sig;
|
||||
}
|
||||
else if (MatchesAny(key, RadioTypeKeys))
|
||||
{
|
||||
radioType = value;
|
||||
}
|
||||
else if (MatchesAny(key, ChannelKeys))
|
||||
{
|
||||
if (int.TryParse(value, out var ch))
|
||||
channel = ch;
|
||||
}
|
||||
}
|
||||
|
||||
if (ssid == null && signal == null && radioType == null && channel == null)
|
||||
return null;
|
||||
|
||||
return new WifiConnectionInfo
|
||||
{
|
||||
Ssid = ssid,
|
||||
SignalPercent = signal,
|
||||
RadioType = radioType,
|
||||
Channel = channel,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool MatchesAny(string key, string[] candidates)
|
||||
{
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (key.Equals(candidate, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
|
||||
/// <summary>SSID of the connected network; null when the AP broadcasts an empty SSID.</summary>
|
||||
public string Ssid { get; internal set; }
|
||||
|
||||
/// <summary>Signal quality 0–100 (same semantics as the netsh "Signal" percentage).</summary>
|
||||
public int SignalPercent { get; internal set; }
|
||||
/// <summary>
|
||||
/// Signal quality 0–100 (same semantics as the netsh "Signal" percentage). Always set
|
||||
/// on the native WLAN API path; null only when the netsh fallback could not parse it.
|
||||
/// </summary>
|
||||
public int? SignalPercent { get; internal set; }
|
||||
|
||||
/// <summary>Human-readable PHY generation ("802.11ax" …); null for exotic/unknown PHYs.</summary>
|
||||
public string RadioType { get; internal set; }
|
||||
@@ -37,20 +40,45 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
|
||||
/// wins. Returns null when no interface has an active connection.
|
||||
/// </summary>
|
||||
public static WifiConnectionInfo TryGetCurrentConnection(Guid? preferredInterfaceId = null)
|
||||
=> TryGetCurrentConnection(preferredInterfaceId, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="TryGetCurrentConnection(Guid?)"/> but reports WHY the read
|
||||
/// yielded nothing via <paramref name="diagnostics"/> (native return codes per step).
|
||||
/// The WLAN API can fail in ways that are invisible from the outside (e.g. per-process
|
||||
/// access gating on newer Windows builds) — callers log this at Debug so field issues
|
||||
/// are explainable from the agent log instead of a silent missing event.
|
||||
/// </summary>
|
||||
public static WifiConnectionInfo TryGetCurrentConnection(Guid? preferredInterfaceId, out string diagnostics)
|
||||
{
|
||||
var diag = new StringBuilder();
|
||||
var clientHandle = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
if (WlanOpenHandle(WLAN_CLIENT_VERSION, IntPtr.Zero, out _, out clientHandle) != ERROR_SUCCESS)
|
||||
var rc = WlanOpenHandle(WLAN_CLIENT_VERSION, IntPtr.Zero, out _, out clientHandle);
|
||||
if (rc != ERROR_SUCCESS)
|
||||
{
|
||||
diagnostics = $"WlanOpenHandle rc={rc}";
|
||||
return null;
|
||||
}
|
||||
|
||||
var interfaceList = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
if (WlanEnumInterfaces(clientHandle, IntPtr.Zero, out interfaceList) != ERROR_SUCCESS)
|
||||
rc = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out interfaceList);
|
||||
if (rc != ERROR_SUCCESS)
|
||||
{
|
||||
diagnostics = $"WlanEnumInterfaces rc={rc}";
|
||||
return null;
|
||||
}
|
||||
|
||||
var count = (uint)Marshal.ReadInt32(interfaceList);
|
||||
if (count == 0)
|
||||
{
|
||||
diagnostics = "no WLAN interfaces";
|
||||
return null;
|
||||
}
|
||||
|
||||
var infoSize = Marshal.SizeOf(typeof(WLAN_INTERFACE_INFO));
|
||||
// List header: DWORD dwNumberOfItems + DWORD dwIndex, then the info array.
|
||||
var firstEntry = new IntPtr(interfaceList.ToInt64() + 8);
|
||||
@@ -61,17 +89,21 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
|
||||
var entryPtr = new IntPtr(firstEntry.ToInt64() + i * infoSize);
|
||||
var info = (WLAN_INTERFACE_INFO)Marshal.PtrToStructure(entryPtr, typeof(WLAN_INTERFACE_INFO));
|
||||
|
||||
var connection = QueryCurrentConnection(clientHandle, info.InterfaceGuid);
|
||||
var connection = QueryCurrentConnection(clientHandle, info.InterfaceGuid, diag);
|
||||
if (connection == null)
|
||||
continue;
|
||||
|
||||
if (preferredInterfaceId.HasValue && info.InterfaceGuid == preferredInterfaceId.Value)
|
||||
{
|
||||
diagnostics = null;
|
||||
return connection;
|
||||
}
|
||||
|
||||
if (fallback == null)
|
||||
fallback = connection;
|
||||
}
|
||||
|
||||
diagnostics = fallback == null ? diag.ToString() : null;
|
||||
return fallback;
|
||||
}
|
||||
finally
|
||||
@@ -80,8 +112,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
|
||||
WlanFreeMemory(interfaceList);
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
diagnostics = $"exception {ex.GetType().Name}: {ex.Message}";
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
@@ -91,21 +124,28 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
|
||||
}
|
||||
}
|
||||
|
||||
private static WifiConnectionInfo QueryCurrentConnection(IntPtr clientHandle, Guid interfaceGuid)
|
||||
private static WifiConnectionInfo QueryCurrentConnection(IntPtr clientHandle, Guid interfaceGuid, StringBuilder diag)
|
||||
{
|
||||
var dataPtr = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
if (WlanQueryInterface(clientHandle, ref interfaceGuid,
|
||||
var rc = WlanQueryInterface(clientHandle, ref interfaceGuid,
|
||||
WLAN_INTF_OPCODE.wlan_intf_opcode_current_connection,
|
||||
IntPtr.Zero, out _, out dataPtr, IntPtr.Zero) != ERROR_SUCCESS)
|
||||
IntPtr.Zero, out _, out dataPtr, IntPtr.Zero);
|
||||
if (rc != ERROR_SUCCESS)
|
||||
{
|
||||
diag.Append($"[{interfaceGuid:D}] query(current_connection) rc={rc}; ");
|
||||
return null;
|
||||
}
|
||||
|
||||
var attributes = (WLAN_CONNECTION_ATTRIBUTES)Marshal.PtrToStructure(
|
||||
dataPtr, typeof(WLAN_CONNECTION_ATTRIBUTES));
|
||||
|
||||
if (attributes.isState != WLAN_INTERFACE_STATE.wlan_interface_state_connected)
|
||||
{
|
||||
diag.Append($"[{interfaceGuid:D}] state={attributes.isState}; ");
|
||||
return null;
|
||||
}
|
||||
|
||||
var assoc = attributes.wlanAssociationAttributes;
|
||||
return new WifiConnectionInfo
|
||||
|
||||
+13
-9
@@ -909,24 +909,28 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo
|
||||
{
|
||||
try
|
||||
{
|
||||
var wifi = WifiInfoReader.TryGetCurrentConnection(interfaceGuid);
|
||||
var wifi = WifiInfoReader.TryGetCurrentConnection(interfaceGuid, out var wifiDiag);
|
||||
if (wifi == null)
|
||||
return;
|
||||
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["wifiSignalPercent"] = wifi.SignalPercent
|
||||
};
|
||||
_logger.Debug($"EnrollmentTracker: native WLAN API returned no WiFi info ({wifiDiag ?? "no connected WLAN interface"}) — trying netsh fallback");
|
||||
wifi = NetshWifiFallback.TryRead();
|
||||
if (wifi == null)
|
||||
return;
|
||||
}
|
||||
|
||||
var data = new Dictionary<string, object>();
|
||||
if (wifi.Ssid != null)
|
||||
data["wifiSsid"] = wifi.Ssid;
|
||||
if (wifi.SignalPercent.HasValue)
|
||||
data["wifiSignalPercent"] = wifi.SignalPercent.Value;
|
||||
if (wifi.RadioType != null)
|
||||
data["wifiRadioType"] = wifi.RadioType;
|
||||
if (wifi.Channel.HasValue)
|
||||
data["wifiChannel"] = wifi.Channel.Value;
|
||||
|
||||
var message = wifi.Ssid != null
|
||||
? $"WiFi: {wifi.Ssid}, Signal: {wifi.SignalPercent}%"
|
||||
: $"WiFi signal info, Signal: {wifi.SignalPercent}%";
|
||||
var message = wifi.Ssid != null ? $"WiFi: {wifi.Ssid}" : "WiFi signal info";
|
||||
if (wifi.SignalPercent.HasValue)
|
||||
message += $", Signal: {wifi.SignalPercent.Value}%";
|
||||
if (wifi.RadioType != null)
|
||||
message += $" ({wifi.RadioType})";
|
||||
|
||||
|
||||
+7
-1
@@ -398,7 +398,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Periodic
|
||||
{
|
||||
try
|
||||
{
|
||||
var wifi = WifiInfoReader.TryGetCurrentConnection(interfaceGuid);
|
||||
var wifi = WifiInfoReader.TryGetCurrentConnection(interfaceGuid, out var wifiDiag);
|
||||
if (wifi == null)
|
||||
{
|
||||
_logger.Debug($"NetworkChangeDetector: native WLAN API returned no WiFi info ({wifiDiag ?? "no connected WLAN interface"}) — trying netsh fallback");
|
||||
wifi = NetshWifiFallback.TryRead();
|
||||
}
|
||||
|
||||
if (wifi != null)
|
||||
return (wifi.Ssid, wifi.SignalPercent, wifi.RadioType);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
|
||||
/// <summary>C:\Windows\System32\tzutil.exe</summary>
|
||||
public static readonly string TzUtil = Path.Combine(System32, "tzutil.exe");
|
||||
|
||||
/// <summary>C:\Windows\System32\netsh.exe</summary>
|
||||
public static readonly string Netsh = Path.Combine(System32, "netsh.exe");
|
||||
|
||||
/// <summary>C:\Windows\System32\schtasks.exe</summary>
|
||||
public static readonly string Schtasks = Path.Combine(System32, "schtasks.exe");
|
||||
|
||||
|
||||
@@ -885,12 +885,16 @@ function SessionCell({
|
||||
</td>
|
||||
);
|
||||
|
||||
case "started":
|
||||
case "started": {
|
||||
// Two-line date/time keeps the column narrow so more columns fit on one page.
|
||||
const started = new Date(session.startedAt);
|
||||
return (
|
||||
<td className="px-3 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(session.startedAt).toLocaleString()}
|
||||
<div>{started.toLocaleDateString()}</div>
|
||||
<div className="text-xs text-gray-400">{started.toLocaleTimeString()}</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
case "country":
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user