Add storage diagnostics to codex doctor (#38795)

## What changed

- Report available space for `CODEX_HOME` and the active worktree, warning below 5 GiB and failing below 1 GiB.
- On Windows, report whether the active Git worktree is on a trusted Dev Drive and provide remediation when it is not.
- Run attached doctor reports against the conversation workspace, including workspaces recovered from persisted thread metadata, while keeping the subprocess anchored in `CODEX_HOME` so deleted workspaces do not prevent it from starting.

## Testing

- Cover disk thresholds, measurement failures, missing paths, conversation workspace selection, command construction, Windows volume detection, and human-readable output.

GitOrigin-RevId: 0a99f9d278717e7d19936ffc504984b3a0958c25
This commit is contained in:
chess
2026-08-15 18:47:25 +00:00
committed by copyberry
parent a95a6fe333
commit 6efcdad4c3
9 changed files with 669 additions and 20 deletions
@@ -8,6 +8,7 @@
//! attaching exactly the same JSON a user could copy from the CLI.
use std::collections::BTreeMap;
use std::path::Path;
use std::process::Stdio;
use std::time::Duration;
@@ -30,19 +31,22 @@ pub(crate) struct DoctorFeedbackReport {
pub(crate) tags: BTreeMap<String, String>,
}
/// Runs `codex doctor --json` and returns a best-effort feedback attachment.
/// Runs `codex --cd <workspace> doctor --json` and returns a best-effort
/// feedback attachment.
///
/// Failure to spawn Codex, finish before the timeout, or parse JSON means the
/// feedback upload proceeds without the doctor report. Callers should merge the
/// returned tags without overriding explicit client-provided tags.
pub(crate) async fn doctor_feedback_report(config: &Config) -> Option<DoctorFeedbackReport> {
pub(crate) async fn doctor_feedback_report(
config: &Config,
workspace: &Path,
) -> Option<DoctorFeedbackReport> {
let executable = config
.codex_self_exe
.clone()
.or_else(|| std::env::current_exe().ok())?;
let mut command = Command::new(&executable);
command.arg("doctor").arg("--json");
let mut command = doctor_command(&executable, workspace, config.codex_home.as_path());
command.stdin(Stdio::null());
command.kill_on_drop(/*kill_on_drop*/ true);
let output = match timeout(DOCTOR_FEEDBACK_REPORT_TIMEOUT, command.output()).await {
@@ -99,6 +103,18 @@ pub(crate) async fn doctor_feedback_report(config: &Config) -> Option<DoctorFeed
})
}
fn doctor_command(executable: &Path, cwd: &Path, codex_home: &Path) -> Command {
let mut command = Command::new(executable);
command
.arg("--cd")
.arg(cwd)
.arg("doctor")
.arg("--json")
.current_dir(codex_home)
.env("CODEX_HOME", codex_home);
command
}
fn doctor_report_tags(report: &Value) -> BTreeMap<String, String> {
let mut tags = BTreeMap::new();
if let Some(overall_status) = report.get("overallStatus").and_then(Value::as_str) {
@@ -178,6 +194,31 @@ mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::tempdir;
#[test]
fn doctor_command_keeps_missing_workspace_out_of_process_cwd() {
let codex_home = tempdir().expect("codex home");
let workspace = codex_home.path().join("deleted-workspace");
let command = doctor_command(Path::new("codex"), &workspace, codex_home.path());
let command = command.as_std();
assert_eq!(command.get_current_dir(), Some(codex_home.path()));
assert_eq!(
command.get_args().collect::<Vec<_>>(),
[
"--cd".as_ref(),
workspace.as_os_str(),
"doctor".as_ref(),
"--json".as_ref()
]
);
assert_eq!(
command.get_envs().collect::<Vec<_>>(),
[("CODEX_HOME".as_ref(), Some(codex_home.path().as_os_str()))]
);
}
#[test]
fn doctor_report_tags_summarize_status_counts() {
@@ -241,13 +241,22 @@ impl FeedbackRequestProcessor {
}
let mut extra_attachments = Vec::new();
if include_logs
&& let Some(doctor_report) =
super::feedback_doctor_report::doctor_feedback_report(&self.config).await
{
extra_attachments.push(doctor_report.attachment);
for (key, value) in doctor_report.tags {
upload_tags.entry(key).or_insert(value);
if include_logs {
let doctor_cwd = feedback_cwd(
&self.thread_manager,
self.state_db.as_ref(),
conversation_id,
self.config.cwd.as_path(),
)
.await;
if let Some(doctor_report) =
super::feedback_doctor_report::doctor_feedback_report(&self.config, &doctor_cwd)
.await
{
extra_attachments.push(doctor_report.attachment);
for (key, value) in doctor_report.tags {
upload_tags.entry(key).or_insert(value);
}
}
}
@@ -303,6 +312,33 @@ impl FeedbackRequestProcessor {
}
}
async fn feedback_cwd(
thread_manager: &ThreadManager,
state_db: Option<&StateDbHandle>,
conversation_id: Option<ThreadId>,
fallback_cwd: &Path,
) -> PathBuf {
let Some(conversation_id) = conversation_id else {
return fallback_cwd.to_path_buf();
};
if let Ok(conversation) = thread_manager.get_thread(conversation_id).await {
return conversation.config_snapshot().await.cwd().to_path_buf();
}
let Some(state_db) = state_db else {
return fallback_cwd.to_path_buf();
};
match state_db.get_thread(conversation_id).await {
Ok(Some(metadata)) => metadata.cwd,
Ok(None) => fallback_cwd.to_path_buf(),
Err(err) => {
warn!("failed to resolve cwd for feedback thread_id={conversation_id}: {err}");
fallback_cwd.to_path_buf()
}
}
}
#[derive(Debug, PartialEq)]
struct FeedbackTurnMetadata {
model: String,
@@ -434,7 +470,28 @@ mod tests {
use super::*;
use codex_protocol::protocol::TurnContextItem;
use codex_rollout::RolloutLine;
use core_test_support::responses::start_mock_server;
use core_test_support::test_codex::test_codex;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn doctor_uses_loaded_feedback_thread_cwd() -> anyhow::Result<()> {
let server = start_mock_server().await;
let test = test_codex().build_with_auto_env(&server).await?;
let daemon_workspace = tempfile::tempdir()?;
let cwd = feedback_cwd(
&test.thread_manager,
/*state_db*/ None,
Some(test.session_configured.thread_id),
daemon_workspace.path(),
)
.await;
assert_eq!(cwd, test.cwd_path());
Ok(())
}
#[test]
fn feedback_tags_drop_unverified_client_prompt_tags() {
let mut upload_tags = BTreeMap::from([
+21 -5
View File
@@ -67,6 +67,7 @@ use serde::Serialize;
use supports_color::Stream;
mod background;
mod disk;
mod git;
mod output;
mod progress;
@@ -75,6 +76,8 @@ mod system;
mod thread_inventory;
mod title;
mod updates;
#[cfg(target_os = "windows")]
mod windows_dev_drive;
use background::background_server_check;
use git::git_check;
@@ -348,6 +351,23 @@ async fn build_report(
progress.begin("config");
let config_result = load_config(root_config_overrides, interactive, arg0_paths).await;
let cwd = config_result
.as_ref()
.map(|config| config.cwd.as_path().to_path_buf())
.unwrap_or_else(|_| {
let mut cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
if let Some(requested_cwd) = &interactive.cwd {
cwd.push(requested_cwd);
}
cwd
});
checks.push(run_sync_check("disk", progress.clone(), || {
disk::check(config_result.as_ref().ok(), &cwd)
}));
#[cfg(target_os = "windows")]
checks.push(run_sync_check("dev drive", progress.clone(), || {
windows_dev_drive::check(&cwd)
}));
match &config_result {
Ok(config) => {
let auth_manager_result =
@@ -446,10 +466,6 @@ async fn build_report(
}
Err(err) => {
let reachability_plan = default_reachability_plan();
let fallback_cwd = interactive
.cwd
.clone()
.unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let (
config_check,
network_check,
@@ -476,7 +492,7 @@ async fn build_report(
terminal_check(command.no_color)
})
},
run_async_check("git", progress.clone(), git_check(fallback_cwd.as_path())),
run_async_check("git", progress.clone(), git_check(&cwd)),
async { run_sync_check("state", progress.clone(), fallback_state_check) },
run_async_check(
"provider reachability",
+178
View File
@@ -0,0 +1,178 @@
use std::io;
use std::path::Path;
use codex_core::config::Config;
use super::CheckStatus;
use super::DoctorCheck;
use super::DoctorIssue;
use super::find_codex_home;
const GIB: u64 = 1024 * 1024 * 1024;
const FAIL_THRESHOLD: u64 = GIB;
const WARNING_THRESHOLD: u64 = 5 * GIB;
pub(super) fn check(config: Option<&Config>, cwd: &Path) -> DoctorCheck {
let home = config
.map(|config| config.codex_home.as_path().to_path_buf())
.or_else(|| find_codex_home().ok().map(Into::into))
.or_else(|| std::env::var_os("CODEX_HOME").map(Into::into));
check_with_paths(home.as_deref(), cwd, available_space)
}
fn check_with_paths(
home: Option<&Path>,
cwd: &Path,
measure: impl Fn(&Path) -> io::Result<u64>,
) -> DoctorCheck {
let home = home.and_then(|path| std::path::absolute(path).ok());
let mut check = DoctorCheck::new(
"system.disk",
"disk",
CheckStatus::Ok,
"sufficient free disk space",
)
.detail(format!(
"warning threshold: {}",
format_capacity(WARNING_THRESHOLD)
))
.detail(format!(
"failure threshold: {}",
format_capacity(FAIL_THRESHOLD)
));
let mut lowest = None;
for (label, path) in [("CODEX_HOME", home.as_deref()), ("worktree", Some(cwd))] {
let field = format!("{label} available");
let available = path
.and_then(|path| path.ancestors().find(|ancestor| ancestor.is_dir()))
.map(&measure)
.unwrap_or_else(|| Err(io::ErrorKind::NotFound.into()));
match available {
Ok(available) => {
let measured = format_capacity(available);
check = check.detail(format!("{field}: {measured}"));
lowest = Some(lowest.map_or(available, |lowest: u64| lowest.min(available)));
let status = match available {
0..FAIL_THRESHOLD => CheckStatus::Fail,
FAIL_THRESHOLD..WARNING_THRESHOLD => CheckStatus::Warning,
WARNING_THRESHOLD.. => CheckStatus::Ok,
};
if status != CheckStatus::Ok {
check.status = check.status.max(status);
check = check.issue(
DoctorIssue::new(status, format!("{label} has insufficient disk space"))
.measured(measured)
.expected(format!("at least {}", format_capacity(WARNING_THRESHOLD)))
.remedy("Free disk space or move the worktree to a larger volume.")
.field(field),
);
}
}
Err(error) => {
check.status = check.status.max(CheckStatus::Warning);
check = check
.detail(format!("{field}: unavailable ({:?})", error.kind()))
.issue(
DoctorIssue::new(
CheckStatus::Warning,
format!("disk space for {label} could not be checked"),
)
.measured(format!("{:?}", error.kind()))
.expected("readable filesystem capacity")
.remedy("Check filesystem access and available disk space.")
.field(field),
);
}
}
}
check.summary = match (check.status, lowest) {
(CheckStatus::Fail, Some(available)) => {
format!("critically low disk space ({})", format_capacity(available))
}
(CheckStatus::Warning, Some(available)) if available < WARNING_THRESHOLD => {
format!("low disk space ({})", format_capacity(available))
}
(CheckStatus::Warning, _) => "disk capacity could not be fully verified".to_string(),
(CheckStatus::Ok, Some(available)) => {
format!(
"sufficient free disk space ({})",
format_capacity(available)
)
}
(CheckStatus::Ok | CheckStatus::Fail, None) => {
"disk capacity could not be verified".to_string()
}
};
check
}
fn format_capacity(bytes: u64) -> String {
if bytes >= GIB {
format!("{:.1} GiB", bytes as f64 / GIB as f64)
} else {
format!("{:.1} MiB", bytes as f64 / (1024 * 1024) as f64)
}
}
#[cfg(unix)]
fn available_space(path: &Path) -> io::Result<u64> {
use std::ffi::CString;
use std::mem::MaybeUninit;
use std::os::unix::ffi::OsStrExt;
let path = CString::new(path.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL"))?;
let mut stats = MaybeUninit::<libc::statvfs>::uninit();
if unsafe { libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) } != 0 {
return Err(io::Error::last_os_error());
}
let stats = unsafe { stats.assume_init() };
if stats.f_files > 0 && stats.f_favail == 0 {
return Ok(0);
}
u64::try_from(u128::from(stats.f_bavail) * u128::from(stats.f_frsize))
.map_err(|_| io::Error::other("available disk space exceeds u64"))
}
#[cfg(windows)]
fn available_space(path: &Path) -> io::Result<u64> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
let mut path = path.as_os_str().encode_wide().collect::<Vec<_>>();
if path.last() != Some(&u16::from(b'\\')) {
path.push(u16::from(b'\\'));
}
path.push(0);
let mut available = 0;
if unsafe {
GetDiskFreeSpaceExW(
path.as_ptr(),
&mut available,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
} == 0
{
return Err(io::Error::last_os_error());
}
Ok(available)
}
#[cfg(not(any(unix, windows)))]
fn available_space(_path: &Path) -> io::Result<u64> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"filesystem capacity is unavailable on this platform",
))
}
#[cfg(test)]
#[path = "disk_tests.rs"]
mod tests;
+160
View File
@@ -0,0 +1,160 @@
use std::cell::RefCell;
use std::io;
use std::path::Path;
use pretty_assertions::assert_eq;
use serde_json::json;
use super::CheckStatus;
use super::FAIL_THRESHOLD;
use super::GIB;
use super::WARNING_THRESHOLD;
use super::check;
use super::check_with_paths;
#[test]
fn disk_capacity_is_reported_without_loaded_configuration() {
let check = check(/*config*/ None, Path::new("."));
for label in ["CODEX_HOME available: ", "worktree available: "] {
assert!(
check
.details
.iter()
.any(|detail| detail.starts_with(label) && !detail.contains("unavailable"))
);
}
}
#[test]
fn disk_capacity_thresholds_produce_complete_diagnostics() {
for (available, status, measured, summary) in [
(0, "fail", "0.0 MiB", "critically low disk space (0.0 MiB)"),
(
FAIL_THRESHOLD - 1,
"fail",
"1024.0 MiB",
"critically low disk space (1024.0 MiB)",
),
(
FAIL_THRESHOLD,
"warning",
"1.0 GiB",
"low disk space (1.0 GiB)",
),
(
WARNING_THRESHOLD - 1,
"warning",
"5.0 GiB",
"low disk space (5.0 GiB)",
),
(
WARNING_THRESHOLD,
"ok",
"5.0 GiB",
"sufficient free disk space (5.0 GiB)",
),
(
WARNING_THRESHOLD + GIB,
"ok",
"6.0 GiB",
"sufficient free disk space (6.0 GiB)",
),
] {
let issues = ["CODEX_HOME", "worktree"]
.into_iter()
.filter(|_| status != "ok")
.map(|label| {
json!({
"severity": status,
"cause": format!("{label} has insufficient disk space"),
"measured": measured,
"expected": "at least 5.0 GiB",
"remedy": "Free disk space or move the worktree to a larger volume.",
"fields": [format!("{label} available")],
})
})
.collect::<Vec<_>>();
let check = check_with_paths(Some(Path::new(".")), Path::new("."), |_| Ok(available));
assert_eq!(
serde_json::to_value(check).unwrap(),
json!({
"id": "system.disk",
"category": "disk",
"status": status,
"summary": summary,
"details": [
"warning threshold: 5.0 GiB",
"failure threshold: 1.0 GiB",
format!("CODEX_HOME available: {measured}"),
format!("worktree available: {measured}"),
],
"issues": issues,
"remediation": null,
"durationMs": 0,
}),
"available bytes: {available}"
);
}
}
#[test]
fn disk_measurement_errors_produce_complete_warning_diagnostics() {
let check = check_with_paths(Some(Path::new(".")), Path::new("."), |_| {
Err(io::ErrorKind::PermissionDenied.into())
});
let issues = ["CODEX_HOME", "worktree"]
.into_iter()
.map(|label| {
json!({
"severity": "warning",
"cause": format!("disk space for {label} could not be checked"),
"measured": "PermissionDenied",
"expected": "readable filesystem capacity",
"remedy": "Check filesystem access and available disk space.",
"fields": [format!("{label} available")],
})
})
.collect::<Vec<_>>();
assert_eq!(
serde_json::to_value(check).unwrap(),
json!({
"id": "system.disk",
"category": "disk",
"status": "warning",
"summary": "disk capacity could not be fully verified",
"details": [
"warning threshold: 5.0 GiB",
"failure threshold: 1.0 GiB",
"CODEX_HOME available: unavailable (PermissionDenied)",
"worktree available: unavailable (PermissionDenied)",
],
"issues": issues,
"remediation": null,
"durationMs": 0,
})
);
}
#[test]
fn nonexistent_relative_codex_home_uses_current_directory_capacity() {
let measured_paths = RefCell::new(Vec::new());
let check = check_with_paths(
Some(Path::new("doctor-nonexistent-relative-home/nested")),
Path::new("."),
|path| {
measured_paths.borrow_mut().push(path.to_path_buf());
Ok(WARNING_THRESHOLD)
},
);
assert_eq!(check.status, CheckStatus::Ok);
assert_eq!(
measured_paths.into_inner(),
[std::env::current_dir().unwrap(), Path::new(".").into()]
);
}
+20 -2
View File
@@ -26,7 +26,7 @@ const GROUPS: &[OutputGroup] = &[
OutputGroup {
title: "Environment",
keys: &[
"system", "runtime", "install", "search", "git", "terminal", "title", "state",
"system", "disk", "runtime", "install", "search", "git", "terminal", "title", "state",
"threads",
],
},
@@ -1301,9 +1301,27 @@ Background Server
#[test]
fn render_human_report_snapshot_covers_environment_rows() {
let mut report = sample_report();
report.checks.push(DoctorCheck::new(
"system.disk",
"disk",
CheckStatus::Ok,
"sufficient free disk space (42.0 GiB)",
));
report.checks.push(
DoctorCheck::new(
"git.worktree.dev_drive",
"git",
CheckStatus::Warning,
"this worktree is not on a Windows Dev Drive",
)
.remediation(
"create a trusted Windows Dev Drive: https://learn.microsoft.com/en-us/windows/dev-drive/",
),
);
insta::assert_snapshot!(
"doctor_human_report_environment_rows",
render_human_report(&sample_report(), detailed_no_color_unicode_options())
render_human_report(&report, detailed_no_color_unicode_options())
);
}
@@ -1,12 +1,13 @@
---
source: cli/src/doctor/output.rs
expression: "render_human_report(&sample_report(), detailed_no_color_unicode_options())"
expression: "render_human_report(&report, detailed_no_color_unicode_options())"
---
Codex Doctor v0.0.0
Notes
⚠ terminal narrow terminal
✗ auth token expired - Run `codex login`.
⚠ git this worktree is not on a Windows Dev Drive - create a trusted Windows Dev Drive: https://learn.microsoft.com/en-us/windows/dev-drive/
─────────────────────────────────────────────────────────────
Environment
@@ -19,6 +20,7 @@ Environment
GIT_PAGER delta
GH_PAGER less
LESS -FRX
✓ disk sufficient free disk space (42.0 GiB)
✓ runtime running local build on darwin-arm64
✓ install consistent
managed by npm: no · bun: no · pnpm: no · package root —
@@ -27,6 +29,7 @@ Environment
selected git /usr/bin/git
version git version 2.54.0
repo detected true
⚠ git this worktree is not on a Windows Dev Drive — create a trusted Windows Dev Drive: https://learn.microsoft.com/en-us/windows/dev-drive/
⚠ terminal narrow terminal
✓ title default · project codex
title source default
@@ -50,7 +53,7 @@ Background Server
✓ app-server background server is not running
─────────────────────────────────────────────────────────────
12 ok · 2 notes · 1 warn · 1 fail failed
13 ok · 3 notes · 2 warn · 1 fail failed
--summary compact output --all expand truncated lists
--json redacted report
@@ -0,0 +1,160 @@
use std::ffi::OsString;
use std::fs::OpenOptions;
use std::io;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::ffi::OsStringExt;
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::AsRawHandle;
use std::path::Path;
use std::path::PathBuf;
use std::ptr;
use codex_git_utils::get_git_repo_root;
use os_info::Version;
use windows_sys::Win32::Foundation::ERROR_INVALID_PARAMETER;
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
use windows_sys::Win32::Storage::FileSystem::FILE_READ_ATTRIBUTES;
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ;
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE;
use windows_sys::Win32::Storage::FileSystem::GetDriveTypeW;
use windows_sys::Win32::Storage::FileSystem::GetVolumePathNameW;
use windows_sys::Win32::System::IO::DeviceIoControl;
use windows_sys::Win32::System::WindowsProgramming::DRIVE_FIXED;
use windows_sys::Win32::System::WindowsProgramming::DRIVE_NO_ROOT_DIR;
use windows_sys::Win32::System::WindowsProgramming::DRIVE_UNKNOWN;
use super::CheckStatus;
use super::DoctorCheck;
const ID: &str = "git.worktree.dev_drive";
const DEV_VOLUME: u32 = 0x0000_2000;
const TRUSTED_VOLUME: u32 = 0x0000_4000;
const QUERY_PERSISTENT_VOLUME_STATE: u32 = 0x0009_023c;
#[repr(C)]
struct PersistentVolumeState {
flags: u32,
mask: u32,
version: u32,
reserved: u32,
}
pub(super) fn check(cwd: &Path) -> DoctorCheck {
let Some(worktree) = get_git_repo_root(cwd) else {
return DoctorCheck::new(ID, "git", CheckStatus::Ok, "no Git worktree is active");
};
if matches!(
os_info::get().version(),
Version::Semantic(_, _, build) if *build < 22_621
) {
return DoctorCheck::new(
ID,
"git",
CheckStatus::Ok,
"Windows Dev Drives are unavailable on this Windows version",
);
}
match volume_flags(&worktree) {
Ok(flags) if flags & DEV_VOLUME == 0 => DoctorCheck::new(
ID,
"git",
CheckStatus::Warning,
"this worktree is not on a Windows Dev Drive; moving it to a trusted Dev Drive can significantly improve repository and filesystem performance",
)
.remediation(
"create a trusted Windows Dev Drive for source repositories: https://learn.microsoft.com/en-us/windows/dev-drive/",
),
Ok(flags) if flags & TRUSTED_VOLUME == 0 => DoctorCheck::new(
ID,
"git",
CheckStatus::Warning,
"the active Git worktree is on an untrusted Windows Dev Drive",
)
.remediation(
"ask your administrator to trust the Windows Dev Drive: https://learn.microsoft.com/en-us/windows/dev-drive/#how-do-i-designate-a-dev-drive-as-trusted",
),
Ok(_) => DoctorCheck::new(
ID,
"git",
CheckStatus::Ok,
"the active Git worktree is on a trusted Windows Dev Drive",
),
Err(error) => DoctorCheck::new(
ID,
"git",
CheckStatus::Warning,
"the active Git worktree's Windows Dev Drive state could not be inspected",
)
.detail(format!("filesystem error: {:?}", error.kind()))
.remediation("check access to the Git worktree and its Windows storage volume"),
}
}
fn volume_flags(path: &Path) -> io::Result<u32> {
let path = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect::<Vec<_>>();
let mut volume = vec![0_u16; path.len().max(261)];
if unsafe { GetVolumePathNameW(path.as_ptr(), volume.as_mut_ptr(), volume.len() as u32) } == 0 {
return Err(io::Error::last_os_error());
}
match unsafe { GetDriveTypeW(volume.as_ptr()) } {
DRIVE_UNKNOWN | DRIVE_NO_ROOT_DIR => {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"the Windows storage volume could not be determined",
));
}
DRIVE_FIXED => {}
_ => return Ok(0),
}
let length = volume
.iter()
.position(|character| *character == 0)
.unwrap_or(volume.len());
let root = PathBuf::from(OsString::from_wide(&volume[..length]));
let handle = OpenOptions::new()
.access_mode(FILE_READ_ATTRIBUTES)
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(root)?;
let mut state = PersistentVolumeState {
flags: 0,
mask: DEV_VOLUME | TRUSTED_VOLUME,
version: 1,
reserved: 0,
};
let state_size = std::mem::size_of::<PersistentVolumeState>() as u32;
let state_ptr = ptr::from_mut(&mut state);
let mut bytes_returned = 0;
if unsafe {
DeviceIoControl(
handle.as_raw_handle() as isize,
QUERY_PERSISTENT_VOLUME_STATE,
state_ptr.cast_const().cast(),
state_size,
state_ptr.cast(),
state_size,
&mut bytes_returned,
ptr::null_mut(),
)
} == 0
{
let error = io::Error::last_os_error();
return if error.raw_os_error() == Some(ERROR_INVALID_PARAMETER as i32) {
Ok(0)
} else {
Err(error)
};
}
Ok(state.flags)
}
#[cfg(test)]
#[path = "windows_dev_drive_tests.rs"]
mod tests;
@@ -0,0 +1,16 @@
use std::path::PathBuf;
use pretty_assertions::assert_eq;
use super::DEV_VOLUME;
use super::volume_flags;
#[test]
fn the_windows_system_volume_is_not_a_dev_drive() {
let system_root = std::env::var_os("SystemRoot")
.map(PathBuf::from)
.expect("Windows exposes its system directory");
let flags = volume_flags(&system_root).expect("the system volume can be inspected");
assert_eq!(flags & DEV_VOLUME, 0);
}