123 lines
5.0 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|