Skip to main content

windows_erg/process/
processes.rs

1//! Core Process type and basic operations.
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5use std::sync::OnceLock;
6use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT};
7use windows::Win32::Storage::FileSystem::QueryDosDeviceW;
8use windows::Win32::System::ProcessStatus::GetProcessImageFileNameW;
9use windows::Win32::System::Threading::{
10    GetCurrentProcess, GetExitCodeProcess, OpenProcess, PROCESS_QUERY_INFORMATION,
11    PROCESS_TERMINATE, TerminateProcess, WaitForSingleObject,
12};
13use windows::core::PCWSTR;
14
15use super::types::{ProcessAccess, ProcessId};
16use crate::error::{Error, ProcessError, ProcessOpenError, Result};
17use crate::utils::to_utf16_nul;
18use crate::wait::Wait;
19
20// STILL_ACTIVE exit code constant
21const STILL_ACTIVE: u32 = 259;
22const DEVICE_PREFIX: &[u16] = &[92, 68, 101, 118, 105, 99, 101, 92];
23
24/// Cache for device path to drive letter mappings
25/// Maps \Device\HarddiskVolumeX to C:, D:, etc.
26static DEVICE_PATH_CACHE: OnceLock<HashMap<Vec<u16>, char>> = OnceLock::new();
27
28/// Initialize the device path cache by querying all drives (A-Z)
29fn init_device_path_cache() -> HashMap<Vec<u16>, char> {
30    let mut cache = HashMap::new();
31    let mut device_path_buffer = vec![0u16; 32768];
32
33    for drive_char in 'A'..='Z' {
34        let drive = format!("{}:", drive_char);
35        let drive_wide = to_utf16_nul(&drive);
36
37        // QueryDosDeviceW returns the device path for the drive
38
39        let len =
40            unsafe { QueryDosDeviceW(PCWSTR(drive_wide.as_ptr()), Some(&mut device_path_buffer)) };
41
42        if len > 0 {
43            let mut device_path_vec: Vec<u16> = device_path_buffer[..len as usize].to_vec();
44            // Trim trailing null terminators
45            while device_path_vec.last() == Some(&0) {
46                device_path_vec.pop();
47            }
48            cache.insert(device_path_vec, drive_char);
49        }
50    }
51
52    cache
53}
54
55/// Convert device path directly from u16 buffer, minimizing allocations
56/// Parses device path boundaries in u16 form before converting to String
57fn device_path_to_drive_path_u16(buffer_u16: &[u16]) -> String {
58    // Quick length check
59    if buffer_u16.is_empty() {
60        return String::new();
61    }
62
63    // Check if starts with \Device\ (in u16 form)
64    const BACKSLASH: u16 = b'\\' as u16;
65    if buffer_u16[0] != BACKSLASH || buffer_u16.len() < 8 {
66        // Not a device path - convert full buffer
67        let path_str = String::from_utf16_lossy(buffer_u16);
68        return path_str;
69    }
70
71    // Check for "\Device\" prefix
72    if buffer_u16.len() < DEVICE_PREFIX.len()
73        || !buffer_u16[..DEVICE_PREFIX.len()].eq(DEVICE_PREFIX)
74    {
75        let path_str = String::from_utf16_lossy(buffer_u16);
76        return path_str;
77    }
78
79    // Find the end of device root (next backslash after \Device\HarddiskVolumeX)
80    let mut device_root_end = DEVICE_PREFIX.len();
81    while device_root_end < buffer_u16.len() && buffer_u16[device_root_end] != BACKSLASH {
82        device_root_end += 1;
83    }
84
85    // Convert only the device root part to String for HashMap lookup
86    let cache = DEVICE_PATH_CACHE.get_or_init(init_device_path_cache);
87
88    if let Some(&drive_char) = cache.get(&buffer_u16[..device_root_end]) {
89        // Found mapping - build result efficiently
90        if device_root_end >= buffer_u16.len() {
91            // Device root is the entire path
92            return format!("{}:\\", drive_char);
93        }
94
95        // Has path after device root - convert rest of path
96        let mut rest_str = String::with_capacity(device_root_end + 3);
97        rest_str.push(drive_char);
98        rest_str.push_str(":\\");
99        let rest_slice = &buffer_u16[device_root_end + 1..];
100        for c in char::decode_utf16(rest_slice.iter().copied()).flatten() {
101            rest_str.push(c);
102        }
103        return rest_str;
104    }
105
106    // No mapping found - return full path converted to String
107    String::from_utf16_lossy(buffer_u16)
108}
109
110/// A handle to a Windows process.
111pub struct Process {
112    handle: HANDLE,
113    pid: ProcessId,
114    close_on_drop: bool,
115}
116
117impl Process {
118    /// Open a process with default access (query information).
119    pub fn open(pid: ProcessId) -> Result<Self> {
120        Self::open_with_access(pid, ProcessAccess::QueryInformation)
121    }
122
123    /// Open a process with specific access rights.
124    pub fn open_with_access(pid: ProcessId, access: ProcessAccess) -> Result<Self> {
125        let handle =
126            unsafe { OpenProcess(access.to_windows(), false, pid.as_u32()) }.map_err(|e| {
127                Error::Process(ProcessError::OpenFailed(ProcessOpenError::with_code(
128                    pid.as_u32(),
129                    "Failed to open process",
130                    e.code().0,
131                )))
132            })?;
133
134        Ok(Process {
135            handle,
136            pid,
137            close_on_drop: true,
138        })
139    }
140
141    /// Get a pseudo-handle to the current process.
142    ///
143    /// This handle does not need to be closed and is valid for the lifetime of the process.
144    pub fn current() -> Self {
145        Process {
146            handle: unsafe { GetCurrentProcess() },
147            pid: ProcessId::new(std::process::id()),
148            close_on_drop: false,
149        }
150    }
151
152    /// Open the same process with additional access rights.
153    ///
154    /// This is useful when you have a process handle but need higher privileges
155    /// (e.g., to read/write memory or terminate the process).
156    ///
157    /// # Example
158    /// ```ignore
159    /// let process = Process::open(pid)?;
160    /// // Need to read memory - upgrade to VmRead access
161    /// let process_with_vm_read = process.with_access(ProcessAccess::VmRead)?;
162    /// ```
163    pub fn with_access(&self, access: ProcessAccess) -> Result<Self> {
164        Self::open_with_access(self.pid, access)
165    }
166
167    /// Get the process ID.
168    pub fn id(&self) -> ProcessId {
169        self.pid
170    }
171
172    /// Get the process name (executable file name without path).
173    pub fn name(&self) -> Result<String> {
174        let mut buffer = Vec::with_capacity(260);
175        self.name_with_buffer(&mut buffer)
176    }
177
178    /// Get the process name using a reusable output buffer.
179    pub fn name_with_buffer(&self, out_buffer: &mut Vec<u8>) -> Result<String> {
180        let path = self.path_with_buffer(out_buffer)?;
181        Ok(path
182            .file_name()
183            .and_then(|s| s.to_str())
184            .unwrap_or("")
185            .to_string())
186    }
187
188    /// Get the full path to the process executable.
189    pub fn path(&self) -> Result<PathBuf> {
190        let mut buffer = Vec::with_capacity(260);
191        self.path_with_buffer(&mut buffer)
192    }
193
194    /// Get the full path to the process executable using a reusable output buffer.
195    pub fn path_with_buffer(&self, out_buffer: &mut Vec<u8>) -> Result<PathBuf> {
196        // Ensure buffer has capacity (1024 bytes = 512 u16 chars)
197        out_buffer.clear();
198        if out_buffer.capacity() < 1024 {
199            out_buffer.reserve(1024);
200        }
201        unsafe {
202            out_buffer.set_len(1024);
203        }
204
205        let buffer_u16 = unsafe {
206            std::slice::from_raw_parts_mut(
207                out_buffer.as_mut_ptr() as *mut u16,
208                out_buffer.len() / 2,
209            )
210        };
211
212        let len = unsafe { GetProcessImageFileNameW(self.handle, buffer_u16) } as usize;
213
214        if len == 0 {
215            return Err(Error::Process(ProcessError::OpenFailed(
216                ProcessOpenError::new(self.pid.as_u32(), "Failed to get process image path"),
217            )));
218        }
219
220        // Convert device path directly from u16 buffer, avoiding intermediate full string conversion
221        let path = device_path_to_drive_path_u16(&buffer_u16[..len]);
222
223        Ok(PathBuf::from(path))
224    }
225
226    /// Check if the process is still running.
227    pub fn is_running(&self) -> Result<bool> {
228        match self.exit_code() {
229            Ok(Some(_)) => Ok(false),
230            Ok(None) => Ok(true),
231            Err(e) => Err(e),
232        }
233    }
234
235    /// Get the exit code of the process, if it has exited.
236    ///
237    /// Returns `None` if the process is still running.
238    pub fn exit_code(&self) -> Result<Option<u32>> {
239        let exit_code = self.get_exit_code_value()?;
240
241        if exit_code == STILL_ACTIVE {
242            Ok(None)
243        } else {
244            Ok(Some(exit_code))
245        }
246    }
247
248    /// Wait until this process exits and return its final exit code.
249    pub fn wait_for_exit(&self) -> Result<u32> {
250        let wait_result = unsafe { WaitForSingleObject(self.handle, u32::MAX) };
251        if wait_result == WAIT_OBJECT_0 {
252            let exit_code = self.get_exit_code_value()?;
253            if exit_code == STILL_ACTIVE {
254                return Err(Error::Process(ProcessError::OpenFailed(
255                    ProcessOpenError::new(
256                        self.pid.as_u32(),
257                        "Process wait completed but exit code is still active",
258                    ),
259                )));
260            }
261            return Ok(exit_code);
262        }
263
264        if wait_result == WAIT_FAILED {
265            return Err(Error::Process(ProcessError::OpenFailed(
266                ProcessOpenError::new(self.pid.as_u32(), "Failed to wait for process exit"),
267            )));
268        }
269
270        Err(Error::Process(ProcessError::OpenFailed(
271            ProcessOpenError::new(
272                self.pid.as_u32(),
273                "Unexpected wait result while waiting for process exit",
274            ),
275        )))
276    }
277
278    /// Wait until this process exits or timeout elapses.
279    ///
280    /// Returns `Ok(Some(code))` when the process exits, `Ok(None)` on timeout.
281    pub fn wait_for_exit_timeout(&self, timeout: std::time::Duration) -> Result<Option<u32>> {
282        let wait_result = unsafe {
283            WaitForSingleObject(
284                self.handle,
285                timeout.as_millis().min(u32::MAX as u128) as u32,
286            )
287        };
288
289        if wait_result == WAIT_TIMEOUT {
290            return Ok(None);
291        }
292
293        if wait_result == WAIT_OBJECT_0 {
294            let exit_code = self.get_exit_code_value()?;
295            if exit_code == STILL_ACTIVE {
296                return Err(Error::Process(ProcessError::OpenFailed(
297                    ProcessOpenError::new(
298                        self.pid.as_u32(),
299                        "Process wait completed but exit code is still active",
300                    ),
301                )));
302            }
303            return Ok(Some(exit_code));
304        }
305
306        if wait_result == WAIT_FAILED {
307            return Err(Error::Process(ProcessError::OpenFailed(
308                ProcessOpenError::new(
309                    self.pid.as_u32(),
310                    "Failed to wait for process exit with timeout",
311                ),
312            )));
313        }
314
315        Err(Error::Process(ProcessError::OpenFailed(
316            ProcessOpenError::new(
317                self.pid.as_u32(),
318                "Unexpected wait result while waiting for process exit",
319            ),
320        )))
321    }
322
323    /// Borrow this process handle as a [`Wait`] object.
324    ///
325    /// The returned wait object does not own the process handle and will not close it on drop.
326    pub fn as_wait(&self) -> Wait {
327        Wait::from_handle_borrowed(self.handle)
328    }
329
330    /// Terminate the process with exit code 1.
331    pub fn kill(&self) -> Result<()> {
332        self.terminate(1)
333    }
334
335    /// Terminate the process with a specific exit code.
336    pub fn terminate(&self, exit_code: u32) -> Result<()> {
337        unsafe { TerminateProcess(self.handle, exit_code) }.map_err(|e| {
338            Error::Process(ProcessError::OpenFailed(ProcessOpenError::with_code(
339                self.pid.as_u32(),
340                "Failed to terminate process",
341                e.code().0,
342            )))
343        })
344    }
345
346    /// Kill a process by ID (convenience method).
347    pub fn kill_by_id(pid: ProcessId) -> Result<()> {
348        let process = Process::open_with_access(
349            pid,
350            ProcessAccess::Custom(PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION),
351        )?;
352        process.kill()
353    }
354
355    /// Get the raw Windows handle.
356    ///
357    /// # Safety
358    ///
359    /// The handle must not outlive the Process instance.
360    pub unsafe fn as_raw_handle(&self) -> HANDLE {
361        self.handle
362    }
363
364    fn get_exit_code_value(&self) -> Result<u32> {
365        let mut exit_code = 0u32;
366        unsafe { GetExitCodeProcess(self.handle, &mut exit_code) }.map_err(|e| {
367            Error::Process(ProcessError::OpenFailed(ProcessOpenError::with_code(
368                self.pid.as_u32(),
369                "Failed to get exit code",
370                e.code().0,
371            )))
372        })?;
373        Ok(exit_code)
374    }
375}
376
377impl Drop for Process {
378    fn drop(&mut self) {
379        if self.close_on_drop {
380            unsafe {
381                let _ = CloseHandle(self.handle);
382            }
383        }
384    }
385}
386
387// Safety: HANDLE can be sent between threads
388unsafe impl Send for Process {}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    // Process API tests
395    #[test]
396    fn test_process_current() {
397        // Test getting current process
398        let current = Process::current();
399        assert_eq!(current.id().as_u32(), std::process::id());
400    }
401
402    #[test]
403    fn test_process_with_access_same_pid() {
404        // Test that with_access preserves the process ID
405        let current = Process::current();
406        let original_pid = current.id();
407
408        // This should fail since we can't open the current process normally,
409        // but we're testing the API, not the result
410        let _ = current.with_access(ProcessAccess::QueryInformation);
411
412        // PID should remain the same
413        assert_eq!(current.id(), original_pid);
414    }
415
416    #[test]
417    fn test_process_with_access_different_rights() {
418        // Test that with_access is callable with different access types
419        let current = Process::current();
420
421        // These calls may fail, but the API should work
422        let _ = current.with_access(ProcessAccess::VmRead);
423        let _ = current.with_access(ProcessAccess::Terminate);
424        let _ = current.with_access(ProcessAccess::AllAccess);
425
426        // Process should still be valid
427        assert_eq!(current.id().as_u32(), std::process::id());
428    }
429
430    // Device path conversion tests
431    #[test]
432    fn test_device_path_to_drive_path_passthrough_non_device_path() {
433        // Non-device paths should pass through unchanged
434        let path = "C:\\Windows\\System32\\file.exe";
435        let u16_path: Vec<u16> = path.encode_utf16().collect();
436        let result = device_path_to_drive_path_u16(&u16_path);
437        assert_eq!(result, path);
438    }
439
440    #[test]
441    fn test_device_path_to_drive_path_initializes_cache() {
442        // First call should initialize the cache
443        let path = r"\Device\HarddiskVolume1\Windows\System32\kernel32.dll";
444        let u16_path: Vec<u16> = path.encode_utf16().collect();
445        let result = device_path_to_drive_path_u16(&u16_path);
446
447        // Result should be non-empty (either converted or fallback to device path)
448        assert!(!result.is_empty(), "Should return a valid path");
449    }
450
451    #[test]
452    fn test_device_path_to_drive_path_consistent_mapping() {
453        // Same device path should always map to same result
454        let path1 = r"\Device\HarddiskVolume1\Windows\System32\kernel32.dll";
455        let path2 = r"\Device\HarddiskVolume1\Program Files\app.exe";
456
457        let u16_path1: Vec<u16> = path1.encode_utf16().collect();
458        let u16_path2: Vec<u16> = path2.encode_utf16().collect();
459
460        let result1 = device_path_to_drive_path_u16(&u16_path1);
461        let result2 = device_path_to_drive_path_u16(&u16_path2);
462
463        // Both should have consistent behavior (same conversion status)
464        let is_device_1 = result1.starts_with(r"\Device\");
465        let is_device_2 = result2.starts_with(r"\Device\");
466
467        assert_eq!(
468            is_device_1, is_device_2,
469            "Consistent mapping for same device"
470        );
471    }
472
473    #[test]
474    fn test_device_path_to_drive_path_root_path() {
475        // Device path without subdirectories should be processed
476        let path = r"\Device\HarddiskVolume1";
477        let u16_path: Vec<u16> = path.encode_utf16().collect();
478        let result = device_path_to_drive_path_u16(&u16_path);
479
480        assert!(!result.is_empty(), "Should return a valid path");
481    }
482
483    #[test]
484    fn test_device_path_to_drive_path_long_path() {
485        // Long device paths should be processed correctly
486        let path = r"\Device\HarddiskVolume1\Windows\System32\Drivers\etc\hosts";
487        let u16_path: Vec<u16> = path.encode_utf16().collect();
488        let result = device_path_to_drive_path_u16(&u16_path);
489
490        // Should handle the path properly (either convert or fallback)
491        assert!(
492            result.contains("hosts") || result.starts_with(r"\Device\"),
493            "Should process path correctly"
494        );
495    }
496
497    #[test]
498    fn test_device_path_to_drive_path_multiple_backslashes() {
499        // Paths with multiple directory levels
500        let path = r"\Device\HarddiskVolume2\Users\Admin\Documents\file.txt";
501        let u16_path: Vec<u16> = path.encode_utf16().collect();
502        let result = device_path_to_drive_path_u16(&u16_path);
503
504        // Should handle multipart path
505        assert!(!result.is_empty(), "Should return a valid path");
506    }
507
508    #[test]
509    fn test_device_path_to_drive_path_preserves_case() {
510        // Case information should be preserved
511        let path = r"\Device\HarddiskVolume1\Program Files\MyApp\config.ini";
512        let u16_path: Vec<u16> = path.encode_utf16().collect();
513        let result = device_path_to_drive_path_u16(&u16_path);
514
515        // Original path components should be present (case may vary)
516        assert!(
517            result.to_lowercase().contains("program files") || result.starts_with(r"\Device\"),
518            "Should handle case appropriately"
519        );
520    }
521
522    #[test]
523    fn test_init_device_path_cache_returns_valid_mappings() {
524        // Cache should contain valid mappings
525        let cache = init_device_path_cache();
526
527        // Should have at least one entry (the C: drive is almost always present)
528        assert!(!cache.is_empty(), "Cache should have entries");
529
530        // All values should be drive letters A-Z
531        for &drive_char in cache.values() {
532            assert!(
533                drive_char.is_ascii_uppercase(),
534                "Drive letter should be A-Z"
535            );
536        }
537    }
538
539    #[test]
540    fn test_init_device_path_cache_has_device_path_keys() {
541        // Cache keys should look like device paths
542        let cache = init_device_path_cache();
543
544        for key in cache.keys() {
545            assert!(
546                key.starts_with(DEVICE_PREFIX),
547                "Cache key should be a device path"
548            );
549        }
550    }
551
552    #[test]
553    fn test_device_path_cache_is_singleton() {
554        // Multiple accesses should return the same cache instance
555        let cache1 = DEVICE_PATH_CACHE.get_or_init(init_device_path_cache);
556        let cache2 = DEVICE_PATH_CACHE.get_or_init(init_device_path_cache);
557
558        // Should be the same object (pointer equality via reference)
559        assert_eq!(cache1.len(), cache2.len(), "Cache should be consistent");
560    }
561
562    #[test]
563    fn test_device_path_conversion_with_special_characters() {
564        // Paths with special characters should be processed
565        let path = r"\Device\HarddiskVolume1\Program Files (x86)\app.exe";
566        let u16_path: Vec<u16> = path.encode_utf16().collect();
567        let result = device_path_to_drive_path_u16(&u16_path);
568
569        assert!(!result.is_empty(), "Should handle special characters");
570    }
571
572    #[test]
573    fn test_device_path_unknown_device_fallback() {
574        // Unknown device paths should be handled gracefully
575        let path = r"\Device\HarddiskVolume999\unknown\path";
576        let u16_path: Vec<u16> = path.encode_utf16().collect();
577        let result = device_path_to_drive_path_u16(&u16_path);
578
579        // Should either be converted (if volume exists) or returned as-is
580        assert!(
581            result.contains("unknown") || result.starts_with(r"\Device\"),
582            "Should handle unknown device gracefully"
583        );
584    }
585
586    #[test]
587    fn test_device_path_empty_subdirectory() {
588        // Device path with trailing backslash
589        let path = r"\Device\HarddiskVolume1\";
590        let u16_path: Vec<u16> = path.encode_utf16().collect();
591        let result = device_path_to_drive_path_u16(&u16_path);
592
593        assert!(!result.is_empty(), "Should handle trailing backslash");
594    }
595
596    #[test]
597    fn test_device_path_c_drive_common_paths() {
598        // Common paths should process correctly
599        let paths = vec![
600            r"\Device\HarddiskVolume1\Windows\System32\kernel32.dll",
601            r"\Device\HarddiskVolume1\Program Files\app.exe",
602            r"\Device\HarddiskVolume1\Users\Admin\Desktop\file.txt",
603        ];
604
605        for path in paths {
606            let u16_path: Vec<u16> = path.encode_utf16().collect();
607            let result = device_path_to_drive_path_u16(&u16_path);
608
609            // Should return a non-empty path
610            assert!(!result.is_empty(), "Should process path without error");
611        }
612    }
613}