Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
aab

Check warning on line 1 in .github/actions/spelling/expect.txt

View workflow job for this annotation

GitHub Actions / Check Spelling

Skipping `.github/actions/spelling/expect.txt` because it seems to have more noise (375) than unique words (1) (total: 376 / 1). (noisy-file)
AAFFBB
aarch
abe
Expand Down Expand Up @@ -58,6 +58,7 @@
datacenter
davidanson
DDCE
DDTHH
debbuild
DEBHELPER
debian
Expand Down Expand Up @@ -176,6 +177,7 @@
microsoftcblmariner
microsoftlosangeles
microsoftwindowsdesktop
mmm
mnt
msasn
msp
Expand Down Expand Up @@ -271,6 +273,7 @@
splitn
SRPMS
SSRF
SSZ
stackoverflow
stdbool
stdint
Expand Down Expand Up @@ -371,3 +374,3 @@
xxxx
xxxxxxxx
xxxxxxxxxxx
Expand Down
6 changes: 6 additions & 0 deletions proxy_agent_extension/src/service_main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion proxy_agent_shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions proxy_agent_shared/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
122 changes: 121 additions & 1 deletion proxy_agent_shared/src/misc_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<OffsetDateTime> {
// 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) => {
Expand Down Expand Up @@ -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"
);
}
}
7 changes: 7 additions & 0 deletions proxy_agent_shared/src/proxy_agent_aggregate_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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\\";
Expand Down Expand Up @@ -88,3 +89,9 @@ pub struct GuestProxyAgentAggregateStatus {
pub proxyConnectionSummary: Vec<ProxyConnectionSummary>,
pub failedAuthenticateSummary: Vec<ProxyConnectionSummary>,
}

impl GuestProxyAgentAggregateStatus {
pub fn get_status_timestamp(&self) -> crate::result::Result<OffsetDateTime> {
misc_helpers::parse_date_time_string(&self.timestamp)
}
}
Loading