AutoIngest/AutoIngest/Engine/AccessTimeTracker.cs

123 lines
5.0 KiB
C#

using System;
using System.Diagnostics;
namespace AutoIngest.Engine
{
/// <summary>
/// Deals with the awkward reality that Windows disables <c>LastAccessTime</c> updates by
/// default (since Vista, for performance). Retention can still work using
/// <c>LastWriteTime</c> (import date), but true "was this folder opened recently?" tracking
/// needs access times to be on. This class detects the current state and offers to enable
/// it (which requires admin + a restart to fully take effect).
///
/// We never silently change system settings. The UI shows the status and, if the user opts
/// in, launches an elevated <c>fsutil</c> that Windows will prompt UAC for.
/// </summary>
public static class AccessTimeTracker
{
/// <summary>
/// Whether NTFS is currently updating LastAccessTime on file reads. Conservative: only
/// returns true when we get a clear "enabled" signal from fsutil. Unknown / system-managed
/// modes return false so the UI degrades to the LastWriteTime fallback.
/// </summary>
public static bool IsEnabled()
{
try
{
var (output, exitCode) = RunFsutil("behavior query DisableLastAccess");
if (exitCode != 0 || string.IsNullOrWhiteSpace(output)) return false;
return ParseDisableLastAccess(output);
}
catch
{
return false;
}
}
/// <summary>
/// Attempts to enable access-time updates by launching an elevated fsutil. Returns true
/// if the elevated process was launched (the UAC prompt was accepted); false if the user
/// declined elevation or launch failed. The actual effect requires a reboot, which the
/// caller is responsible for communicating.
/// </summary>
public static bool TryEnable()
{
try
{
var psi = new ProcessStartInfo
{
FileName = "fsutil.exe",
Arguments = "behavior set DisableLastAccess 0",
Verb = "runas", // triggers UAC
UseShellExecute = true, // required for Verb = runas
WindowStyle = ProcessWindowStyle.Hidden,
CreateNoWindow = true
};
using var p = Process.Start(psi);
if (p == null) return false;
// Don't block forever — fsutil is fast, but UAC might be pending. 30s is plenty
// for the user to respond to the prompt.
return p.WaitForExit(30000) && p.ExitCode == 0;
}
catch (System.ComponentModel.Win32Exception)
{
// User cancelled the UAC prompt (ERROR_CANCELLED).
return false;
}
catch
{
return false;
}
}
/// <summary>
/// Parses <c>fsutil behavior query DisableLastAccess</c> output.
/// Historical values:
/// 0 = enabled (updating access times)
/// 1 = disabled (the Vista+ default)
/// 80000003 = system-managed (Win10 1803+); may or may not update — treat as off
/// The output line looks like "DisableLastAccess = 1" or includes a note line.
/// </summary>
static bool ParseDisableLastAccess(string output)
{
foreach (var raw in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
var line = raw.Trim();
int eq = line.IndexOf('=');
if (eq < 0) continue;
var value = line.Substring(eq + 1).Trim();
// Take the leading integer token (ignore trailing notes).
var digits = new System.Text.StringBuilder();
foreach (var ch in value)
{
if (char.IsDigit(ch)) digits.Append(ch);
else if (digits.Length > 0) break;
}
if (digits.Length == 0) continue;
if (!int.TryParse(digits.ToString(), out var n)) continue;
// 0 = enabled. Anything else (1, system-managed codes) = treat as disabled.
return n == 0;
}
return false;
}
static (string output, int exitCode) RunFsutil(string args)
{
var psi = new ProcessStartInfo
{
FileName = "fsutil.exe",
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var p = Process.Start(psi);
if (p == null) return ("", -1);
var stdout = p.StandardOutput.ReadToEnd();
p.WaitForExit(5000);
return (stdout, p.ExitCode);
}
}
}