Implement asset inventory

This commit is contained in:
2026-05-08 23:05:45 +02:00
parent a2c79e56d3
commit b59be25a16
7 changed files with 542 additions and 3 deletions
Generated
+2 -1
View File
@@ -3197,13 +3197,14 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hello-agent"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"anyhow",
"env_logger 0.10.2",
"hbb_common",
"log",
"rustdesk",
"serde_json 1.0.118",
"tokio",
"winapi",
"windows-service",
+8 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "hello-agent"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
rust-version = "1.75"
description = "Headless RustDesk-protocol-compatible support agent for Windows"
@@ -28,10 +28,16 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync",
log = "0.4"
env_logger = "0.10"
anyhow = "1"
# Used by `inventory.rs` to validate the PowerShell-produced JSON before
# we stamp it into the sysinfo upload. hbb_common already pulls serde_json
# transitively, so this is a free re-export — listed explicitly here so
# the inventory module's `use serde_json` doesn't depend on internal
# implementation details of hbb_common.
serde_json = "1"
[target.'cfg(target_os = "windows")'.dependencies]
windows-service = "0.6"
winapi = { version = "0.3", features = ["winuser", "wtsapi32", "processthreadsapi", "synchapi", "handleapi", "winbase"] }
winapi = { version = "0.3", features = ["winuser", "wtsapi32", "processthreadsapi", "synchapi", "handleapi", "winbase", "wlanapi", "wlantypes"] }
winreg = "0.11"
# Embed the icon and EXE metadata via the Windows resource compiler.
+208
View File
@@ -0,0 +1,208 @@
//! System inventory collection for hello-agent (CMDB).
//!
//! Collects hardware and OS metadata at startup — BIOS serial number,
//! manufacturer / model, AD domain, OS edition + release, CPU details,
//! RAM, disks, and the BitLocker recovery key for the system drive — and
//! returns it as a compact JSON object. The caller stamps the result into
//! `hbb_common::config::INVENTORY` so the next /api/sysinfo upload carries
//! it under the `inventory` key. The rustdesk-server admin UI's per-device
//! detail page reads it back from `device_sysinfo.payload`.
//!
//! Implementation: a single PowerShell child gathers everything via
//! Get-CimInstance and emits compact JSON. One subprocess for the whole
//! inventory is cheaper than per-field WMI queries and avoids pulling a
//! `wmi`/COM crate into the dep tree. Inventory is collected once at
//! startup; the sysinfo loop's hash-compare suppresses re-uploads of
//! unchanged data, so this isn't repeated on every 120 s tick.
//!
//! Non-Windows builds return an empty JSON object — hello-agent v0 only
//! ships on Windows, but keeping the cross-platform surface compiling
//! makes future Linux work cheap.
#[cfg(target_os = "windows")]
const PS_SCRIPT: &str = r#"
$ErrorActionPreference = 'SilentlyContinue'
$bios = Get-CimInstance -ClassName Win32_BIOS
$cs = Get-CimInstance -ClassName Win32_ComputerSystem
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$cpus = @(Get-CimInstance -ClassName Win32_Processor)
$first_cpu = $cpus | Select-Object -First 1
$total_phys_cores = ($cpus | Measure-Object -Property NumberOfCores -Sum).Sum
$total_log_cores = ($cpus | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum
$disks = @(Get-CimInstance -ClassName Win32_DiskDrive | ForEach-Object {
[pscustomobject]@{
name = $_.DeviceID
model = $_.Model
size_gb = if ($_.Size) { [math]::Round([double]$_.Size / 1GB, 1) } else { 0 }
media = $_.MediaType
}
})
# DisplayVersion is the marketing release ID (e.g. "23H2") — not surfaced
# by Win32_OperatingSystem.Version, which only carries the build number.
$displayVersion = ''
try {
$displayVersion = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name DisplayVersion -ErrorAction Stop).DisplayVersion
} catch {}
# BitLocker recovery key for the system drive. Get-BitLockerVolume needs
# the BitLocker PowerShell module (present on Pro/Enterprise SKUs) and
# admin rights; the agent runs as LocalSystem in production. On Home SKUs
# or Linux this just stays empty.
$bl_key = ''
try {
$sysDrive = $env:SystemDrive
$bv = Get-BitLockerVolume -MountPoint $sysDrive -ErrorAction Stop
$kp = $bv.KeyProtector | Where-Object { $_.KeyProtectorType -eq 'RecoveryPassword' } | Select-Object -First 1
if ($kp -and $kp.RecoveryPassword) { $bl_key = $kp.RecoveryPassword }
} catch {}
# Network interfaces: every adapter (Up + Disabled + Disconnected) with
# its MAC and bound IPv4/IPv6 addresses. Filtering happens server-side
# so the operator can still see "this NIC is disabled" rather than
# silently dropping it. LinkSpeed comes back as a string like "1 Gbps"
# or "100 Mbps" — normalize to integer Mbps, 0 when unknown / down.
$nics = @(Get-NetAdapter | ForEach-Object {
$nic = $_
$ipv4 = @(Get-NetIPAddress -InterfaceIndex $nic.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | ForEach-Object { $_.IPAddress })
$ipv6 = @(Get-NetIPAddress -InterfaceIndex $nic.ifIndex -AddressFamily IPv6 -ErrorAction SilentlyContinue | ForEach-Object { $_.IPAddress })
$speed_mbps = 0
if ($nic.LinkSpeed) {
if ($nic.LinkSpeed -match '^([\d.]+)\s*Gbps') { $speed_mbps = [int]([double]$Matches[1] * 1000) }
elseif ($nic.LinkSpeed -match '^([\d.]+)\s*Mbps') { $speed_mbps = [int]([double]$Matches[1]) }
elseif ($nic.LinkSpeed -match '^([\d.]+)\s*Kbps') { $speed_mbps = 0 }
}
[pscustomobject]@{
name = $nic.Name
description = $nic.InterfaceDescription
mac = $nic.MacAddress
status = "$($nic.Status)"
ipv4 = $ipv4
ipv6 = $ipv6
speed_mbps = $speed_mbps
is_wifi = ($nic.PhysicalMediaType -eq 'Native 802.11')
}
})
# Wi-Fi inventory is collected separately, in Rust, against the Win32
# Native Wi-Fi API (`wlanapi.dll`). netsh's text output is partially
# localized — `Authentication` becomes `Authentifizierung` /
# `Autenticación` / `Autentificare` on de/es/ro Windows — and our
# regexes silently dropped fields on non-English hosts. The native API
# returns SSIDs as bytes and auth/cipher as numeric enums, so the
# resulting data is locale-stable. See `src/wifi_native.rs`. The fields
# `wifi_current` / `wifi_nearby` are merged into this object after
# PowerShell exits.
# Public egress IP: best-effort lookup against an external echo service.
# Used when the operator wants to correlate the device with a NAT'd
# location. 5 s timeout so a blocked corporate firewall doesn't stall
# inventory collection. ipify is HTTPS, IPv4-only by default, no auth.
$public_ip = ''
try {
$public_ip = (Invoke-RestMethod -Uri 'https://api.ipify.org' -TimeoutSec 5 -ErrorAction Stop).ToString().Trim()
} catch {}
$os_release = "$($os.Version)"
if ($displayVersion) { $os_release = "$($os.Version) $displayVersion" }
$result = [pscustomobject]@{
serial_number = $bios.SerialNumber
manufacturer = $cs.Manufacturer
model = $cs.Model
domain = $cs.Domain
os_distro = $os.Caption
os_release = $os_release
cpu_model = $first_cpu.Name
cpu_speed_ghz = if ($first_cpu.MaxClockSpeed) { [math]::Round([double]$first_cpu.MaxClockSpeed / 1000, 2) } else { 0 }
cpu_cores_physical = $total_phys_cores
cpu_cores_logical = $total_log_cores
ram_gb = if ($cs.TotalPhysicalMemory) { [math]::Round([double]$cs.TotalPhysicalMemory / 1GB, 1) } else { 0 }
disks = $disks
bitlocker_recovery_key = $bl_key
network_interfaces = $nics
public_ip = $public_ip
}
$result | ConvertTo-Json -Compress -Depth 6
"#;
/// Collect the inventory and return it as a JSON string. Empty string on
/// any failure — the caller treats that as "skip this upload's
/// `inventory` field" rather than uploading garbage.
#[cfg(target_os = "windows")]
pub fn collect_inventory() -> String {
use std::os::windows::process::CommandExt;
use std::process::Command;
// CREATE_NO_WINDOW prevents a brief PowerShell console flash if the
// agent is ever run interactively (dev mode). In service mode there's
// no console anyway, but the flag is harmless.
const CREATE_NO_WINDOW: u32 = 0x08000000;
let output = match Command::new("powershell.exe")
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
PS_SCRIPT,
])
.creation_flags(CREATE_NO_WINDOW)
.output()
{
Ok(o) => o,
Err(e) => {
log::warn!("inventory: powershell failed to spawn: {e}");
return String::new();
}
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
log::warn!(
"inventory: powershell exited non-zero ({:?}): {}",
output.status.code(),
stderr.trim()
);
return String::new();
}
let stdout = String::from_utf8_lossy(&output.stdout);
let trimmed = stdout.trim();
if trimmed.is_empty() {
log::warn!("inventory: powershell produced empty stdout");
return String::new();
}
// Parse the PowerShell-produced object so we can merge in the
// native Wi-Fi data below. Any parse failure aborts the whole
// collection — sync.rs would otherwise have to reject malformed
// payloads mid-upload and we'd carry the bad data until the next
// collect.
let mut value: serde_json::Value = match serde_json::from_str(trimmed) {
Ok(v) => v,
Err(e) => {
log::warn!(
"inventory: powershell output is not valid JSON: {e}; first 200 chars: {:.200}",
trimmed
);
return String::new();
}
};
// Native Wi-Fi via wlanapi.dll. Emits SSIDs as bytes and auth/cipher
// as numeric enums, so the result is locale-stable across the
// en/de/es/ro Windows builds in our fleet — replaces the
// previously-localized netsh parser.
let (wifi_current, wifi_nearby) = crate::wifi_native::collect();
if let Some(c) = wifi_current {
value["wifi_current"] = c;
}
value["wifi_nearby"] = serde_json::Value::Array(wifi_nearby);
let serialized = value.to_string();
log::info!("inventory: collected ({} bytes)", serialized.len());
serialized
}
#[cfg(not(target_os = "windows"))]
pub fn collect_inventory() -> String {
String::new()
}
+19
View File
@@ -23,6 +23,7 @@
mod cli;
mod config_import;
mod inventory;
#[cfg(target_os = "windows")]
mod cm_popup;
@@ -30,6 +31,8 @@ mod cm_popup;
mod service;
#[cfg(target_os = "windows")]
mod unattended_password;
#[cfg(target_os = "windows")]
mod wifi_native;
use cli::{Action, ParsedArgs};
@@ -206,6 +209,22 @@ fn run_server() {
),
}
// Kick off CMDB inventory collection on a background thread before
// start_server boots. PowerShell's first-run cost (a few hundred ms
// to a few seconds) shouldn't delay the rendezvous heartbeat — the
// sysinfo upload loop only fires every TIME_CONN seconds, so the
// inventory will be ready in time for the very first /api/sysinfo
// POST whether collection finishes in 50ms or 5s. We deliberately
// don't retry on failure: a transient PowerShell hiccup leaves the
// INVENTORY global empty, and sync.rs simply omits the `inventory`
// key from the upload. Next agent restart re-tries.
std::thread::spawn(|| {
let inv = inventory::collect_inventory();
if !inv.is_empty() {
*hbb_common::config::INVENTORY.write().unwrap() = inv;
}
});
// `start_server` is `#[tokio::main]` and runs forever. (is_server=true,
// no_server=false). It boots the default IPC server, input service,
// rendezvous mediator, and heartbeat sync.
+278
View File
@@ -0,0 +1,278 @@
//! Locale-independent Wi-Fi inventory via the Win32 Native Wi-Fi API
//! (`wlanapi.dll`).
//!
//! Replaces the previous `netsh wlan show …` text-parsing approach.
//! `netsh` partially localizes its output — labels like
//! `Authentication` / `Authentifizierung` / `Autenticación` / `Autentificare`
//! shift with the user's display language, and our regexes silently
//! dropped fields on non-English Windows. The fleet runs four
//! languages (en, de, es, ro), so we move to the structured API:
//!
//! * SSIDs come back as raw `UCHAR[32]` bytes — no localization
//! possible.
//! * Authentication comes back as a `DOT11_AUTH_ALGORITHM` enum
//! (a small integer); we map to our own English labels.
//! * Cipher / signal quality are likewise plain enums / integers.
//!
//! Returned shape (consumed by `inventory::collect_inventory` and
//! merged into the sysinfo upload):
//!
//! ```text
//! wifi_current: { ssid, bssid, signal_pct, rssi_dbm, rx_kbps, tx_kbps,
//! auth, cipher } | None
//! wifi_nearby: [ { ssid, signal_pct, auth, cipher }, … up to 30 ]
//! ```
//!
//! All allocations from the API (handle, interface list, network list,
//! query-interface buffer) are freed in this module — no caller cleanup.
//! Every failure path returns best-effort partial data; we never panic.
//!
//! We deliberately do **not** trigger a fresh scan via `WlanScan`. The
//! OS scans periodically on its own, and the cached BSS list is usually
//! < 60 s old. Triggering a scan would either delay startup by ~5 s
//! waiting for `wlan_notification_acm_scan_complete`, or return stale
//! data anyway if we don't wait. The next inventory cycle (≤ 120 s)
//! picks up any change.
#![cfg(target_os = "windows")]
use serde_json::{json, Value};
use std::collections::HashMap;
use std::ptr;
use std::slice;
// IMPORTANT: use winapi's `HANDLE` / `PVOID` (which under the hood wrap
// `winapi::ctypes::c_void`) rather than `std::ffi::c_void`. Rust treats
// the two void types as distinct even though they're the same in C —
// mixing them produces opaque "expected `winapi::ctypes::c_void`, found
// `std::ffi::c_void`" errors. macOS hosts can't catch this because
// winapi is gated to Windows targets, so Cargo never typechecks this
// file off Windows.
use winapi::shared::minwindef::DWORD;
use winapi::shared::ntdef::{HANDLE, PVOID};
use winapi::shared::winerror::ERROR_SUCCESS;
use winapi::shared::wlantypes::DOT11_SSID;
use winapi::um::wlanapi::{
wlan_intf_opcode_current_connection, wlan_interface_state_connected, WlanCloseHandle,
WlanEnumInterfaces, WlanFreeMemory, WlanGetAvailableNetworkList, WlanOpenHandle,
WlanQueryInterface, WLAN_AVAILABLE_NETWORK_LIST, WLAN_CONNECTION_ATTRIBUTES,
WLAN_INTERFACE_INFO_LIST,
};
/// Vista+ client version. Win10/Win11 happily negotiate down via
/// `negotiated_version`. We never need v1 (XP-era).
const CLIENT_VERSION_VISTA: DWORD = 2;
/// Public entry point. Returns `(current_connection, nearby_list)`.
/// On hosts without a Wi-Fi adapter, without the WLAN AutoConfig
/// service, or on any FFI failure → `(None, vec![])`.
pub fn collect() -> (Option<Value>, Vec<Value>) {
// SAFETY: every raw-pointer dereference and array read below is
// bounds-checked against the `dwNumberOfItems` field of the parent
// list, and the API contract is that those fields match the
// allocated array length. All allocations are paired with
// `WlanFreeMemory` before this function returns.
unsafe { collect_inner() }
}
unsafe fn collect_inner() -> (Option<Value>, Vec<Value>) {
let mut handle: HANDLE = ptr::null_mut();
let mut neg_ver: DWORD = 0;
if WlanOpenHandle(
CLIENT_VERSION_VISTA,
ptr::null_mut(),
&mut neg_ver,
&mut handle,
) != ERROR_SUCCESS as DWORD
{
return (None, Vec::new());
}
let mut current: Option<Value> = None;
// Dedupe nearby by SSID, keeping the strongest-quality entry per
// network. WlanGetAvailableNetworkList already collapses BSSIDs to
// one row per (SSID, BSS-type), but if multiple Wi-Fi NICs are
// present we'd see the same SSID twice; this keeps the louder copy.
let mut nearby_by_ssid: HashMap<String, Value> = HashMap::new();
let mut iface_list: *mut WLAN_INTERFACE_INFO_LIST = ptr::null_mut();
if WlanEnumInterfaces(handle, ptr::null_mut(), &mut iface_list) == ERROR_SUCCESS as DWORD
&& !iface_list.is_null()
{
let count = (*iface_list).dwNumberOfItems as usize;
if count > 0 {
let ifaces = slice::from_raw_parts((*iface_list).InterfaceInfo.as_ptr(), count);
for iface in ifaces {
read_iface(handle, iface, &mut current, &mut nearby_by_ssid);
}
}
WlanFreeMemory(iface_list as *mut _);
}
WlanCloseHandle(handle, ptr::null_mut());
let mut nearby: Vec<Value> = nearby_by_ssid.into_values().collect();
nearby.sort_by(|a, b| {
let bs = b.get("signal_pct").and_then(|v| v.as_u64()).unwrap_or(0);
let as_ = a.get("signal_pct").and_then(|v| v.as_u64()).unwrap_or(0);
bs.cmp(&as_)
});
nearby.truncate(30);
(current, nearby)
}
unsafe fn read_iface(
handle: HANDLE,
iface: &winapi::um::wlanapi::WLAN_INTERFACE_INFO,
current: &mut Option<Value>,
nearby_by_ssid: &mut HashMap<String, Value>,
) {
let guid = iface.InterfaceGuid;
// Current connection. Querying when the interface isn't connected
// returns ERROR_NOT_FOUND — no harm, but skipping the call is
// tidier and avoids a noisy ETW event on locked-down endpoints.
if iface.isState == wlan_interface_state_connected && current.is_none() {
let mut data_size: DWORD = 0;
let mut data: PVOID = ptr::null_mut();
let mut value_type: u32 = 0;
if WlanQueryInterface(
handle,
&guid,
wlan_intf_opcode_current_connection,
ptr::null_mut(),
&mut data_size,
&mut data,
&mut value_type,
) == ERROR_SUCCESS as DWORD
&& !data.is_null()
&& data_size as usize >= std::mem::size_of::<WLAN_CONNECTION_ATTRIBUTES>()
{
let attrs = &*(data as *const WLAN_CONNECTION_ATTRIBUTES);
let assoc = &attrs.wlanAssociationAttributes;
let sec = &attrs.wlanSecurityAttributes;
let ssid = read_ssid(&assoc.dot11Ssid);
let bssid = format_mac(&assoc.dot11Bssid);
*current = Some(json!({
"ssid": ssid,
"bssid": bssid,
"signal_pct": assoc.wlanSignalQuality,
"rssi_dbm": rssi_from_quality(assoc.wlanSignalQuality),
"rx_kbps": assoc.ulRxRate,
"tx_kbps": assoc.ulTxRate,
"auth": auth_label(sec.dot11AuthAlgorithm as u32),
"cipher": cipher_label(sec.dot11CipherAlgorithm as u32),
}));
WlanFreeMemory(data);
}
}
// Available networks (cached scan).
let mut net_list: *mut WLAN_AVAILABLE_NETWORK_LIST = ptr::null_mut();
if WlanGetAvailableNetworkList(handle, &guid, 0, ptr::null_mut(), &mut net_list)
== ERROR_SUCCESS as DWORD
&& !net_list.is_null()
{
let n = (*net_list).dwNumberOfItems as usize;
if n > 0 {
let nets = slice::from_raw_parts((*net_list).Network.as_ptr(), n);
for net in nets {
let ssid = read_ssid(&net.dot11Ssid);
if ssid.is_empty() {
// Hidden networks broadcast empty SSIDs in beacons —
// skip rather than emit `{ ssid: "" }` rows that
// collapse together in the dedupe map.
continue;
}
let entry_signal = net.wlanSignalQuality;
let prev_signal = nearby_by_ssid
.get(&ssid)
.and_then(|v| v.get("signal_pct"))
.and_then(|v| v.as_u64())
.unwrap_or(0);
if (entry_signal as u64) >= prev_signal {
nearby_by_ssid.insert(
ssid.clone(),
json!({
"ssid": ssid,
"signal_pct": entry_signal,
"auth": auth_label(net.dot11DefaultAuthAlgorithm as u32),
"cipher": cipher_label(net.dot11DefaultCipherAlgorithm as u32),
}),
);
}
}
}
WlanFreeMemory(net_list as *mut _);
}
}
/// Read a `DOT11_SSID` (length-prefixed UCHAR[32]) into a Rust String.
/// SSIDs are nominally any byte sequence ≤ 32 octets; UTF-8 is by far
/// the most common (also the IEEE recommendation), but Latin-1 and
/// random bytes occur. We use lossy UTF-8 decoding so weird encodings
/// don't yield panics or empty strings — the operator can still
/// recognize most networks visually.
fn read_ssid(s: &DOT11_SSID) -> String {
let len = (s.uSSIDLength as usize).min(s.ucSSID.len());
String::from_utf8_lossy(&s.ucSSID[..len]).into_owned()
}
fn format_mac(bytes: &[u8; 6]) -> String {
format!(
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]
)
}
/// MS docs: WLAN_SIGNAL_QUALITY 0 ↔ 100 dBm, 100 ↔ 50 dBm, linear
/// interpolation in between. `quality / 2 - 100`.
fn rssi_from_quality(q: u32) -> i32 {
-100 + (q.min(100) as i32 / 2)
}
/// Human label for a `DOT11_AUTH_ALGORITHM` value. Matched on raw
/// integers because winapi 0.3 was tagged before WPA3 / OWE went into
/// the SDK — relying on the named constants would force a dep upgrade
/// just to recognize WPA3 networks. Numeric values are part of the
/// stable Wi-Fi protocol spec, not the SDK.
fn auth_label(a: u32) -> &'static str {
match a {
1 => "Open",
2 => "Shared",
3 => "WPA-Enterprise",
4 => "WPA-Personal",
5 => "WPA-None",
6 => "WPA2-Enterprise",
7 => "WPA2-Personal",
8 => "WPA3-Enterprise (192-bit)",
9 => "WPA3-Personal (SAE)",
10 => "OWE",
11 => "WPA3-Enterprise",
_ => "Unknown",
}
}
/// Human label for a `DOT11_CIPHER_ALGORITHM` value. Same numeric-match
/// rationale as `auth_label`. Values from the IEEE 802.11 / Microsoft
/// header (wlantypes.h DOT11_CIPHER_ALGORITHM enum).
fn cipher_label(c: u32) -> &'static str {
match c {
0x00 => "None",
0x01 => "WEP-40",
0x02 => "TKIP",
0x04 => "AES (CCMP)",
0x05 => "WEP-104",
0x06 => "BIP-CMAC-128",
0x08 => "GCMP",
0x09 => "GCMP-256",
0x0A => "CCMP-256",
0x0B => "BIP-GMAC-128",
0x0C => "BIP-GMAC-256",
0x0D => "BIP-CMAC-256",
0x100 => "WPA-Use-Group",
0x101 => "WEP",
_ => "Unknown",
}
}
+11
View File
@@ -123,6 +123,17 @@ lazy_static::lazy_static! {
/// "empty = omit" convention as `AGENT_NAME`. For hello-agent this
/// is `env!("CARGO_PKG_VERSION")`.
pub static ref AGENT_VERSION: RwLock<String> = RwLock::new(String::new());
/// Pre-serialized JSON object describing the host's hardware /
/// firmware / OS edition inventory (BIOS serial, manufacturer, model,
/// AD domain, OS edition + release, CPU details, RAM, disks,
/// BitLocker recovery key). Same "empty = omit" convention as
/// `AGENT_NAME`: when non-empty, `hbbs_http::sync` parses it and
/// merges it into the sysinfo upload under the `inventory` key, where
/// the rustdesk-server admin UI's per-device detail page reads it.
/// Stored pre-serialized so the producer (hello-agent's `inventory`
/// module) owns the schema and the consumer (sync) doesn't need to
/// know the field set.
pub static ref INVENTORY: RwLock<String> = RwLock::new(String::new());
static ref KEY_PAIR: Mutex<Option<KeyPair>> = Default::default();
static ref USER_DEFAULT_CONFIG: RwLock<(UserDefaultConfig, Instant)> = RwLock::new((UserDefaultConfig::load(), Instant::now()));
pub static ref NEW_STORED_PEER_CONFIG: Mutex<HashSet<String>> = Default::default();
+16
View File
@@ -147,6 +147,22 @@ async fn start_hbbs_sync_async() {
if !agent_version.is_empty() {
v["agent_version"] = json!(agent_version);
}
// Optional CMDB inventory. Producer (hello-agent's
// `inventory` module) populates this with a pre-
// serialized JSON object covering BIOS / hardware /
// OS-edition fields. We re-parse on each upload (the
// string is small — single-digit kB at most) rather
// than caching, so a refreshed inventory is picked up
// without bookkeeping. Parse failure is silently
// dropped: the sysinfo upload still goes out without
// the `inventory` key, identical to a vanilla rustdesk
// install.
let inventory = config::INVENTORY.read().unwrap().clone();
if !inventory.is_empty() {
if let Ok(inv_v) = serde_json::from_str::<Value>(&inventory) {
v["inventory"] = inv_v;
}
}
let ab_name = Config::get_option(keys::OPTION_PRESET_ADDRESS_BOOK_NAME);
if !ab_name.is_empty() {
v[keys::OPTION_PRESET_ADDRESS_BOOK_NAME] = json!(ab_name);