diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 9e336d69..35213d2f 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -58,6 +58,7 @@ daddr datacenter davidanson DDCE +DDTHH debbuild DEBHELPER debian @@ -176,6 +177,7 @@ MFC microsoftcblmariner microsoftlosangeles microsoftwindowsdesktop +mmm mnt msasn msp @@ -271,6 +273,7 @@ spellright splitn SRPMS SSRF +SSZ stackoverflow stdbool stdint diff --git a/proxy_agent_extension/src/service_main.rs b/proxy_agent_extension/src/service_main.rs index 4ad85e32..120ca171 100644 --- a/proxy_agent_extension/src/service_main.rs +++ b/proxy_agent_extension/src/service_main.rs @@ -921,6 +921,12 @@ mod tests { proxyConnectionSummary: vec![proxy_connection_summary_obj], failedAuthenticateSummary: vec![proxy_failedAuthenticateSummary_obj], }; + let result = toplevel_status.get_status_timestamp(); + assert!( + result.is_ok(), + "Status timestamp parse expected Ok result, got Err: {:?}", + result.err() + ); let mut status = StatusObj { name: constants::PLUGIN_NAME.to_string(), diff --git a/proxy_agent_shared/Cargo.toml b/proxy_agent_shared/Cargo.toml index dcbc9b07..182100b3 100644 --- a/proxy_agent_shared/Cargo.toml +++ b/proxy_agent_shared/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] concurrent-queue = "2.1.0" # for event queue once_cell = "1.17.0" # use Lazy -time = { version = "0.3.30", features = ["formatting"] } +time = { version = "0.3.30", features = ["formatting", "parsing"] } thread-id = "4.0.0" serde = "1.0.152" serde_derive = "1.0.152" diff --git a/proxy_agent_shared/src/error.rs b/proxy_agent_shared/src/error.rs index 44cc2749..b933a999 100644 --- a/proxy_agent_shared/src/error.rs +++ b/proxy_agent_shared/src/error.rs @@ -53,6 +53,9 @@ pub enum Error { #[error("Failed to receive '{0}' action response with error {1}")] RecvError(String, tokio::sync::oneshot::error::RecvError), + + #[error("Parse datetime string error: {0}")] + ParseDateTimeStringError(String), } #[derive(Debug, thiserror::Error)] diff --git a/proxy_agent_shared/src/misc_helpers.rs b/proxy_agent_shared/src/misc_helpers.rs index e984734b..d8631098 100644 --- a/proxy_agent_shared/src/misc_helpers.rs +++ b/proxy_agent_shared/src/misc_helpers.rs @@ -13,7 +13,7 @@ use std::{ process::Command, }; use thread_id; -use time::{format_description, OffsetDateTime}; +use time::{format_description, OffsetDateTime, PrimitiveDateTime}; #[cfg(windows)] use super::windows; @@ -57,6 +57,54 @@ pub fn get_date_time_unix_nano() -> i128 { OffsetDateTime::now_utc().unix_timestamp_nanos() } +/// Parse a datetime string to OffsetDateTime (UTC) +/// Supports multiple formats: +/// - ISO 8601 with/without 'Z': "YYYY-MM-DDTHH:MM:SS" or "YYYY-MM-DDTHH:MM:SSZ" +/// - With milliseconds: "YYYY-MM-DDTHH:MM:SS.mmm" +/// # Arguments +/// * `datetime_str` - A datetime string to parse +/// # Returns +/// A Result containing the parsed OffsetDateTime (UTC) or an error if parsing fails +/// # Example +/// ```rust +/// use proxy_agent_shared::misc_helpers; +/// let datetime1 = misc_helpers::parse_date_time_string("2024-01-15T10:30:45Z").unwrap(); +/// let datetime2 = misc_helpers::parse_date_time_string("2024-01-15T10:30:45").unwrap(); +/// let datetime3 = misc_helpers::parse_date_time_string("2024-01-15T10:30:45.123").unwrap(); +/// ``` +pub fn parse_date_time_string(datetime_str: &str) -> Result { + // Remove the 'Z' suffix if present + let datetime_str_trimmed = datetime_str.trim_end_matches('Z'); + + // Try parsing with milliseconds first + let date_format_with_millis = + format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond]") + .map_err(|e| { + Error::ParseDateTimeStringError(format!("Failed to parse date format: {e}")) + })?; + + if let Ok(primitive_datetime) = + PrimitiveDateTime::parse(datetime_str_trimmed, &date_format_with_millis) + { + return Ok(primitive_datetime.assume_utc()); + } + + // Fall back to parsing without milliseconds + let date_format = format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second]") + .map_err(|e| { + Error::ParseDateTimeStringError(format!("Failed to parse date format: {e}")) + })?; + + let primitive_datetime = + PrimitiveDateTime::parse(datetime_str_trimmed, &date_format).map_err(|e| { + Error::ParseDateTimeStringError(format!( + "Failed to parse datetime string '{datetime_str}': {e}" + )) + })?; + + Ok(primitive_datetime.assume_utc()) +} + pub fn try_create_folder(dir: &Path) -> Result<()> { match dir.try_exists() { Ok(exists) => { @@ -654,4 +702,76 @@ mod tests { ); } } + + #[test] + fn parse_date_time_string_test() { + // Test parsing with milliseconds + let datetime_str = "2024-01-15T10:30:45.123"; + let result = super::parse_date_time_string(datetime_str); + assert!( + result.is_ok(), + "Failed to parse datetime string with milliseconds" + ); + + let datetime = result.unwrap(); + assert_eq!(datetime.year(), 2024); + assert_eq!(datetime.month() as u8, 1); + assert_eq!(datetime.day(), 15); + assert_eq!(datetime.hour(), 10); + assert_eq!(datetime.minute(), 30); + assert_eq!(datetime.second(), 45); + assert_eq!(datetime.millisecond(), 123); + + // Test parsing with 'Z' suffix + let datetime_str = "2024-01-15T10:30:45Z"; + let result = super::parse_date_time_string(datetime_str); + assert!( + result.is_ok(), + "Failed to parse datetime string with Z suffix" + ); + + let datetime = result.unwrap(); + assert_eq!(datetime.year(), 2024); + assert_eq!(datetime.month() as u8, 1); + assert_eq!(datetime.day(), 15); + assert_eq!(datetime.hour(), 10); + assert_eq!(datetime.minute(), 30); + assert_eq!(datetime.second(), 45); + + // Test parsing without 'Z' suffix + let datetime_str_without_z = "2024-01-15T10:30:45"; + let result = super::parse_date_time_string(datetime_str_without_z); + assert!(result.is_ok(), "Should parse datetime string without 'Z'"); + + // Test round-trip with milliseconds format + let original_datetime_str = super::get_date_time_string_with_milliseconds(); + let result = super::parse_date_time_string(&original_datetime_str); + assert!( + result.is_ok(), + "Failed to parse datetime string with milliseconds" + ); + + // Test round-trip with standard format + let original_datetime_str = super::get_date_time_string(); + let result = super::parse_date_time_string(&original_datetime_str); + assert!( + result.is_ok(), + "Failed to parse datetime string without milliseconds" + ); + + // Test invalid format + let invalid_datetime_str = "2024-01-15 10:30:45"; // space instead of 'T' + let result = super::parse_date_time_string(invalid_datetime_str); + assert!( + result.is_err(), + "Should fail to parse invalid datetime string" + ); + + let invalid_datetime_str = "2024-01-15T10:30"; // without seconds + let result = super::parse_date_time_string(invalid_datetime_str); + assert!( + result.is_err(), + "Should fail to parse invalid datetime string" + ); + } } diff --git a/proxy_agent_shared/src/proxy_agent_aggregate_status.rs b/proxy_agent_shared/src/proxy_agent_aggregate_status.rs index 5a3f2e6a..6a6a2971 100644 --- a/proxy_agent_shared/src/proxy_agent_aggregate_status.rs +++ b/proxy_agent_shared/src/proxy_agent_aggregate_status.rs @@ -3,6 +3,7 @@ use crate::misc_helpers; use serde_derive::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; +use time::OffsetDateTime; #[cfg(windows)] const PROXY_AGENT_AGGREGATE_STATUS_FOLDER: &str = "%SYSTEMDRIVE%\\WindowsAzure\\ProxyAgent\\Logs\\"; @@ -88,3 +89,9 @@ pub struct GuestProxyAgentAggregateStatus { pub proxyConnectionSummary: Vec, pub failedAuthenticateSummary: Vec, } + +impl GuestProxyAgentAggregateStatus { + pub fn get_status_timestamp(&self) -> crate::result::Result { + misc_helpers::parse_date_time_string(&self.timestamp) + } +}