Missing files

This commit is contained in:
Herbert Wolverson committed 2026-04-13 13:34:08 -05:00
1 parent 7ca64aabfd
commit 92b386ede9
2 files changed
+687

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
//! Bus types for dynamic circuit overlay operations.
//!
//! `DynamicAddCircuit` and `DynamicRemoveCircuit` are high-level requests used
//! to manage runtime-only dynamic circuits. These operations apply to the
//! overlay state and never mutate `ShapedDevices.csv`.
//!
//! When callers omit identity fields, the daemon allocates stable defaults
//! such as `Dynamic <u64>` for `circuit_id` and reuses that value for
//! `device_id`.
use allocative::Allocative;
use serde::{Deserialize, Serialize};
/// Attachment target for a dynamic circuit overlay request.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Allocative)]
pub struct DynamicCircuitAttachmentTarget {
/// Stable topology node identifier from `network.json` or `network.effective.json`.
pub node_id: Option<String>,
/// Human-readable topology node name used when a stable ID is unavailable.
pub node_name: Option<String>,
}
/// Optional caller-supplied identity fields for a dynamic circuit.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Allocative)]
pub struct DynamicCircuitIdentitySpec {
/// Circuit identifier override. The daemon allocates `Dynamic <n>` when omitted.
pub circuit_id: Option<String>,
/// Device identifier override. Defaults to `circuit_id` when omitted.
pub device_id: Option<String>,
/// Human-readable circuit display name.
pub circuit_name: Option<String>,
/// Human-readable device display name.
pub device_name: Option<String>,
}
/// Rate inputs for a dynamic circuit overlay request.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Allocative)]
pub struct DynamicCircuitRates {
/// Minimum guaranteed downstream rate in Mbps.
pub download_min_mbps: f32,
/// Minimum guaranteed upstream rate in Mbps.
pub upload_min_mbps: f32,
/// Maximum downstream rate in Mbps.
pub download_max_mbps: f32,
/// Maximum upstream rate in Mbps.
pub upload_max_mbps: f32,
/// Optional SQM override such as `cake` or `fq_codel`.
pub sqm_override: Option<String>,
}
/// Request payload for adding a dynamic circuit overlay entry.
///
/// This is a high-level overlay operation and never writes to
/// `ShapedDevices.csv`. When `identity` is omitted, the daemon allocates
/// defaults such as `Dynamic <u64>`.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Allocative)]
pub struct DynamicAddCircuitRequest {
/// Attachment target for the dynamic circuit.
pub attachment: DynamicCircuitAttachmentTarget,
/// Optional caller-supplied identity fields.
pub identity: Option<DynamicCircuitIdentitySpec>,
/// Requested shaping rates.
pub rates: DynamicCircuitRates,
/// IPs and CIDRs owned by this dynamic circuit.
pub ip_cidrs: Vec<String>,
/// Optional TTL in seconds. The daemon applies the default when omitted.
pub ttl_seconds: Option<u64>,
}
/// Request payload for removing a dynamic circuit overlay entry.
///
/// This removes only the runtime overlay entry; it never edits
/// `ShapedDevices.csv`.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Allocative)]
pub struct DynamicRemoveCircuitRequest {
/// Stable circuit identifier to remove from the dynamic overlay.
pub circuit_id: String,
}
/// Reply payload returned after creating a dynamic circuit overlay entry.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Allocative)]
pub struct DynamicAddCircuitReply {
/// Allocated or accepted circuit identifier.
pub circuit_id: String,
/// Allocated or accepted device identifier.
pub device_id: String,
}
+600
View File
@@ -0,0 +1,600 @@
//! Dynamic circuit persistence for the runtime overlay.
//!
//! The dynamic-circuits store is an overlay on top of
//! `lqos_config::ConfigShapedDevices` and never mutates ShapedDevices.csv.
//! Static `ShapedDevices.csv` data remains authoritative; dynamic state exists
//! only to persist runtime overlay entries in `dynamic_status.json`.
//!
//! This module is intentionally small until the runtime overlay loader lands.
use allocative::Allocative;
use serde::{Deserialize, Serialize};
use std::{fs, path::{Path, PathBuf}};
use thiserror::Error;
/// File name used to persist dynamic circuit overlay state.
pub const DYNAMIC_STATUS_FILENAME: &str = "dynamic_status.json";
/// Current supported on-disk schema version for `dynamic_status.json`.
pub const DYNAMIC_STATUS_VERSION: u32 = 1;
/// Errors that can occur while resolving or loading `dynamic_status.json`.
#[derive(Debug, Error)]
pub enum DynamicStatusError {
/// The main LibreQoS config could not be loaded.
#[error("Unable to load LibreQoS config while resolving dynamic_status.json path")]
ConfigLoadError,
/// A `dynamic_status.json` file could not be read from disk.
#[error("Unable to read dynamic status file at {path}: {source}")]
CannotRead {
/// Full path to the file that failed.
path: String,
/// Source IO error.
source: std::io::Error,
},
/// A `dynamic_status.json` file existed but could not be parsed.
#[error("Unable to parse dynamic status file at {path}: {details}")]
ParseError {
/// Full path to the file that failed.
path: String,
/// Parse error details.
details: String,
},
/// A `dynamic_status.json` file could not be written to disk.
#[error("Unable to write dynamic status file at {path}: {source}")]
CannotWrite {
/// Full path to the file that failed.
path: String,
/// Source IO error.
source: std::io::Error,
},
/// Dynamic status data could not be serialized.
#[error("Unable to serialize dynamic status file: {details}")]
SerializeError {
/// Serialization error details.
details: String,
},
/// A dynamic circuit record is internally inconsistent or contains invalid data.
#[error("Invalid dynamic circuit record: {details}")]
InvalidRecord {
/// Specific validation or conversion details.
details: String,
},
}
/// Root persisted state for the dynamic circuit runtime overlay.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Allocative)]
pub struct DynamicStatusFile {
/// On-disk schema version for `dynamic_status.json`.
pub version: u32,
/// Monotonic ID allocator so `Dynamic <n>` values are never reused.
pub next_dynamic_id: u64,
/// Persisted dynamic circuits that can be materialized into overlay devices.
pub circuits: Vec<DynamicCircuitRecord>,
}
impl DynamicStatusFile {
/// Loads `dynamic_status.json`, returning an empty default state when the file is absent.
pub fn load_or_default() -> Result<Self, DynamicStatusError> {
let path = dynamic_status_path()?;
if !path.exists() {
return Ok(Self::default());
}
let raw = fs::read_to_string(&path).map_err(|source| DynamicStatusError::CannotRead {
path: path.display().to_string(),
source,
})?;
let parsed: Self = serde_json::from_str(&raw).map_err(|error| DynamicStatusError::ParseError {
path: path.display().to_string(),
details: error.to_string(),
})?;
if parsed.version != DYNAMIC_STATUS_VERSION {
return Err(DynamicStatusError::InvalidRecord {
details: format!(
"Unsupported dynamic_status.json version {} at {} (expected {})",
parsed.version,
path.display(),
DYNAMIC_STATUS_VERSION
),
});
}
Ok(parsed)
}
/// Saves `dynamic_status.json` using a same-directory temp file and atomic rename.
pub fn save_atomic(&self) -> Result<(), DynamicStatusError> {
let path = dynamic_status_path()?;
let tmp_path = path.with_file_name(format!("{DYNAMIC_STATUS_FILENAME}.tmp"));
let serialized =
serde_json::to_string_pretty(self).map_err(|error| DynamicStatusError::SerializeError {
details: error.to_string(),
})?;
fs::write(&tmp_path, serialized).map_err(|source| DynamicStatusError::CannotWrite {
path: tmp_path.display().to_string(),
source,
})?;
fs::rename(&tmp_path, &path).map_err(|source| DynamicStatusError::CannotWrite {
path: path.display().to_string(),
source,
})?;
Ok(())
}
/// Allocates default circuit/device identifiers when callers omit them.
pub fn allocate_default_ids(
&mut self,
circuit_id: Option<String>,
device_id: Option<String>,
) -> (String, String) {
let circuit_id = circuit_id.unwrap_or_else(|| {
let allocated = format!("Dynamic {}", self.next_dynamic_id);
self.next_dynamic_id += 1;
allocated
});
let device_id = device_id.unwrap_or_else(|| circuit_id.clone());
(circuit_id, device_id)
}
}
/// Persisted attachment target for a dynamic circuit.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Allocative)]
pub struct DynamicAttachmentTarget {
/// Stable topology node identifier when known.
pub node_id: Option<String>,
/// Human-readable parent node name.
pub node_name: String,
}
/// Persisted dynamic circuit record.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Allocative)]
pub struct DynamicCircuitRecord {
/// Stable circuit identifier.
pub circuit_id: String,
/// Stable device identifier.
pub device_id: String,
/// Optional human-readable circuit name.
pub circuit_name: Option<String>,
/// Optional human-readable device name.
pub device_name: Option<String>,
/// Attachment target used to place the circuit in the topology.
pub attachment: DynamicAttachmentTarget,
/// Optional MAC address associated with the device.
pub mac: Option<String>,
/// IP/CIDR strings assigned to the dynamic circuit.
pub ip_cidrs: Vec<String>,
/// Minimum guaranteed downstream rate in Mbps.
pub download_min_mbps: f32,
/// Minimum guaranteed upstream rate in Mbps.
pub upload_min_mbps: f32,
/// Maximum downstream rate in Mbps.
pub download_max_mbps: f32,
/// Maximum upstream rate in Mbps.
pub upload_max_mbps: f32,
/// Optional SQM override.
pub sqm_override: Option<String>,
/// TTL in seconds applied when extending expiry on activity.
pub ttl_seconds: u64,
/// Unix timestamp when the record was created.
pub created: u64,
/// Unix timestamp when the record was last observed active.
#[serde(alias = "last_seen")]
pub last_seen_at: u64,
/// Unix timestamp when the record should expire if not refreshed.
#[serde(alias = "expires")]
pub expires_at: u64,
}
impl DynamicCircuitRecord {
/// Converts the persisted record into a `ShapedDevice` for overlay indexing.
pub fn to_shaped_device(&self) -> Result<crate::ShapedDevice, DynamicStatusError> {
let mut ipv4 = Vec::new();
let mut ipv6 = Vec::new();
for cidr in &self.ip_cidrs {
if let Ok(parsed) = crate::ShapedDevice::parse_cidr_v4(cidr) {
ipv4.push(parsed);
continue;
}
if let Ok(parsed) = crate::ShapedDevice::parse_cidr_v6(cidr) {
ipv6.push(parsed);
continue;
}
return Err(DynamicStatusError::InvalidRecord {
details: format!("Unable to parse CIDR '{cidr}' for dynamic circuit '{}'", self.circuit_id),
});
}
let circuit_name = self
.circuit_name
.clone()
.unwrap_or_else(|| self.circuit_id.clone());
let device_name = self
.device_name
.clone()
.unwrap_or_else(|| self.device_id.clone());
let parent_node = self.attachment.node_name.clone();
Ok(crate::ShapedDevice {
circuit_hash: lqos_utils::hash_to_i64(&self.circuit_id),
device_hash: lqos_utils::hash_to_i64(&self.device_id),
parent_hash: lqos_utils::hash_to_i64(&parent_node),
circuit_id: self.circuit_id.clone(),
circuit_name,
device_id: self.device_id.clone(),
device_name,
parent_node,
mac: self.mac.clone().unwrap_or_default(),
ipv4,
ipv6,
download_min_mbps: self.download_min_mbps,
upload_min_mbps: self.upload_min_mbps,
download_max_mbps: self.download_max_mbps,
upload_max_mbps: self.upload_max_mbps,
comment: String::new(),
sqm_override: self.sqm_override.clone(),
})
}
}
fn dynamic_status_path_for_config(config: &crate::Config) -> PathBuf {
Path::new(&config.lqos_directory).join(DYNAMIC_STATUS_FILENAME)
}
/// Resolves the active `dynamic_status.json` path from the LibreQoS config.
pub fn dynamic_status_path() -> Result<PathBuf, DynamicStatusError> {
let config = crate::load_config().map_err(|_| DynamicStatusError::ConfigLoadError)?;
Ok(dynamic_status_path_for_config(&config))
}
#[cfg(test)]
mod tests {
use super::{
DYNAMIC_STATUS_FILENAME, DYNAMIC_STATUS_VERSION, DynamicAttachmentTarget,
DynamicCircuitRecord, DynamicStatusFile,
dynamic_status_path,
};
use crate::{Config, clear_cached_config};
use std::{
fs,
path::PathBuf,
sync::Mutex,
sync::atomic::{AtomicU64, Ordering},
};
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
static TEST_ENV_LOCK: Mutex<()> = Mutex::new(());
fn unique_test_dir(prefix: &str) -> PathBuf {
let suffix = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("libreqos-{prefix}-{}-{suffix}", std::process::id()))
}
#[test]
fn dynamic_status_path_ends_with_filename() {
let _guard = TEST_ENV_LOCK.lock().expect("lock test env");
clear_cached_config();
let config_dir = unique_test_dir("dynamic-status-path");
let config_path = config_dir.join("lqos.conf");
fs::create_dir_all(&config_dir).expect("create config dir");
let mut config = Config::default();
config.lqos_directory = config_dir.to_string_lossy().to_string();
let config_toml = toml::to_string(&config).expect("serialize config");
fs::write(&config_path, config_toml).expect("write config");
let old_lqos_config = std::env::var_os("LQOS_CONFIG");
let old_lqos_directory = std::env::var_os("LQOS_DIRECTORY");
unsafe {
std::env::set_var("LQOS_CONFIG", &config_path);
std::env::set_var("LQOS_DIRECTORY", &config_dir);
}
let resolved = dynamic_status_path().expect("dynamic_status_path");
assert!(resolved.ends_with(DYNAMIC_STATUS_FILENAME));
match old_lqos_config {
Some(value) => unsafe { std::env::set_var("LQOS_CONFIG", value) },
None => unsafe { std::env::remove_var("LQOS_CONFIG") },
}
match old_lqos_directory {
Some(value) => unsafe { std::env::set_var("LQOS_DIRECTORY", value) },
None => unsafe { std::env::remove_var("LQOS_DIRECTORY") },
}
clear_cached_config();
let _ = fs::remove_file(&config_path);
let _ = fs::remove_dir_all(&config_dir);
}
#[test]
fn load_or_default_missing_file_returns_empty_status() {
let _guard = TEST_ENV_LOCK.lock().expect("lock test env");
clear_cached_config();
let config_dir = unique_test_dir("dynamic-status-missing");
let config_path = config_dir.join("lqos.conf");
fs::create_dir_all(&config_dir).expect("create config dir");
let mut config = Config::default();
config.lqos_directory = config_dir.to_string_lossy().to_string();
let config_toml = toml::to_string(&config).expect("serialize config");
fs::write(&config_path, config_toml).expect("write config");
let old_lqos_config = std::env::var_os("LQOS_CONFIG");
let old_lqos_directory = std::env::var_os("LQOS_DIRECTORY");
unsafe {
std::env::set_var("LQOS_CONFIG", &config_path);
std::env::set_var("LQOS_DIRECTORY", &config_dir);
}
let loaded = DynamicStatusFile::load_or_default().expect("load_or_default");
assert_eq!(loaded.circuits.len(), 0);
match old_lqos_config {
Some(value) => unsafe { std::env::set_var("LQOS_CONFIG", value) },
None => unsafe { std::env::remove_var("LQOS_CONFIG") },
}
match old_lqos_directory {
Some(value) => unsafe { std::env::set_var("LQOS_DIRECTORY", value) },
None => unsafe { std::env::remove_var("LQOS_DIRECTORY") },
}
clear_cached_config();
let _ = fs::remove_file(&config_path);
let _ = fs::remove_dir_all(&config_dir);
}
#[test]
fn save_atomic_then_load_round_trip() {
let _guard = TEST_ENV_LOCK.lock().expect("lock test env");
clear_cached_config();
let config_dir = unique_test_dir("dynamic-status-save");
let config_path = config_dir.join("lqos.conf");
fs::create_dir_all(&config_dir).expect("create config dir");
let mut config = Config::default();
config.lqos_directory = config_dir.to_string_lossy().to_string();
let config_toml = toml::to_string(&config).expect("serialize config");
fs::write(&config_path, config_toml).expect("write config");
let old_lqos_config = std::env::var_os("LQOS_CONFIG");
let old_lqos_directory = std::env::var_os("LQOS_DIRECTORY");
unsafe {
std::env::set_var("LQOS_CONFIG", &config_path);
std::env::set_var("LQOS_DIRECTORY", &config_dir);
}
let status = DynamicStatusFile {
version: DYNAMIC_STATUS_VERSION,
next_dynamic_id: 43,
circuits: vec![super::DynamicCircuitRecord {
circuit_id: "Dynamic 42".to_string(),
device_id: "Dynamic 42".to_string(),
circuit_name: Some("Transient Subscriber".to_string()),
device_name: Some("Transient CPE".to_string()),
attachment: super::DynamicAttachmentTarget {
node_id: Some("tower-a-sector-3".to_string()),
node_name: "Tower A / Sector 3".to_string(),
},
mac: Some("aa:bb:cc:dd:ee:ff".to_string()),
ip_cidrs: vec!["192.0.2.10/32".to_string(), "2001:db8::1234/128".to_string()],
download_min_mbps: 25.0,
upload_min_mbps: 5.0,
download_max_mbps: 150.0,
upload_max_mbps: 30.0,
sqm_override: Some("cake".to_string()),
ttl_seconds: 3600,
created: 1_775_606_400,
last_seen_at: 1_775_608_200,
expires_at: 1_775_611_800,
}],
};
status.save_atomic().expect("save_atomic");
let loaded = DynamicStatusFile::load_or_default().expect("load_or_default");
assert_eq!(loaded, status);
match old_lqos_config {
Some(value) => unsafe { std::env::set_var("LQOS_CONFIG", value) },
None => unsafe { std::env::remove_var("LQOS_CONFIG") },
}
match old_lqos_directory {
Some(value) => unsafe { std::env::set_var("LQOS_DIRECTORY", value) },
None => unsafe { std::env::remove_var("LQOS_DIRECTORY") },
}
clear_cached_config();
let _ = fs::remove_file(config_dir.join(DYNAMIC_STATUS_FILENAME));
let _ = fs::remove_file(&config_path);
let _ = fs::remove_dir_all(&config_dir);
}
#[test]
fn allocate_default_ids_increments_next_dynamic_id() {
let mut status = DynamicStatusFile {
version: DYNAMIC_STATUS_VERSION,
next_dynamic_id: 42,
circuits: Vec::new(),
};
let first = status.allocate_default_ids(None, None);
let second = status.allocate_default_ids(None, None);
assert_eq!(first.0, "Dynamic 42");
assert_eq!(first.1, "Dynamic 42");
assert_eq!(second.0, "Dynamic 43");
assert_eq!(second.1, "Dynamic 43");
assert_eq!(status.next_dynamic_id, 44);
}
#[test]
fn to_shaped_device_sets_hashes_from_ids() {
let record = DynamicCircuitRecord {
circuit_id: "Dynamic 42".to_string(),
device_id: "Dynamic 42".to_string(),
circuit_name: Some("Transient Subscriber".to_string()),
device_name: Some("Transient CPE".to_string()),
attachment: DynamicAttachmentTarget {
node_id: Some("tower-a-sector-3".to_string()),
node_name: "Tower A / Sector 3".to_string(),
},
mac: Some("aa:bb:cc:dd:ee:ff".to_string()),
ip_cidrs: vec!["192.0.2.10/32".to_string(), "2001:db8::1234/128".to_string()],
download_min_mbps: 25.0,
upload_min_mbps: 5.0,
download_max_mbps: 150.0,
upload_max_mbps: 30.0,
sqm_override: Some("cake".to_string()),
ttl_seconds: 3600,
created: 1_775_606_400,
last_seen_at: 1_775_608_200,
expires_at: 1_775_611_800,
};
let device = record.to_shaped_device().expect("to_shaped_device");
assert_eq!(device.circuit_hash, lqos_utils::hash_to_i64(&device.circuit_id));
}
#[test]
fn to_shaped_device_ipv4_cidr_uses_ipv6_mapped_prefix() {
let record = DynamicCircuitRecord {
circuit_id: "Dynamic 7".to_string(),
device_id: "Dynamic 7".to_string(),
circuit_name: None,
device_name: None,
attachment: DynamicAttachmentTarget {
node_id: None,
node_name: "Orphans".to_string(),
},
mac: None,
ip_cidrs: vec!["192.0.2.10/24".to_string()],
download_min_mbps: 10.0,
upload_min_mbps: 10.0,
download_max_mbps: 20.0,
upload_max_mbps: 20.0,
sqm_override: None,
ttl_seconds: 3600,
created: 1,
last_seen_at: 1,
expires_at: 2,
};
let device = record.to_shaped_device().expect("to_shaped_device");
let ipv6_list = device.to_ipv6_list();
assert_eq!(ipv6_list.len(), 1);
let (mapped, prefix) = ipv6_list[0];
assert_eq!(mapped, std::net::Ipv4Addr::new(192, 0, 2, 10).to_ipv6_mapped());
assert_eq!(prefix, 120);
}
#[test]
fn to_shaped_device_invalid_ip_reports_offending_string() {
let record = DynamicCircuitRecord {
circuit_id: "Dynamic 99".to_string(),
device_id: "Dynamic 99".to_string(),
circuit_name: None,
device_name: None,
attachment: DynamicAttachmentTarget {
node_id: None,
node_name: "Orphans".to_string(),
},
mac: None,
ip_cidrs: vec!["not-an-ip".to_string()],
download_min_mbps: 10.0,
upload_min_mbps: 10.0,
download_max_mbps: 20.0,
upload_max_mbps: 20.0,
sqm_override: None,
ttl_seconds: 3600,
created: 1,
last_seen_at: 1,
expires_at: 2,
};
let error = record.to_shaped_device().expect_err("invalid CIDR should fail");
let message = error.to_string();
assert!(message.contains("not-an-ip"));
}
#[test]
fn example_fixture_parses_ttl_fields() {
let parsed: DynamicStatusFile =
serde_json::from_str(include_str!("testdata/dynamic_status.example.json"))
.expect("parse example fixture");
assert_eq!(parsed.version, DYNAMIC_STATUS_VERSION);
assert_eq!(parsed.circuits.len(), 1);
assert_eq!(parsed.circuits[0].ttl_seconds, 3600);
assert_eq!(parsed.circuits[0].last_seen_at, 1_775_608_200);
assert_eq!(parsed.circuits[0].expires_at, 1_775_611_800);
}
#[test]
fn legacy_ttl_field_names_still_parse() {
let parsed: DynamicStatusFile = serde_json::from_str(
r#"{
"version": 1,
"next_dynamic_id": 2,
"circuits": [
{
"circuit_id": "Dynamic 1",
"device_id": "Dynamic 1",
"attachment": { "node_id": null, "node_name": "AP-1" },
"mac": null,
"ip_cidrs": ["100.64.0.10/32"],
"download_min_mbps": 25.0,
"upload_min_mbps": 10.0,
"download_max_mbps": 50.0,
"upload_max_mbps": 20.0,
"sqm_override": null,
"ttl_seconds": 3600,
"created": 1,
"last_seen": 2,
"expires": 3602
}
]
}"#,
)
.expect("parse legacy ttl field names");
assert_eq!(parsed.circuits[0].last_seen_at, 2);
assert_eq!(parsed.circuits[0].expires_at, 3602);
}
#[test]
fn load_or_default_rejects_unsupported_version() {
let _guard = TEST_ENV_LOCK.lock().expect("lock test env");
clear_cached_config();
let config_dir = unique_test_dir("dynamic-status-bad-version");
let config_path = config_dir.join("lqos.conf");
fs::create_dir_all(&config_dir).expect("create config dir");
let mut config = Config::default();
config.lqos_directory = config_dir.to_string_lossy().to_string();
let config_toml = toml::to_string(&config).expect("serialize config");
fs::write(&config_path, config_toml).expect("write config");
let old_lqos_config = std::env::var_os("LQOS_CONFIG");
let old_lqos_directory = std::env::var_os("LQOS_DIRECTORY");
unsafe {
std::env::set_var("LQOS_CONFIG", &config_path);
std::env::set_var("LQOS_DIRECTORY", &config_dir);
}
let status_path = config_dir.join(DYNAMIC_STATUS_FILENAME);
fs::write(
&status_path,
r#"{"version":99,"next_dynamic_id":1,"circuits":[]}"#,
)
.expect("write dynamic status");
let error = DynamicStatusFile::load_or_default().expect_err("unsupported version");
assert!(matches!(error, super::DynamicStatusError::InvalidRecord { .. }));
match old_lqos_config {
Some(value) => unsafe { std::env::set_var("LQOS_CONFIG", value) },
None => unsafe { std::env::remove_var("LQOS_CONFIG") },
}
match old_lqos_directory {
Some(value) => unsafe { std::env::set_var("LQOS_DIRECTORY", value) },
None => unsafe { std::env::remove_var("LQOS_DIRECTORY") },
}
clear_cached_config();
let _ = fs::remove_file(&status_path);
let _ = fs::remove_file(&config_path);
let _ = fs::remove_dir_all(&config_dir);
}
}