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
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ path = "src/main.rs"
anyhow = "1.0.98"
clap = { version = "4.5.38", features = ["derive"] }
log = "0.4.27"
colored = "3.0.0"

[dependencies.windows]
version = "0.61.1"
Expand Down
6 changes: 3 additions & 3 deletions src/handle_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ use anyhow::anyhow;
use log::debug;
use windows::{
Wdk::{
Foundation::{ObjectTypeInformation, OBJECT_INFORMATION_CLASS, OBJECT_NAME_INFORMATION},
Foundation::{OBJECT_INFORMATION_CLASS, OBJECT_NAME_INFORMATION, ObjectTypeInformation},
System::SystemInformation::SYSTEM_INFORMATION_CLASS,
},
Win32::{
Foundation::{
DuplicateHandle, DUPLICATE_SAME_ACCESS, ERROR_ACCESS_DENIED, ERROR_INVALID_HANDLE,
DUPLICATE_SAME_ACCESS, DuplicateHandle, ERROR_ACCESS_DENIED, ERROR_INVALID_HANDLE,
ERROR_NOT_SUPPORTED, HANDLE,
},
Storage::FileSystem::{GetFileType, FILE_TYPE_DISK},
Storage::FileSystem::{FILE_TYPE_DISK, GetFileType},
System::{
Threading::{GetCurrentProcess, OpenProcess, PROCESS_DUP_HANDLE},
WindowsProgramming::PUBLIC_OBJECT_TYPE_INFORMATION,
Expand Down
106 changes: 105 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use anyhow::Context;
use clap::Parser;
use colored::Colorize;
use std::collections::HashMap;
use std::io::{self, Write};
use std::time::Instant;

mod handle_ext;
Expand All @@ -21,6 +23,10 @@ struct Cli {
/// Path to the file you want to check for locks
#[arg(required = true)]
path: String,

/// Forcefully kill the processes locking the file (requires confirmation)
#[arg(short = 'k', long, default_value_t = false)]
kill: bool,
}

fn main() {
Expand All @@ -35,12 +41,74 @@ fn main() {
eprintln!("No locker found");
} else {
println!("Found {} locker(s):\n", results.len());
for (_, result) in results {
for result in results.values() {
println!("pid: {}", result.pid);
println!("name: {}", result.name);
println!("path: {}", result.path);
println!();
}

if cli.kill {
println!(
"{}",
"WARNING: You are about to attempt to KILL the process(es) listed above."
.bold()
.yellow()
);
println!(
"{}",
"This is a DESTRUCTIVE and UNRECOVERABLE operation that could lead to data loss or system instability."
.bold()
.red()
);
print!(
"{} ",
"Are you absolutely sure you want to proceed? (y/N):"
.bold()
.yellow()
);
io::stdout()
.flush()
.context("Failed to flush stdout")
.unwrap_or_else(|e| eprintln!("Error flushing stdout: {}", e));

let mut confirmation = String::new();
match io::stdin().read_line(&mut confirmation) {
Ok(_) => {
if confirmation.trim().eq_ignore_ascii_case("y") {
println!("Proceeding to kill processes...");
match kill_processes(&results) {
Ok(killed_count) => {
if killed_count > 0 {
println!(
"Successfully attempted to kill {} process(es).",
killed_count
);
} else {
println!("No processes were targeted or killed.");
}
if killed_count < results.len() {
println!(
"{}",
"Note: Some processes might not have been killed due to errors, lack of permissions, or if they already exited."
.yellow()
);
}
}
Err(e) => eprintln!(
"An error occurred during the kill process: {:?}",
e
),
}
} else {
println!("Operation cancelled by user.");
}
}
Err(e) => {
eprintln!("Failed to read user input: {:?}. Operation cancelled.", e);
}
}
}
}
}
Err(err) => {
Expand All @@ -51,6 +119,42 @@ fn main() {
println!("elapsed: {:.2}s", elapsed.as_secs_f64());
}

fn kill_processes(processes: &HashMap<u32, ProcessResult>) -> anyhow::Result<usize> {
let mut killed_count = 0;
if processes.is_empty() {
println!("No processes to kill.");
return Ok(0);
}

println!(
"{}",
"IMPORTANT: Attempting to terminate processes. This can have unintended consequences."
.bold()
.red()
);

for (pid, process_info) in processes {
println!(
"Attempting to kill process: PID {}, Name: '{}', Path: '{}'",
process_info.pid, process_info.name, process_info.path
);
match process_ext::kill_process_by_pid(*pid) {
Ok(_) => {
println!(
"Successfully sent termination signal to process PID {}.",
pid
);
killed_count += 1;
}
Err(e) => {
eprintln!("Failed to kill process PID {}: {:?}", pid, e);
// Ignore the error and continue
}
}
}
Ok(killed_count)
}

fn find_locker(cli: &Cli) -> anyhow::Result<HashMap<u32, ProcessResult>> {
let nt_path = path_ext::win32_path_to_nt_path(cli.path.to_string())
.with_context(|| "win32_path_to_nt_path failed")?;
Expand Down
6 changes: 3 additions & 3 deletions src/path_ext.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use anyhow::anyhow;
use windows::{
core::HSTRING,
Win32::Storage::FileSystem::{
CreateFileW, GetFileType, FILE_FLAG_BACKUP_SEMANTICS, FILE_SHARE_DELETE, FILE_SHARE_READ,
FILE_SHARE_WRITE, FILE_TYPE_DISK, OPEN_EXISTING,
CreateFileW, FILE_FLAG_BACKUP_SEMANTICS, FILE_SHARE_DELETE, FILE_SHARE_READ,
FILE_SHARE_WRITE, FILE_TYPE_DISK, GetFileType, OPEN_EXISTING,
},
core::HSTRING,
};

use crate::{handle_ext::handle_to_nt_path, safe_handle::SafeHandle};
Expand Down
78 changes: 51 additions & 27 deletions src/process_ext.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use windows::{
core::{Error, PWSTR},
Wdk::System::SystemInformation::SystemProcessInformation,
Win32::{
Foundation::{GetLastError, ERROR_INSUFFICIENT_BUFFER, HMODULE, MAX_PATH},
Foundation::{ERROR_INSUFFICIENT_BUFFER, GetLastError, HMODULE, MAX_PATH},
Security::{
GetTokenInformation, LookupAccountSidW, TokenUser, SID_NAME_USE, TOKEN_QUERY,
TOKEN_USER,
GetTokenInformation, LookupAccountSidW, SID_NAME_USE, TOKEN_QUERY, TOKEN_USER,
TokenUser,
},
System::{
ProcessStatus::{EnumProcessModules, GetModuleBaseNameW, GetModuleFileNameExW},
Threading::{
OpenProcess, OpenProcessToken, PROCESS_QUERY_INFORMATION,
PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ,
PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_TERMINATE, PROCESS_VM_READ,
TerminateProcess,
},
WindowsProgramming::SYSTEM_PROCESS_INFORMATION,
},
},
core::{Error, PWSTR},
};

use crate::safe_handle::SafeHandle;
Expand Down Expand Up @@ -107,15 +108,15 @@ pub fn enum_process_modules(pid: u32) -> anyhow::Result<Vec<String>> {
buffer.as_ptr().add((i * size_of_single_module) as usize) as *const HMODULE
)
};
let module_name = get_moudle_name(&safe_process_handle, Some(module))?;
let module_name = get_module_name(&safe_process_handle, Some(module))?;
let module_nt_path = path_ext::win32_path_to_nt_path(module_name)?;
moudle_nt_path_collection.push(module_nt_path);
}

Ok(moudle_nt_path_collection)
}

fn get_moudle_name(
fn get_module_name(
safe_process_handle: &SafeHandle,
module: Option<HMODULE>,
) -> anyhow::Result<String> {
Expand All @@ -137,7 +138,7 @@ fn get_moudle_name(
));
}

let module_name = String::from_utf16_lossy(&buffer);
let module_name = String::from_utf16_lossy(&buffer[..actual_len as usize]);
Ok(module_name)
}

Expand Down Expand Up @@ -206,51 +207,74 @@ pub fn _pid_to_user(pid: u32) -> anyhow::Result<(String, String)> {
}
}

// Allocate buffers and get user/domain names
// Allocate buffers and get user and domain
let mut user_buffer = vec![0u16; user_size as usize];
let mut domain_buffer = vec![0u16; domain_size as usize];

unsafe {
LookupAccountSidW(
None,
psid,
Some(PWSTR::from_raw(user_buffer.as_mut_ptr())),
Some(PWSTR(user_buffer.as_mut_ptr())),
&mut user_size,
Some(PWSTR::from_raw(domain_buffer.as_mut_ptr())),
Some(PWSTR(domain_buffer.as_mut_ptr())),
&mut domain_size,
&mut sid_name_use,
)?
}
)
.with_context(|| "LookupAccountSidW failed")?
};

let domain_name = String::from_utf16_lossy(&domain_buffer[..domain_size as usize]);
let user_name = String::from_utf16_lossy(&user_buffer[..user_size as usize]);
let user = String::from_utf16_lossy(&user_buffer[..user_size as usize]);
let domain = String::from_utf16_lossy(&domain_buffer[..domain_size as usize]);

Ok((domain_name, user_name))
Ok((user, domain))
}

pub fn pid_to_process_name(pid: u32) -> anyhow::Result<String> {
let process_handle =
unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid)? };
let process_handle = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ,
false,
pid,
)?
};
let safe_process_handle = SafeHandle::new(process_handle);

let mut buffer = vec![0u16; MAX_PATH as usize];
let len = unsafe { GetModuleBaseNameW(safe_process_handle.handle, None, &mut buffer) };
if len == 0 {
let actual_len = unsafe { GetModuleBaseNameW(safe_process_handle.handle, None, &mut buffer) };

if actual_len == 0 {
return Err(anyhow!(
"GetModuleBaseNameW failed, error: {}",
Error::from_win32()
));
}
let process_name = String::from_utf16_lossy(&buffer[..len as usize]);

let process_name = String::from_utf16_lossy(&buffer[..actual_len as usize]);
Ok(process_name)
}

pub fn pid_to_process_full_path(pid: u32) -> anyhow::Result<String> {
let process_handle =
unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid)? };
let process_handle = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ,
false,
pid,
)?
};
let safe_process_handle = SafeHandle::new(process_handle);

let process_full_path = get_moudle_name(&safe_process_handle, None)?;
Ok(process_full_path)
get_module_name(&safe_process_handle, None)
}

pub fn kill_process_by_pid(pid: u32) -> anyhow::Result<()> {
let process_handle = unsafe { OpenProcess(PROCESS_TERMINATE, false, pid)? };
let safe_process_handle = SafeHandle::new(process_handle);

unsafe {
TerminateProcess(safe_process_handle.handle, 1)
.context(format!("Failed to terminate process with PID: {}", pid))?
}
Ok(())
}

#[cfg(test)]
Expand Down