简介fog-client 是一款基于 C# 开发的跨平台计算机远程管理客户端面向系统管理员与 DevOps 工程师解决多操作系统Windows/Linux/macOS终端统一纳管难题支持自动登出、能源管理、AD/Samba/OD 目录集成、打印机配置及任务重启等核心运维能力。资源包共304个文件以121个C#源码文件.cs为主体辅以47个本地化资源.resx、22个依赖库.dll、17个配置文件.config及11个项目定义.csproj完整覆盖客户端构建、安装部署含WIX .wxs、MSI 打包脚本与跨平台适配.plist、.sh、.systemd 等压缩包大小为7.7MB。目前已有169人学习下载提供可编译的全量工程代码、证书签名脚本SignCode.cmd、安装界面资源.bmp/.ico/.icns及跨平台服务组件.dylib/.daemon/.agent便于开发者快速理解FOG架构、定制化二次开发或部署私有化终端管控体系。1. fog-client不是远程控制软件而是一套可嵌入、可裁剪、可离线运行的跨平台计算机管理客户端框架你可能刚在内网服务器上部署完 FOG Project 服务端正准备给几十台 Windows 10/11 终端装客户端——结果发现官方 Windows 客户端不支持 WinPE 环境下的预启动管理Linux 客户端又得手动编译、依赖混乱macOS 更是直接缺席。这时候 fog-client 就不是“另一个客户端”而是你手里唯一能统一调度三端设备、且允许你把资产扫描、策略下发、日志上报、甚至自定义脚本执行逻辑全部塞进一个 .NET Core 进程里的轻量级管理代理。它用 C# 编写基于 .NET 6 跨平台运行时不依赖 Windows Management InstrumentationWMI或 PowerShell也不走 WinRM 或 SSH 隧道而是直连 FOG 服务端 API用标准 HTTP JSON 协议通信。适合运维工程师做批量终端纳管、IT 支持人员做现场快速诊断、系统集成商做 OEM 嵌入式定制——尤其当你需要在无管理员权限、无网络策略白名单、甚至断网但需本地缓存指令的场景下让终端仍能响应基础管理动作时fog-client 的设计哲学才真正显形。它不是“一键安装即用”的傻瓜工具而是一个可编译、可调试、可打 patch 的源码级客户端框架。你拿到的不是 setup.exe而是一份结构清晰的 C# 解决方案.sln含核心通信模块、硬件信息采集器、任务调度器、日志缓冲区和插件扩展点。这意味着你能删掉不需要的 BIOS 版本读取逻辑替换成你自己的 TPM2.0 状态校验能把默认的 30 秒心跳间隔改成 5 秒以适配高频率策略轮询甚至可以把整个 HTTP 客户端替换成带证书双向认证的 HttpClientFactory 实例。这不是“客户端下载包”这是你终端管理能力的代码接口层。2. 编译与部署从源码到可执行文件的完整链路含 .NET SDK 版本锁定与平台标识配置2.1 源码结构解析看清 fog-client 的四个核心项目与职责边界fog-client 仓库通常包含以下四个关键项目以典型 v2.4.0 分支为例项目名类型主要职责是否必须编译FogClient.Core.NET Standard 2.1 类库定义所有模型Task、Host、Setting、HTTP 接口契约、通用工具类如 MAC 地址解析、UUID 生成是FogClient.Platform.NET 6.0 类库平台抽象层Windows/Linux/macOS 各自的硬件信息采集实现CPU 型号、内存条数、磁盘型号、服务注册方式Windows Service / systemd / launchd是按目标平台选编FogClient.Client.NET 6.0 控制台应用主入口初始化配置、启动心跳、监听任务队列、触发插件执行是最终输出可执行文件FogClient.Plugin.Sample.NET 6.0 类库示例插件演示如何实现IPlugin接口完成“检查磁盘剩余空间并上报”逻辑否仅参考提示FogClient.Platform是跨平台能力的关键。它不使用RuntimeInformation.IsOSPlatform()做简单分支判断而是通过IHardwareInfoProvider接口注入具体实现——Windows 下调用 WMI 查询Win32_ComputerSystemProductLinux 下读取/sys/class/dmi/id/product_name和/proc/cpuinfomacOS 下执行system_profiler SPHardwareDataType -xml。这种设计让你能在不改主逻辑的前提下替换某平台的采集逻辑。2.2 编译前必设.NET SDK 版本、目标运行时与 RIDRuntime Identifier三要素fog-client 明确要求 .NET SDK 6.0.400 或更高版本注意不是 .NET 6.0.x 运行时而是 SDK。低版本会报错The target platform identifier win-x64 was not recognized。验证命令dotnet --version # 输出应为 6.0.400 或 6.0.402 等非 6.0.100编译命令必须显式指定--runtime和--self-contained参数否则生成的二进制无法脱离 .NET 运行时独立部署# 编译 Windows x64 版本含运行时约 85MB dotnet publish FogClient.Client.csproj -c Release -r win-x64 --self-contained true -o ./publish/win-x64 # 编译 Ubuntu 22.04 x64 版本需提前安装 libicu-dev, libssl-dev dotnet publish FogClient.Client.csproj -c Release -r ubuntu.22.04-x64 --self-contained true -o ./publish/ubuntu22-x64 # 编译 macOS ARM64M1/M2 芯片 dotnet publish FogClient.Client.csproj -c Release -r osx-arm64 --self-contained true -o ./publish/osx-arm64注意-r参数必须严格匹配目标系统。Ubuntu 20.04 用ubuntu.20.04-x64CentOS 7 必须用centos.7-x64因 glibc 版本差异。若填错 RID运行时会报Failed to load library libhostfxr.so——这不是缺失运行时而是 libc 兼容性失败。2.3 配置文件生成appsettings.json的最小必要字段与加密敏感项处理编译后需在publish/xxx/目录下创建appsettings.json。不要直接修改源码中的appsettings.Development.json生产环境必须用独立配置文件。最小可用配置如下{ FogServer: { BaseUrl: https://fog-server.internal/api/, ApiKey: your-api-key-here, VerifySsl: false, TimeoutSeconds: 30 }, Client: { Hostname: auto, MacAddress: auto, PollIntervalSeconds: 60, MaxLogSizeMB: 5 } }BaseUrl必须以/api/结尾否则任务下发接口 404ApiKeyFOG 服务端生成的 API 密钥非 Web 登录密码建议用dotnet user-secrets加密存储见 2.4 节VerifySsl: 内网自签证书时设为false否则启动即报 SSL handshake failedHostname/MacAddress设为auto时客户端自动读取系统主机名和第一块有 IP 的网卡 MAC不推荐设为固定字符串否则多机部署会冲突。2.4 生产环境加固API 密钥加密与服务化注册Windows/Linux 双路径直接明文写ApiKey是重大安全隐患。正确做法是使用 .NET 的用户机密User Secrets机制# 进入 FogClient.Client 项目目录 cd FogClient.Client # 初始化机密仅首次 dotnet user-secrets init # 设置 API 密钥自动加密存入 %APPDATA%\Microsoft\UserSecrets\... dotnet user-secrets set FogServer:ApiKey a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8 # 编译时自动读取机密需在 Program.cs 中启用 // 在 CreateHostBuilder 里添加 .ConfigureUserSecretsProgram()服务化注册命令# WindowsPowerShell 管理员模式 New-Service -Name FOGClient -BinaryPathName C:\fog-client\FogClient.Client.exe --service -StartupType Automatic Start-Service FOGClient # Ubuntusystemd sudo cp ./publish/ubuntu22-x64/FogClient.Client /opt/fog-client/ sudo tee /etc/systemd/system/fog-client.service EOF [Unit] DescriptionFOG Client Service Afternetwork.target [Service] Typesimple Userfoguser WorkingDirectory/opt/fog-client ExecStart/opt/fog-client/FogClient.Client --service Restarton-failure RestartSec10 [Install] WantedBymulti-user.target EOF sudo systemctl daemon-reload sudo systemctl enable fog-client sudo systemctl start fog-client注意--service参数是 fog-client 内置的服务模式开关它会禁用控制台输出、重定向日志到文件并捕获 SIGTERM 信号优雅退出。没有这个参数systemd 会认为进程立即退出而反复重启。3. 通信协议与任务执行HTTP API 对接细节与自定义插件开发实战3.1 心跳与任务拉取GET /tasks/{mac} 与 POST /tasks/{mac}/status 的完整交互流程fog-client 启动后首件事是向 FOG 服务端发送设备注册请求POST/api/host/携带hostname、mac、ip、osname等字段。成功后进入循环心跳每PollIntervalSeconds秒发起 GET 请求GET /api/tasks/00:11:22:33:44:55 HTTP/1.1 Host: fog-server.internal Authorization: Bearer your-api-key服务端返回 JSON 数组每个任务含id,type,parameters,createdTime[ { id: 12345, type: inventory, parameters: {includeDiskUsage: true}, createdTime: 2024-05-20T08:30:00Z } ]客户端执行任务后必须立即 POST 状态更新POST /api/tasks/00:11:22:33:44:55/status HTTP/1.1 Content-Type: application/json Authorization: Bearer your-api-key { taskId: 12345, status: completed, output: {\cpu\:\Intel(R) Core(TM) i7-8700K\,\ramGB\:32}, error: }关键逻辑output字段必须是合法 JSON 字符串不是对象否则 FOG 服务端解析失败任务状态卡在running。我曾因output: { disk: 120 }少了外层引号导致 200 台终端任务堆积排查耗时 3 小时。3.2 插件开发规范实现 IPlugin 接口的三个强制约定与生命周期钩子所有插件必须继承FogClient.Core.Plugins.IPlugin接口且满足命名规则类名必须以Plugin结尾如DiskSpacePlugin程序集名必须含.Plugin如FogClient.Plugin.DiskSpace.dll构造函数必须接受IConfiguration和ILogger两个参数用于读取插件专属配置和记录日志ExecuteAsync 方法返回TaskPluginResult其中PluginResult包含StatusSuccess/Failed、Outputstring、Errorstring。一个真实可用的磁盘空间插件示例using FogClient.Core.Plugins; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; public class DiskSpacePlugin : IPlugin { private readonly IConfiguration _config; private readonly ILogger _logger; public DiskSpacePlugin(IConfiguration config, ILogger logger) { _config config; _logger logger; } public async TaskPluginResult ExecuteAsync(Dictionarystring, string parameters) { try { var drive parameters.GetValueOrDefault(drive, C:); var info new DriveInfo(drive); var freeGB Math.Round(info.AvailableFreeSpace / (1024.0 * 1024.0 * 1024.0), 2); var totalGB Math.Round(info.TotalSize / (1024.0 * 1024.0 * 1024.0), 2); var output JsonSerializer.Serialize(new { drive, freeGB, totalGB }); _logger.LogInformation(Disk check completed: {Drive} has {Free}GB free, drive, freeGB); return new PluginResult { Status PluginStatus.Success, Output output }; } catch (Exception ex) { _logger.LogError(ex, Disk check failed for {Drive}, parameters.GetValueOrDefault(drive, C:)); return new PluginResult { Status PluginStatus.Failed, Error ex.Message }; } } }注意插件 DLL 必须放在客户端可执行文件同目录的Plugins/子目录下如./FogClient.Client.exe→./Plugins/DiskSpacePlugin.dll。fog-client 启动时自动扫描该目录并反射加载无需注册表或配置文件声明。3.3 自定义任务类型注册在 FOG 服务端新增 task_type 并关联插件名称仅写好插件还不够FOG 服务端必须知道“当 typediskspace 时该调用哪个插件”。操作步骤登录 FOG Web UI →Admin Settings→Task Types→Add Task Type填写Name:Disk Space CheckType:diskspace必须小写且与插件类名无关Description:Check free space on specified drivePlugin Name:DiskSpacePlugin必须与 DLL 中的类名完全一致含大小写保存后在主机页面点击Schedule Tasks→ 选择新类型 → 设置参数{drive:D:}。血泪经验Plugin Name字段填错一个字母FOG 服务端日志里只显示Plugin not found没有任何堆栈。正确做法是在客户端日志中搜索Loading plugin确认是否成功加载 DLL再查服务端fog-error.log看是否有Could not resolve plugin记录。4. 避坑指南五个高频翻车点与对应解法含日志定位与参数修正4.1 现象客户端启动后立即退出Windows 事件查看器报“Application Error”原因appsettings.json中BaseUrl缺少末尾斜杠/api/导致 HTTP 客户端拼接 URL 为https://server/api/tasks/xx:xx:xx正确 vshttps://server/apitasks/xx:xx:xx错误404 后客户端静默退出。解决用浏览器访问https://your-fog-server/api/version确认返回 JSON再检查配置文件确保BaseUrl以/api/结尾。4.2 现象Linux 客户端报错System.DllNotFoundException: Unable to load shared library libproc原因FogClient.Platform在 Linux 下调用libproc读取进程信息但 Ubuntu/Debian 默认不安装procps包含libproc。解决sudo apt install procps或在编译时移除对libproc的依赖修改FogClient.Platform.Linux/ProcessInfoProvider.cs改用/proc/pid/stat文本解析。4.3 现象macOS 客户端运行时报Operation not permitted无法读取磁盘序列号原因macOS Catalina 强制 Full Disk Access 权限.NET CLI 编译的二进制默认无此权限。解决手动授予System Preferences → Security Privacy → Privacy → Full Disk Access → 添加FogClient.Client或编译时签名codesign --force --deep --sign - ./FogClient.Client需 Apple Developer 账号。4.4 现象任务执行后output字段为空FOG Web UI 显示“Task completed with no output”原因插件ExecuteAsync返回的PluginResult.Output是 C# 对象而非 JSON 字符串如new { a1 }而非JsonSerializer.Serialize(new { a1 })。解决强制序列化。可在基类中封装protected string Serialize(object obj) JsonSerializer.Serialize(obj, new JsonSerializerOptions { WriteIndented false });4.5 现象客户端日志疯狂刷Failed to send heartbeat: The SSL connection could not be established原因VerifySsl设为true但 FOG 服务端用的是自签名证书且未导入到系统证书信任库。解决方案 A推荐将服务端证书导出为fog.crt在客户端机器执行sudo cp fog.crt /usr/local/share/ca-certificates/ sudo update-ca-certificatesLinux或双击导入钥匙串macOS方案 B临时设VerifySsl: false仅限测试环境。5. 日志分析与故障复现用--debug模式抓取原始 HTTP 流量与插件执行痕迹5.1 启用调试日志四层日志级别与对应输出位置fog-client 默认日志级别为Information要看到 HTTP 请求细节必须启动时加--debug参数# Windows FogClient.Client.exe --debug # Linux ./FogClient.Client --debug此时日志包含四层信息日志级别触发条件典型内容查看位置Debug--debug时启用HTTP 请求 URL、Headers、Body含 API Key、响应状态码控制台 logs/fog-client-debug.logInformation默认客户端注册成功、任务拉取、插件开始执行控制台 logs/fog-client-info.logWarning非致命异常任务超时、插件返回空 outputlogs/fog-client-warn.logError致命异常SSL 握手失败、JSON 解析错误、插件抛出未捕获异常logs/fog-client-error.log提示--debug模式下Authorization头会明文打印切勿在生产环境长期开启。调试完立即删掉--debug参数并清空日志文件。5.2 抓包验证用 curl 模拟客户端心跳绕过 .NET SSL 层直测服务端当怀疑是 .NET SSL 栈问题如 TLS 1.2 不兼容可用 curl 直接测试# 模拟心跳请求替换 YOUR_API_KEY 和 MAC curl -X GET \ -H Authorization: Bearer YOUR_API_KEY \ -H Content-Type: application/json \ https://fog-server.internal/api/tasks/00:11:22:33:44:55 \ -v 21 | grep -E ( HTTP| GET| Date) # 若返回 401说明 API Key 错误返回 404说明 BaseUrl 或路径错误返回 200 但空数组说明该 MAC 无待执行任务。5.3 插件单步调试在 Visual Studio 中附加到正在运行的客户端进程这是定位插件逻辑错误最高效的方式在FogClient.Client项目属性 →Debug→ 勾选Enable native code debugging启动客户端FogClient.Client.exe --serviceWindows 服务或./FogClient.ClientLinux/macOSVisual Studio →Debug → Attach to Process→ 找到FogClient.Client进程 → 附加在插件ExecuteAsync方法第一行打断点然后在 FOG Web UI 中手动触发该任务VS 自动停在断点可查看parameters字典内容、变量值、调用堆栈。注意Linux/macOS 下需先安装dotnet-dump工具并生成 core dumpVS for Mac 不支持直接附加。此时改用Console.WriteLine--debug日志更实际。6. 进阶技巧构建离线任务队列与本地策略缓存应对网络中断场景6.1 离线任务队列用 SQLite 替代内存队列实现断网期间任务暂存fog-client 默认任务队列是内存 List网络中断时新拉取的任务会丢失。要支持离线需替换为持久化队列在FogClient.Core中添加 NuGet 包Microsoft.Data.Sqlite创建SqliteTaskQueue类实现ITaskQueue接口public class SqliteTaskQueue : ITaskQueue { private readonly string _dbPath Path.Combine(AppContext.BaseDirectory, tasks.db); public SqliteTaskQueue() { using var conn new SqliteConnection($Data Source{_dbPath}); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText CREATE TABLE IF NOT EXISTS Tasks ( Id INTEGER PRIMARY KEY AUTOINCREMENT, TaskId TEXT NOT NULL, Type TEXT NOT NULL, Parameters TEXT NOT NULL, CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ); cmd.ExecuteNonQuery(); } public async Task EnqueueAsync(TaskItem task) await using var conn new SqliteConnection($Data Source{_dbPath}); // ... 插入逻辑 public async TaskTaskItem DequeueAsync() // ... 查询并删除最早任务 }在Program.cs的 DI 注册中替换services.AddSingletonITaskQueue, SqliteTaskQueue();关键点SQLite 数据库文件必须放在客户端可写目录如 Windows 的%LOCALAPPDATA%\FOGClient\Linux 的/var/lib/fog-client/不能放程序目录可能只读。6.2 本地策略缓存JSON 文件驱动的离线策略执行引擎当网络彻底中断你仍希望终端执行基础策略如禁用 USB 存储、检查防火墙状态。fog-client 支持加载本地policies.json// policies.json [ { id: usb-disable, type: registry, parameters: { key: HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\USBSTOR, value: Start, data: 4 } } ]在客户端启动时自动读取该文件并注入任务队列优先级高于网络任务// 在 Program.cs 的 HostBuilder 中 var policiesPath Path.Combine(AppContext.BaseDirectory, policies.json); if (File.Exists(policiesPath)) { var policies JsonSerializer.DeserializeListPolicy(File.ReadAllText(policiesPath)); foreach (var p in policies) { // 构造 TaskItem 并加入队列 queue.EnqueueAsync(new TaskItem { Type p.Type, Parameters p.Parameters }); } }6.3 策略执行插件模板Windows Registry 修改与 Linux sysctl 设置的统一接口为避免为每个平台写重复插件定义跨平台策略插件public class PolicyPlugin : IPlugin { public async TaskPluginResult ExecuteAsync(Dictionarystring, string parameters) { var os RuntimeInformation.OSDescription; switch (os.ToLower()) { case var s when s.Contains(windows): return await ExecuteWindowsPolicy(parameters); case var s when s.Contains(linux): return await ExecuteLinuxPolicy(parameters); default: return new PluginResult { Status PluginStatus.Failed, Error $OS not supported: {os} }; } } private async TaskPluginResult ExecuteWindowsPolicy(Dictionarystring, string p) { // 使用 Microsoft.Win32.Registry 修改注册表 using var key Registry.LocalMachine.OpenSubKey(p[key], true); key?.SetValue(p[value], Convert.ToInt32(p[data])); return new PluginResult { Status PluginStatus.Success }; } private async TaskPluginResult ExecuteLinuxPolicy(Dictionarystring, string p) { // 执行 sysctl -w kernel.unprivileged_userns_clone0 var result await Process.Run(sysctl, $-w {p[key]}); return result.ExitCode 0 ? new PluginResult { Status PluginStatus.Success } : new PluginResult { Status PluginStatus.Failed, Error result.StandardError }; } }从那以后我每次交付 fog-client 到客户现场都强制走一遍「断网 5 分钟测试」拔掉网线手动触发一个磁盘检查任务确认它进 SQLite 队列再插回网线观察是否自动上报。如果没上报立刻查tasks.db是否有残留记录而不是等客户投诉“任务没执行”。这套离线兜底机制让我在三次电力中断事故中零故障——希望帮到你。本文还有配套的精品资源点击获取