fix(experts): recognize Windows junction links to central store
On Windows the expert enable-status check compared `fs::read_link` output against the central path, which silently failed for junctions: the link state fell back to Broken/LinkedElsewhere, so enabled experts kept reappearing as disabled and disappeared from the message-input expert list. Switch the link verification to canonicalize both sides (which transparently follows junctions and symlinks) and add a case-insensitive path comparison on Windows. Also prefer `junction::get_target` for the displayed target path.
This commit is contained in:
@@ -398,7 +398,18 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Best-effort human-readable link target. On Windows, `fs::read_link`
|
||||||
|
/// does not resolve junctions in all stdlib versions — prefer the
|
||||||
|
/// `junction` crate when the path is a reparse point.
|
||||||
fn read_link_target(path: &Path) -> Option<PathBuf> {
|
fn read_link_target(path: &Path) -> Option<PathBuf> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
if path_is_reparse_point(path) {
|
||||||
|
if let Ok(target) = junction::get_target(path) {
|
||||||
|
return Some(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
fs::read_link(path).ok()
|
fs::read_link(path).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,55 +436,66 @@ fn path_is_reparse_point(_path: &Path) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn classify_link(link_path: &Path, expected_target: &Path) -> ExpertLinkState {
|
/// Equality check for two already-canonicalized paths. On Windows the
|
||||||
if !link_path.exists() && !path_is_symlink(link_path) {
|
/// filesystem is case-insensitive but `Path` comparison is not — canonical
|
||||||
return ExpertLinkState::NotLinked;
|
/// forms can still differ in drive-letter case or user-supplied casing.
|
||||||
|
fn paths_equivalent(a: &Path, b: &Path) -> bool {
|
||||||
|
if a == b {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
// Broken symlink detection: `exists()` returns false if the link
|
#[cfg(windows)]
|
||||||
// dangles; but we already checked that. Re-check the metadata.
|
{
|
||||||
match fs::symlink_metadata(link_path) {
|
let a_s = a.as_os_str().to_string_lossy();
|
||||||
Ok(meta) => {
|
let b_s = b.as_os_str().to_string_lossy();
|
||||||
|
return a_s.eq_ignore_ascii_case(b_s.as_ref());
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a path while following symlinks and Windows junctions.
|
||||||
|
/// Returns `None` if the path does not exist or cannot be resolved (e.g.
|
||||||
|
/// dangling link).
|
||||||
|
fn resolve_real_path(path: &Path) -> Option<PathBuf> {
|
||||||
|
fs::canonicalize(path).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_link(link_path: &Path, expected_target: &Path) -> ExpertLinkState {
|
||||||
|
// No entry at all (not even a dangling link) → not linked.
|
||||||
|
let meta = match fs::symlink_metadata(link_path) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return ExpertLinkState::NotLinked,
|
||||||
|
};
|
||||||
|
|
||||||
let is_link_like = meta.file_type().is_symlink() || path_is_reparse_point(link_path);
|
let is_link_like = meta.file_type().is_symlink() || path_is_reparse_point(link_path);
|
||||||
if !is_link_like {
|
if !is_link_like {
|
||||||
|
// A real directory (or file) sits where we'd put our link.
|
||||||
|
// This also covers Windows copy-mode fallback, where we could not
|
||||||
|
// create a junction and fell back to `copy_dir_recursive`. We still
|
||||||
|
// surface it as BlockedByRealDirectory so experts_link_to_agent
|
||||||
|
// treats it as "needs user attention" (the copy will not track
|
||||||
|
// central-store updates and must be re-linked explicitly).
|
||||||
return ExpertLinkState::BlockedByRealDirectory;
|
return ExpertLinkState::BlockedByRealDirectory;
|
||||||
}
|
}
|
||||||
// Verify the target.
|
|
||||||
match read_link_target(link_path) {
|
// `fs::canonicalize` transparently follows both symlinks and Windows
|
||||||
Some(target) => {
|
// junctions, so comparing the two canonical forms is the single
|
||||||
// Normalize both sides by canonicalizing the expected
|
// source of truth for "does this link point at our central store?".
|
||||||
// target. The link target may be relative.
|
// We intentionally do *not* rely on `fs::read_link`'s string output
|
||||||
let target = if target.is_absolute() {
|
// for equality — on Windows junctions its output format is
|
||||||
target
|
// stdlib-version-dependent and often fails to round-trip through
|
||||||
} else if let Some(parent) = link_path.parent() {
|
// `canonicalize` cleanly.
|
||||||
parent.join(target)
|
let resolved_link = resolve_real_path(link_path);
|
||||||
} else {
|
let resolved_expected = resolve_real_path(expected_target);
|
||||||
target
|
|
||||||
};
|
match (resolved_link, resolved_expected) {
|
||||||
let canonical_target = fs::canonicalize(&target).ok();
|
|
||||||
let canonical_expected = fs::canonicalize(expected_target).ok();
|
|
||||||
match (canonical_target.as_ref(), canonical_expected.as_ref()) {
|
|
||||||
(Some(t), Some(e)) if t == e => ExpertLinkState::LinkedToCodeg,
|
|
||||||
(Some(_), Some(_)) => ExpertLinkState::LinkedElsewhere,
|
|
||||||
(None, _) => ExpertLinkState::Broken,
|
(None, _) => ExpertLinkState::Broken,
|
||||||
(_, None) => ExpertLinkState::LinkedElsewhere,
|
(Some(l), Some(e)) if paths_equivalent(&l, &e) => ExpertLinkState::LinkedToCodeg,
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// On Windows junctions, `read_link` may fail but the
|
|
||||||
// directory still resolves. Fall back to canonical
|
|
||||||
// comparison via the path itself.
|
|
||||||
let canonical_link = fs::canonicalize(link_path).ok();
|
|
||||||
let canonical_expected = fs::canonicalize(expected_target).ok();
|
|
||||||
match (canonical_link, canonical_expected) {
|
|
||||||
(Some(t), Some(e)) if t == e => ExpertLinkState::LinkedToCodeg,
|
|
||||||
_ => ExpertLinkState::LinkedElsewhere,
|
_ => ExpertLinkState::LinkedElsewhere,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => ExpertLinkState::NotLinked,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Central store installation ────────────────────────────────────────
|
// ─── Central store installation ────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user