diff --git a/.justfile b/.justfile
index cda2d8d..55ae22c 100644
--- a/.justfile
+++ b/.justfile
@@ -15,11 +15,15 @@ clean:
rm -rf "dist"
rm -rf "wwwroot"
-update:
- npm install
- dotnet restore ./server/server.csproj
+update: update-node update-dotnet
git submodule update --init --remote --recursive
+update-node:
+ npm install
+
+update-dotnet:
+ dotnet restore ./server/server.csproj
+
# 生成Restful API到网页客户端
gen-api:
npm run gen-api
diff --git a/components.d.ts b/components.d.ts
index dca5943..c80ebed 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -9,11 +9,8 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
Alert: typeof import('./src/components/Alert/Alert.vue')['default']
- AlertDemo: typeof import('./src/components/AlertDemo.vue')['default']
BaseBoard: typeof import('./src/components/equipments/BaseBoard.vue')['default']
BaseInputField: typeof import('./src/components/InputField/BaseInputField.vue')['default']
- Canvas: typeof import('./src/components/Canvas.vue')['default']
- ChannelConfig: typeof import('./src/components/LogicAnalyzer/ChannelConfig.vue')['default']
CollapsibleSection: typeof import('./src/components/CollapsibleSection.vue')['default']
ComponentSelector: typeof import('./src/components/LabCanvas/ComponentSelector.vue')['default']
DDR: typeof import('./src/components/equipments/DDR.vue')['default']
@@ -22,16 +19,10 @@ declare module 'vue' {
DiagramCanvas: typeof import('./src/components/LabCanvas/DiagramCanvas.vue')['default']
Dialog: typeof import('./src/components/Dialog.vue')['default']
ETH: typeof import('./src/components/equipments/ETH.vue')['default']
- FunctionBar: typeof import('./src/components/FunctionBar.vue')['default']
HDMI: typeof import('./src/components/equipments/HDMI.vue')['default']
IpInputField: typeof import('./src/components/InputField/IpInputField.vue')['default']
ItemList: typeof import('./src/components/LabCanvas/ItemList.vue')['default']
- LabCanvas: typeof import('./src/components/LabCanvasNew/LabCanvas.vue')['default']
- LabCanvasNew: typeof import('./src/components/LabCanvas/LabCanvasNew.vue')['default']
- LabComponentsDrawer: typeof import('./src/components/LabCanvasNew/LabComponentsDrawer.vue')['default']
- LabComponentsDrawerNew: typeof import('./src/components/LabCanvas/LabComponentsDrawerNew.vue')['default']
LogicalWaveFormDisplay: typeof import('./src/components/LogicAnalyzer/LogicalWaveFormDisplay.vue')['default']
- LoginCard: typeof import('./src/components/LoginCard.vue')['default']
MarkdownRenderer: typeof import('./src/components/MarkdownRenderer.vue')['default']
MechanicalButton: typeof import('./src/components/equipments/MechanicalButton.vue')['default']
MotherBoard: typeof import('./src/components/equipments/MotherBoard.vue')['default']
@@ -43,29 +34,15 @@ declare module 'vue' {
PortInputField: typeof import('./src/components/InputField/PortInputField.vue')['default']
PropertyEditor: typeof import('./src/components/PropertyEditor.vue')['default']
PropertyPanel: typeof import('./src/components/PropertyPanel.vue')['default']
- RekaSplitterGroup: typeof import('reka-ui')['SplitterGroup']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
- ScrollAreaCorner: typeof import('reka-ui')['ScrollAreaCorner']
- ScrollAreaRoot: typeof import('reka-ui')['ScrollAreaRoot']
- ScrollAreaScrollbar: typeof import('reka-ui')['ScrollAreaScrollbar']
- ScrollAreaThumb: typeof import('reka-ui')['ScrollAreaThumb']
- ScrollAreaViewport: typeof import('reka-ui')['ScrollAreaViewport']
SD: typeof import('./src/components/equipments/SD.vue')['default']
SevenSegmentDisplay: typeof import('./src/components/equipments/SevenSegmentDisplay.vue')['default']
SFP: typeof import('./src/components/equipments/SFP.vue')['default']
Sidebar: typeof import('./src/components/Sidebar.vue')['default']
SMA: typeof import('./src/components/equipments/SMA.vue')['default']
SMT_LED: typeof import('./src/components/equipments/SMT_LED.vue')['default']
- SplitterGroup: typeof import('reka-ui')['SplitterGroup']
- SplitterPanel: typeof import('reka-ui')['SplitterPanel']
- SplitterResizeHandle: typeof import('reka-ui')['SplitterResizeHandle']
Switch: typeof import('./src/components/equipments/Switch.vue')['default']
- TabsContent: typeof import('reka-ui')['TabsContent']
- TabsIndicator: typeof import('reka-ui')['TabsIndicator']
- TabsList: typeof import('reka-ui')['TabsList']
- TabsRoot: typeof import('reka-ui')['TabsRoot']
- TabsTrigger: typeof import('reka-ui')['TabsTrigger']
ThemeControlButton: typeof import('./src/components/ThemeControlButton.vue')['default']
ThemeControlToggle: typeof import('./src/components/ThemeControlToggle.vue')['default']
TriggerSettings: typeof import('./src/components/LogicAnalyzer/TriggerSettings.vue')['default']
diff --git a/flake.nix b/flake.nix
index b099c89..7f9b44b 100644
--- a/flake.nix
+++ b/flake.nix
@@ -23,6 +23,7 @@
sqls
sql-studio
zlib
+ bash
# Backend
(dotnetCorePackages.combinePackages [
dotnetCorePackages.sdk_9_0
diff --git a/server/src/ArpClient.cs b/server/src/ArpClient.cs
index 6a806df..6398670 100644
--- a/server/src/ArpClient.cs
+++ b/server/src/ArpClient.cs
@@ -7,6 +7,8 @@ using System.Text.RegularExpressions;
///
public static class Arp
{
+ private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+
///
/// 读取所有 ARP 记录
///
@@ -41,6 +43,28 @@ public static class Arp
return entries;
}
+ ///
+ /// 通过 Ping 动态更新指定 IP 的 ARP 记录
+ ///
+ /// 要更新的 IP 地址
+ /// 是否成功发送 Ping
+ public static async Task UpdateArpEntryByPingAsync(string ipAddress)
+ {
+ if (string.IsNullOrWhiteSpace(ipAddress))
+ throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
+
+ try
+ {
+ using var ping = new System.Net.NetworkInformation.Ping();
+ var reply = await ping.SendPingAsync(ipAddress, 100);
+ return reply.Status == System.Net.NetworkInformation.IPStatus.Success;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
///
/// 添加 ARP 记录
///
@@ -48,7 +72,7 @@ public static class Arp
/// MAC 地址
/// 网络接口名称(可选)
/// 是否成功
- public static async Task AddArpEntryAsync(string ipAddress, string macAddress, string interfaceName = null)
+ public static async Task AddArpEntryAsync(string ipAddress, string macAddress, string? interfaceName = null)
{
if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
@@ -74,7 +98,7 @@ public static class Arp
/// 要删除的 IP 地址
/// 网络接口名称(可选)
/// 是否成功
- public static async Task DeleteArpEntryAsync(string ipAddress, string interfaceName = null)
+ public static async Task DeleteArpEntryAsync(string ipAddress, string? interfaceName = null)
{
if (string.IsNullOrWhiteSpace(ipAddress))
throw new ArgumentException("IP 地址不能为空", nameof(ipAddress));
@@ -149,15 +173,13 @@ public static class Arp
///
private static string GetArpListCommand()
{
- return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
- ? "arp -a"
- : "arp -a";
+ return "arp -a";
}
///
/// 获取 ARP 添加命令
///
- private static string GetArpAddCommand(string ipAddress, string macAddress, string interfaceName)
+ private static string GetArpAddCommand(string ipAddress, string macAddress, string? interfaceName)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@@ -186,7 +208,7 @@ public static class Arp
///
/// 获取 ARP 删除命令
///
- private static string GetArpDeleteCommand(string ipAddress, string interfaceName)
+ private static string GetArpDeleteCommand(string ipAddress, string? interfaceName)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@@ -238,14 +260,7 @@ public static class Arp
///
private static string GetArpQueryCommand(string ipAddress)
{
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- return $"arp -a {ipAddress}";
- }
- else
- {
- return $"arp -n {ipAddress}";
- }
+ return $"arp -a {ipAddress}";
}
///
@@ -284,6 +299,8 @@ public static class Arp
};
}
+ logger.Debug($"Executing command: {processInfo.FileName} {processInfo.Arguments}");
+
using var process = new Process { StartInfo = processInfo };
process.Start();
@@ -292,6 +309,11 @@ public static class Arp
await process.WaitForExitAsync();
+ logger.Debug($"Command output: {output}");
+ if (!string.IsNullOrWhiteSpace(error))
+ logger.Debug($"Command error: {error}");
+ logger.Debug($"Command exit code: {process.ExitCode}");
+
return new CommandResult
{
IsSuccess = process.ExitCode == 0,
@@ -302,6 +324,7 @@ public static class Arp
}
catch (Exception ex)
{
+ logger.Error(ex, $"Command execution failed: {command}");
return new CommandResult
{
IsSuccess = false,
@@ -358,8 +381,8 @@ public static class Arp
///
private static ArpEntry? ParseUnixArpEntry(string line)
{
- // Unix/Linux arp -a 输出格式: hostname (ip) at mac [ether] on interface
- var pattern = @"(\S+)\s+\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([a-fA-F0-9:]{17})\s+\[(\w+)\]\s+on\s+(\S+)";
+ // Unix/Linux arp -a 输出格式: hostname (ip) at mac [ether] PERM on interface
+ var pattern = @"(\S+)\s+\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([a-fA-F0-9:]{17})\s+\[(\w+)\]\s+(\w+)\s+on\s+(\S+)";
var match = Regex.Match(line, pattern);
if (match.Success)
@@ -369,8 +392,8 @@ public static class Arp
Hostname = match.Groups[1].Value,
IpAddress = match.Groups[2].Value,
MacAddress = match.Groups[3].Value,
- Type = match.Groups[4].Value,
- Interface = match.Groups[5].Value
+ Type = match.Groups[5].Value,
+ Interface = match.Groups[6].Value
};
}
@@ -390,6 +413,26 @@ public static class Arp
return null;
}
+
+ ///
+ /// 判断当前进程是否具有管理员(或 root)权限
+ ///
+ /// 如果有管理员权限返回 true,否则返回 false
+ public static bool IsAdministrator()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // Windows: 检查当前用户是否为管理员
+ using var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
+ var principal = new System.Security.Principal.WindowsPrincipal(identity);
+ return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);
+ }
+ else
+ {
+ // Unix/Linux/macOS: 检查是否为 root 用户
+ return Environment.UserName == "root" || (Environment.GetEnvironmentVariable("USER") == "root");
+ }
+ }
}
///
@@ -397,12 +440,30 @@ public static class Arp
///
public class ArpEntry
{
+ ///
+ /// [TODO:description]
+ ///
public string Hostname { get; set; } = string.Empty;
+ ///
+ /// [TODO:description]
+ ///
public string IpAddress { get; set; } = string.Empty;
+ ///
+ /// [TODO:description]
+ ///
public string MacAddress { get; set; } = string.Empty;
+ ///
+ /// [TODO:description]
+ ///
public string Type { get; set; } = string.Empty;
+ ///
+ /// [TODO:description]
+ ///
public string Interface { get; set; } = string.Empty;
+ ///
+ /// [TODO:description]
+ ///
public override string ToString()
{
return $"{IpAddress} -> {MacAddress} ({Interface})";
@@ -414,8 +475,20 @@ public class ArpEntry
///
public class CommandResult
{
+ ///
+ /// [TODO:description]
+ ///
public bool IsSuccess { get; set; }
+ ///
+ /// [TODO:description]
+ ///
public string Output { get; set; } = string.Empty;
+ ///
+ /// [TODO:description]
+ ///
public string Error { get; set; } = string.Empty;
+ ///
+ /// [TODO:description]
+ ///
public int ExitCode { get; set; }
}
diff --git a/server/src/MsgBus.cs b/server/src/MsgBus.cs
index 573b074..c5dff1a 100644
--- a/server/src/MsgBus.cs
+++ b/server/src/MsgBus.cs
@@ -3,6 +3,8 @@
///
public static class MsgBus
{
+ private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+
private static readonly UDPServer udpServer = new UDPServer(1234, 12);
///
/// 获取UDP服务器
@@ -19,8 +21,13 @@ public static class MsgBus
/// 通信总线初始化
///
/// 无
- public static void Init()
+ public async static void Init()
{
+ if (!Arp.IsAdministrator())
+ {
+ logger.Error($"非管理员运行,ARP无法更新,请用管理员权限运行");
+ // throw new Exception($"非管理员运行,ARP无法更新,请用管理员权限运行");
+ }
udpServer.Start();
isRunning = true;
}
diff --git a/server/src/UdpServer.cs b/server/src/UdpServer.cs
index 70a9cc6..7459404 100644
--- a/server/src/UdpServer.cs
+++ b/server/src/UdpServer.cs
@@ -128,7 +128,7 @@ public class UDPServer
if (IsPortInUse(currentPort))
{
throw new ArgumentException(
- $"Port {currentPort} is already in use.",
+ $"端口{currentPort}已被占用,无法启动UDP Server",
nameof(port)
);
}