agentmux_common/
pagefile.rs1#[cfg(target_os = "windows")]
19const BYTES_PER_GB: f64 = 1_073_741_824.0;
20
21#[cfg(any(target_os = "windows", test))]
28#[derive(Debug, Clone, PartialEq)]
29struct PagingFileEntry {
30 drive_letter: Option<char>,
33 system_managed: bool,
34}
35
36#[cfg(any(target_os = "windows", test))]
42fn parse_paging_file_entry(line: &str) -> Option<PagingFileEntry> {
43 let mut parts = line.split_whitespace();
44 let path = parts.next()?;
45 let initial: u64 = parts.next()?.parse().ok()?;
46 let maximum: u64 = parts.next()?.parse().ok()?;
47 let drive_letter = path
48 .chars()
49 .next()
50 .filter(|c| c.is_ascii_alphabetic())
51 .map(|c| c.to_ascii_uppercase());
52 Some(PagingFileEntry {
53 drive_letter,
54 system_managed: initial == 0 && maximum == 0,
55 })
56}
57
58#[cfg(any(target_os = "windows", test))]
73fn resolve_pagefile_watch_target(entries: &[PagingFileEntry], system_drive: char) -> (char, bool) {
74 if entries.is_empty() {
75 return (system_drive, true);
76 }
77 for e in entries {
78 if e.system_managed {
79 return (e.drive_letter.unwrap_or(system_drive), true);
80 }
81 }
82 (system_drive, false)
83}
84
85#[cfg(target_os = "windows")]
91fn read_paging_files_registry() -> Vec<PagingFileEntry> {
92 use windows_sys::Win32::Foundation::ERROR_SUCCESS;
93 use windows_sys::Win32::System::Registry::{
94 RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY, HKEY_LOCAL_MACHINE, KEY_READ,
95 REG_MULTI_SZ,
96 };
97
98 let subkey: Vec<u16> =
99 "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Memory Management\0"
100 .encode_utf16()
101 .collect();
102 let value_name: Vec<u16> = "PagingFiles\0".encode_utf16().collect();
103
104 unsafe {
105 let mut hkey: HKEY = std::ptr::null_mut();
106 if RegOpenKeyExW(HKEY_LOCAL_MACHINE, subkey.as_ptr(), 0, KEY_READ, &mut hkey) as u32
107 != ERROR_SUCCESS
108 {
109 return Vec::new();
110 }
111
112 let mut buf_bytes: u32 = 0;
113 let mut value_type: u32 = 0;
114 let query1 = RegQueryValueExW(
115 hkey,
116 value_name.as_ptr(),
117 std::ptr::null(),
118 &mut value_type,
119 std::ptr::null_mut(),
120 &mut buf_bytes,
121 );
122 if query1 as u32 != ERROR_SUCCESS || value_type != REG_MULTI_SZ || buf_bytes == 0 {
123 RegCloseKey(hkey);
124 return Vec::new();
125 }
126
127 let mut buf: Vec<u16> = vec![0u16; buf_bytes as usize / 2];
128 let query2 = RegQueryValueExW(
129 hkey,
130 value_name.as_ptr(),
131 std::ptr::null(),
132 std::ptr::null_mut(),
133 buf.as_mut_ptr() as *mut u8,
134 &mut buf_bytes,
135 );
136 RegCloseKey(hkey);
137 if query2 as u32 != ERROR_SUCCESS {
138 return Vec::new();
139 }
140
141 buf.split(|&c| c == 0)
143 .filter(|s| !s.is_empty())
144 .filter_map(|s| parse_paging_file_entry(&String::from_utf16_lossy(s)))
145 .collect()
146 }
147}
148
149#[cfg(target_os = "windows")]
153pub fn drive_free_total_gb(drive_letter: char) -> Option<(f64, f64)> {
154 use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
155 let root: Vec<u16> = format!("{}:\\\0", drive_letter).encode_utf16().collect();
156 let mut free_available: u64 = 0;
157 let mut total: u64 = 0;
158 let ok = unsafe {
159 GetDiskFreeSpaceExW(
160 root.as_ptr(),
161 &mut free_available,
162 &mut total,
163 std::ptr::null_mut(),
164 )
165 };
166 if ok == 0 {
167 return None;
168 }
169 Some((
170 free_available as f64 / BYTES_PER_GB,
171 total as f64 / BYTES_PER_GB,
172 ))
173}
174
175#[cfg(target_os = "windows")]
182pub fn pagefile_watch_target() -> (char, bool) {
183 static TARGET: std::sync::OnceLock<(char, bool)> = std::sync::OnceLock::new();
184 *TARGET.get_or_init(|| {
185 let system_drive = std::env::var("SystemDrive")
186 .ok()
187 .and_then(|s| s.chars().next())
188 .map(|c| c.to_ascii_uppercase())
189 .unwrap_or('C');
190 let entries = read_paging_files_registry();
191 resolve_pagefile_watch_target(&entries, system_drive)
192 })
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn parses_system_managed_entry() {
201 let e = parse_paging_file_entry("C:\\pagefile.sys 0 0").unwrap();
202 assert_eq!(e.drive_letter, Some('C'));
203 assert!(e.system_managed);
204 }
205
206 #[test]
207 fn parses_fixed_size_entry() {
208 let e = parse_paging_file_entry("D:\\pagefile.sys 4096 8192").unwrap();
209 assert_eq!(e.drive_letter, Some('D'));
210 assert!(!e.system_managed);
211 }
212
213 #[test]
214 fn lowercases_drive_letter_normalized_to_upper() {
215 let e = parse_paging_file_entry("c:\\pagefile.sys 0 0").unwrap();
217 assert_eq!(e.drive_letter, Some('C'));
218 }
219
220 #[test]
221 fn malformed_line_returns_none() {
222 assert!(parse_paging_file_entry("").is_none());
223 assert!(parse_paging_file_entry("C:\\pagefile.sys").is_none());
224 assert!(parse_paging_file_entry("C:\\pagefile.sys notanumber 0").is_none());
225 }
226
227 #[test]
228 fn empty_entries_falls_back_to_system_drive_as_managed() {
229 let (drive, managed) = resolve_pagefile_watch_target(&[], 'C');
234 assert_eq!(drive, 'C');
235 assert!(managed);
236 }
237
238 #[test]
239 fn watches_the_first_system_managed_volume() {
240 let entries = vec![
241 PagingFileEntry { drive_letter: Some('D'), system_managed: false },
242 PagingFileEntry { drive_letter: Some('E'), system_managed: true },
243 ];
244 let (drive, managed) = resolve_pagefile_watch_target(&entries, 'C');
245 assert_eq!(drive, 'E');
246 assert!(managed);
247 }
248
249 #[test]
250 fn all_fixed_size_reports_not_managed_on_system_drive() {
251 let entries = vec![PagingFileEntry { drive_letter: Some('D'), system_managed: false }];
255 let (drive, managed) = resolve_pagefile_watch_target(&entries, 'C');
256 assert_eq!(drive, 'C');
257 assert!(!managed);
258 }
259}