Trim full path to binary from minitop

Summary:
On Windows, minitop should hide the absolute path to processes' binaries to make the process filename more visible.

There's a shlex crate that can parse Unix-like command lines, but I didn't see an equivalent for Windows. (The Unix command line grammar is different, and in particular shlex will eat unquoted backslashes that are legal on the Windows command line.) So here we use the `CommandLineToArgvW` win32 function to get the Windows equivalent.

After parsing the command line and stripping the full path from the executable name, for now we just naively join the arguments back together, which means binary names containing spaces won't be properly quoted. I may add this in a following diff.

Reviewed By: xavierd

Differential Revision: D43509750

fbshipit-source-id: d56535ded4984a8106f4a26dd5d1b54e2c60e297
This commit is contained in:
Mark Shroyer 2023-02-24 17:21:01 -08:00 committed by Facebook GitHub Bot
parent b36645215e
commit 8de61887bb
4 changed files with 237 additions and 24 deletions

View File

@ -11,7 +11,6 @@ use std::collections::BTreeMap;
use std::io::stdout;
use std::io::Stdout;
use std::io::Write;
use std::path::Path;
use std::time::Duration;
use std::time::Instant;
@ -39,7 +38,6 @@ use edenfs_utils::humantime::TimeUnit;
use edenfs_utils::path_from_bytes;
use futures::FutureExt;
use futures::StreamExt;
use shlex::quote;
use sysinfo::Pid;
use sysinfo::System;
use sysinfo::SystemExt;
@ -47,6 +45,10 @@ use thrift_types::edenfs::types::pid_t;
use thrift_types::edenfs::types::AccessCounts;
use thrift_types::edenfs::types::GetAccessCountsResult;
#[cfg(unix)]
use self::unix::trim_cmd_binary_path;
#[cfg(windows)]
use self::windows::trim_cmd_binary_path;
use crate::ExitCode;
#[derive(Parser, Debug)]
@ -111,29 +113,11 @@ impl GetAccessCountsResultExt for GetAccessCountsResult {
// extra empty string on the end
let cmd = cmd.trim_end_matches(char::from(0));
let mut parts: Vec<&str> = cmd.split(char::from(0)).collect();
let path = Path::new(parts[0]);
if path.is_absolute() {
parts[0] = path
.file_name()
.ok_or_else(|| anyhow!("cmd filename is missing"))?
.to_str()
.ok_or_else(|| anyhow!("cmd is not UTF-8"))?;
}
// Show only the binary's filename, not its full path.
let cmd = trim_cmd_binary_path(cmd)
.unwrap_or_else(|e| format!("{}: {}", UNKNOWN_COMMAND, e));
Ok(parts
.into_iter()
.enumerate()
.map(|(i, part)| {
if i == 0 {
// the first item is the cmd
String::from(part)
} else {
quote(part).into_owned()
}
})
.collect::<Vec<String>>()
.join(" "))
Ok(cmd)
}
None => Ok(String::from(UNKNOWN_COMMAND)),
}
@ -581,3 +565,96 @@ impl crate::Subcommand for MinitopCmd {
}
}
}
#[cfg(unix)]
mod unix {
use std::path::Path;
use anyhow::anyhow;
use anyhow::Result;
use shlex::quote;
pub fn trim_cmd_binary_path(cmd: &str) -> Result<String> {
let mut parts: Vec<&str> = cmd.split(char::from(0)).collect();
let path = Path::new(parts[0]);
if path.is_absolute() {
parts[0] = path
.file_name()
.ok_or_else(|| anyhow!("cmd filename is missing"))?
.to_str()
.ok_or_else(|| anyhow!("cmd is not UTF-8"))?;
}
Ok(parts
.into_iter()
.enumerate()
.map(|(i, part)| {
if i == 0 {
// the first item is the cmd
String::from(part)
} else {
quote(part).into_owned()
}
})
.collect::<Vec<String>>()
.join(" "))
}
}
#[cfg(windows)]
mod windows {
use std::ffi::OsStr;
use std::ffi::OsString;
use std::path::Path;
use anyhow::anyhow;
use anyhow::Error;
use anyhow::Result;
use edenfs_utils::winargv::command_line_to_argv;
pub fn trim_cmd_binary_path(cmd: &str) -> Result<String> {
let argv = command_line_to_argv(OsStr::new(cmd))?;
Ok(argv
.into_iter()
.enumerate()
.map(|part| {
match part {
(0, binary) => {
let filename = binary_filename_only(&binary)?;
Ok::<OsString, Error>(filename.to_owned())
}
(_, part) => Ok(part),
}?
.into_string()
.map_err(|_| anyhow!("failed string conversion"))
})
.collect::<Result<Vec<_>>>()?
.join(" "))
}
fn binary_filename_only(binary: &OsStr) -> Result<&OsStr> {
Ok(Path::new(binary)
.file_name()
.ok_or(anyhow!("cmd filename is missing"))?)
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use super::trim_cmd_binary_path;
#[test]
fn test_trim_cmd_binary_path() -> Result<()> {
assert_eq!(trim_cmd_binary_path("rustc.exe")?, "rustc.exe");
assert_eq!(trim_cmd_binary_path("\"rustc.exe\"")?, "rustc.exe");
assert_eq!(
trim_cmd_binary_path("\"C:\\Program Files\\foo\\bar.exe\" baz.txt")?,
"bar.exe baz.txt"
);
Ok(())
}
}
}

View File

@ -19,3 +19,6 @@ nix = "0.25"
[target.'cfg(target_os = "macos")'.dependencies]
nix = "0.25"
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["everything"] }

View File

@ -28,6 +28,9 @@ use tracing::trace;
pub mod humantime;
pub mod metadata;
#[cfg(windows)]
pub mod winargv;
pub fn path_from_bytes(bytes: &[u8]) -> Result<PathBuf> {
Ok(PathBuf::from(std::str::from_utf8(bytes).from_err()?))
}

View File

@ -0,0 +1,130 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/
#![cfg(windows)]
use std::ffi::OsStr;
use std::ffi::OsString;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::ffi::OsStringExt;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::Result;
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::shellapi::CommandLineToArgvW;
use winapi::um::winbase::LocalFree;
/// Parses a Windows command line into an argv vector of program name followed
/// by zero or more command-line arguments.
pub fn command_line_to_argv(command_line: &OsStr) -> Result<Vec<OsString>> {
if command_line.is_empty() {
// CommandLineToArgvW assumes the current executable file if passed an
// empty string, but we don't want that behavior.
return Ok(vec![]);
}
let command_line_w = command_line
.encode_wide()
.chain(Some(0))
.collect::<Vec<_>>();
let mut argv = Vec::<OsString>::new();
let mut num_args: i32 = 0;
let argv_w = LocalPtr(unsafe { CommandLineToArgvW(command_line_w.as_ptr(), &mut num_args) });
if argv_w.0.is_null() {
return Err(anyhow!("CommandLineToArgvW failed: {:?}", unsafe {
GetLastError()
}));
}
for i in 0..num_args {
let arg_offset: isize = i.try_into()?;
let arg_w = unsafe { *argv_w.0.offset(arg_offset) } as *const u16;
let arg_w_slice = unsafe { null_terminated_slice(arg_w)? };
argv.push(OsString::from_wide(arg_w_slice));
}
Ok(argv)
}
/// Owned pointer that needs to be freed with LocalFree.
struct LocalPtr<T>(*mut T);
impl<T> Drop for LocalPtr<T> {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { LocalFree(self.0 as *mut _) };
}
}
}
/// Given a pointer to a null-terminated wide character string, returns a slice
/// of the string, excluding the null wide character. Behavior is undefined if
/// given a pointer to a non-null-terminated string.
unsafe fn null_terminated_slice<'a>(ptr: *const u16) -> Result<&'a [u16]> {
let mut i = 0isize;
loop {
if *ptr.offset(i) == 0u16 {
break;
}
i += 1;
}
if *ptr.offset(i) != 0u16 {
bail!("No null terminator found");
}
let slice_size: usize = i.try_into()?;
Ok(std::slice::from_raw_parts(ptr, slice_size))
}
#[cfg(test)]
mod tests {
use std::ffi::OsStr;
use std::ffi::OsString;
use std::str::FromStr;
use anyhow::Result;
use super::command_line_to_argv;
use super::null_terminated_slice;
#[test]
fn test_command_line_to_argv() -> Result<()> {
assert!(command_line_to_argv(OsStr::new(""))?.is_empty());
assert_eq!(
command_line_to_argv(OsStr::new("C:\\Windows\\system32\\svchost.exe"))?,
vec![OsString::from_str("C:\\Windows\\system32\\svchost.exe")?]
);
assert_eq!(
command_line_to_argv(OsStr::new("foo.exe bar.txt"))?,
vec![
OsString::from_str("foo.exe")?,
OsString::from_str("bar.txt")?
]
);
Ok(())
}
#[test]
fn test_null_terminated_slice() -> Result<()> {
unsafe {
assert!(null_terminated_slice(vec![0u16].as_ptr())?.is_empty());
assert_eq!(
null_terminated_slice(vec![1u16, 0u16].as_ptr())?,
vec![1u16]
);
assert_eq!(
null_terminated_slice(vec![1u16, 2u16, 0u16].as_ptr())?,
vec![1u16, 2u16]
);
}
Ok(())
}
}