Files
DevDrive/Program.cs

639 lines
21 KiB
C#

using System.Management;
using System.Runtime.Versioning;
using System.Text.Json;
using DokanNet;
using DokanNet.Logging;
namespace DevDrive;
/// <summary>
/// Entry point for the DevDrive application.
/// Uses Dokan to mount a virtual drive with fake capacity and real mklink symlinks.
/// </summary>
[SupportedOSPlatform("windows")]
public static class Program
{
private const string DefaultDriveLetter = "A:";
private const string DefaultConfigPath = "C:\\PhysicalDrive\\";
private const string ConfigFile = "index.json";
private const string LogPrefix = "[DevDrive]";
private const string DevDriveFolderName = "DevDriveRoot";
private static readonly ManualResetEvent ShutdownEvent = new(false);
private static string _driveLetter = DefaultDriveLetter;
private static string _mountPoint = "A:\\";
private static string _configPath = DefaultConfigPath;
private static string _configFilePath = "";
private static string _devDrivePath = "";
private static string _devConfigPath = "";
private static DevDriveConfig? _lastConfig;
private static readonly object SyncLock = new();
private static FileSystemWatcher? _devConfigWatcher;
private static Dokan? _dokan;
private static DokanInstance? _dokanInstance;
private static MirrorFileSystem? _mirrorFs;
private static bool _isMounted;
private static bool _isUpdatingDevConfig;
private static bool _isConfigDriveLetter;
/// <summary>
/// Main entry point.
/// </summary>
/// <param name="args">Command line arguments. [0] = drive letter (e.g. "A:"), [1] = config path.</param>
public static void Main(string[] args)
{
_driveLetter = args.Length > 0 ? args[0].TrimEnd('\\') : DefaultDriveLetter;
_configPath = args.Length > 1 ? args[1] : DefaultConfigPath;
if (!_driveLetter.EndsWith(":"))
{
_driveLetter += ":";
}
_mountPoint = _driveLetter + "\\";
if (!_configPath.EndsWith("\\"))
{
_configPath += "\\";
}
var exeDir = AppContext.BaseDirectory;
_devDrivePath = Path.Combine(exeDir, DevDriveFolderName);
_devConfigPath = Path.Combine(_devDrivePath, "devDriveConfig.json");
_configFilePath = Path.Combine(_configPath, ConfigFile);
_isConfigDriveLetter = _configPath.Length <= 3 && _configPath[1] == ':';
Console.WriteLine($"{LogPrefix} DevDrive (Dokan + mklink mode)");
Console.WriteLine($"{LogPrefix} ==============================");
Console.WriteLine($"{LogPrefix} Drive letter: {_driveLetter}");
Console.WriteLine($"{LogPrefix} Config file: {_configFilePath}");
Console.WriteLine($"{LogPrefix} Root folder: {_devDrivePath}");
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Press Ctrl+C to exit.");
Console.WriteLine();
Console.CancelKeyPress += (sender, eventArgs) =>
{
eventArgs.Cancel = true;
Console.WriteLine($"{LogPrefix} Shutting down...");
ShutdownEvent.Set();
};
try
{
_dokan = new Dokan(new ConsoleLogger(LogPrefix));
EnsureRootFolder();
SetupDevConfigWatcher();
if (_isConfigDriveLetter)
{
StartWmiWatchers();
}
else
{
StartVolumeWatchers();
}
CheckAndMountIfReady();
ShutdownEvent.WaitOne();
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Fatal error: {ex.Message}");
Environment.Exit(1);
}
finally
{
CleanupSymlinks();
UnmountDrive();
_devConfigWatcher?.Dispose();
_dokan?.Dispose();
}
}
private static void EnsureRootFolder()
{
if (!Directory.Exists(_devDrivePath))
{
Directory.CreateDirectory(_devDrivePath);
Console.WriteLine($"{LogPrefix} Created root folder: {_devDrivePath}");
}
}
private static bool IsConfigAccessible()
{
try
{
return Directory.Exists(_configPath) && File.Exists(_configFilePath);
}
catch
{
return false;
}
}
private static void StartWmiWatchers()
{
var driveLetter = _configPath.TrimEnd('\\');
var insertQuery = new WqlEventQuery(
"SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_LogicalDisk'"
);
var insertWatcher = new ManagementEventWatcher(insertQuery);
insertWatcher.EventArrived += OnDriveInserted;
insertWatcher.Start();
var removeQuery = new WqlEventQuery(
"SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_LogicalDisk'"
);
var removeWatcher = new ManagementEventWatcher(removeQuery);
removeWatcher.EventArrived += OnDriveRemoved;
removeWatcher.Start();
Console.WriteLine($"{LogPrefix} WMI watchers started (event-based, no polling)");
Console.WriteLine($"{LogPrefix} Watching for drive: {driveLetter}");
}
private static void StartVolumeWatchers()
{
try
{
var insertQuery = new WqlEventQuery(
"SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Volume'"
);
var insertWatcher = new ManagementEventWatcher(insertQuery);
insertWatcher.EventArrived += (s, e) =>
{
Console.WriteLine($"{LogPrefix} Volume added, checking config...");
Thread.Sleep(1000);
CheckAndMountIfReady();
};
insertWatcher.Start();
var removeQuery = new WqlEventQuery(
"SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Volume'"
);
var removeWatcher = new ManagementEventWatcher(removeQuery);
removeWatcher.EventArrived += (s, e) =>
{
Console.WriteLine($"{LogPrefix} Volume removed");
lock (SyncLock)
{
if (_isMounted && !IsConfigAccessible())
{
Console.WriteLine($"{LogPrefix} Config drive no longer accessible, unmounting...");
UnmountDrive();
_lastConfig = null;
}
}
};
removeWatcher.Start();
Console.WriteLine($"{LogPrefix} Volume watchers started for {_configPath}");
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Warning: Could not start WMI volume watchers: {ex.Message}");
}
}
private static void OnDriveInserted(object sender, EventArrivedEventArgs e)
{
try
{
var targetInstance = (ManagementBaseObject)e.NewEvent["TargetInstance"];
var deviceId = targetInstance["DeviceID"]?.ToString();
if (string.IsNullOrEmpty(deviceId))
{
return;
}
var insertedDrive = deviceId + "\\";
if (string.Equals(insertedDrive, _configPath, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Drive {deviceId} inserted!");
Thread.Sleep(500);
CheckAndMountIfReady();
}
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error handling drive insert: {ex.Message}");
}
}
private static void OnDriveRemoved(object sender, EventArrivedEventArgs e)
{
try
{
var targetInstance = (ManagementBaseObject)e.NewEvent["TargetInstance"];
var deviceId = targetInstance["DeviceID"]?.ToString();
if (string.IsNullOrEmpty(deviceId))
{
return;
}
var removedDrive = deviceId + "\\";
if (string.Equals(removedDrive, _configPath, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Drive {deviceId} removed!");
lock (SyncLock)
{
if (_isMounted)
{
Console.WriteLine($"{LogPrefix} Config drive disconnected, unmounting...");
UnmountDrive();
_lastConfig = null;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error handling drive removal: {ex.Message}");
}
}
private static void CheckAndMountIfReady()
{
lock (SyncLock)
{
var driveExists = Directory.Exists(_configPath);
var configExists = driveExists && File.Exists(_configFilePath);
if (!driveExists)
{
Console.WriteLine($"{LogPrefix} Waiting for {_configPath} to be connected...");
return;
}
if (!configExists)
{
Console.WriteLine($"{LogPrefix} Drive present but {ConfigFile} not found");
return;
}
var config = ReadConfig(_configFilePath);
if (config == null || config.Paths.Count == 0)
{
Console.WriteLine($"{LogPrefix} Config empty or invalid");
if (_isMounted)
{
UnmountDrive();
_lastConfig = null;
}
return;
}
if (!_isMounted)
{
EnsureDevConfigFromConfig(config);
MountDrive();
SyncSymlinksFromConfig(config);
_lastConfig = config;
}
}
}
private static void MountDrive()
{
if (_dokan == null || _isMounted)
{
return;
}
var config = ReadConfig(_configFilePath);
var volumeName = config?.DriveName ?? "Dev Drive";
var totalBytes = (long)((config?.DriveTotalMB ?? 1.44) * 1024 * 1024);
var freeBytes = (long)((config?.DriveFreeMB ?? 0.72) * 1024 * 1024);
try
{
_mirrorFs = new MirrorFileSystem(_devDrivePath, volumeName, totalBytes, freeBytes);
var dokanBuilder = new DokanInstanceBuilder(_dokan)
.ConfigureOptions(options =>
{
options.Options = DokanOptions.FixedDrive | DokanOptions.CurrentSession;
options.MountPoint = _mountPoint;
options.SingleThread = false;
});
_dokanInstance = dokanBuilder.Build(_mirrorFs);
_isMounted = true;
Console.WriteLine($"{LogPrefix} Mounted {_driveLetter} (Dokan)");
Console.WriteLine($"{LogPrefix} Volume: {volumeName}");
Console.WriteLine($"{LogPrefix} Capacity: {config?.DriveTotalMB ?? 1.44} MB total, {config?.DriveFreeMB ?? 0.72} MB free");
}
catch (DokanException ex)
{
Console.WriteLine($"{LogPrefix} Mount error: {ex.Message}");
_isMounted = false;
}
}
private static void UnmountDrive()
{
if (!_isMounted)
{
return;
}
try
{
_dokan?.RemoveMountPoint(_mountPoint);
_dokanInstance?.Dispose();
_dokanInstance = null;
_mirrorFs = null;
_isMounted = false;
Console.WriteLine($"{LogPrefix} Unmounted {_driveLetter}");
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Unmount error: {ex.Message}");
}
}
private static void SetupDevConfigWatcher()
{
if (_devConfigWatcher != null)
{
return;
}
if (!Directory.Exists(_devDrivePath))
{
return;
}
_devConfigWatcher = new FileSystemWatcher(_devDrivePath)
{
Filter = "devDriveConfig.json",
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime | NotifyFilters.FileName,
EnableRaisingEvents = true
};
_devConfigWatcher.Changed += OnDevConfigChanged;
_devConfigWatcher.Created += OnDevConfigChanged;
Console.WriteLine($"{LogPrefix} Watching devDriveConfig.json for changes...");
}
private static void OnDevConfigChanged(object sender, FileSystemEventArgs e)
{
lock (SyncLock)
{
if (_isUpdatingDevConfig)
{
// Change originated from real config sync, ignore.
_isUpdatingDevConfig = false;
return;
}
}
Thread.Sleep(100);
try
{
if (!File.Exists(_devConfigPath))
{
return;
}
var json = File.ReadAllText(_devConfigPath);
var config = JsonSerializer.Deserialize<DevDriveConfig>(json);
if (config == null)
{
Console.WriteLine($"{LogPrefix} devDriveConfig.json change could not be deserialized.");
return;
}
lock (SyncLock)
{
File.WriteAllText(_configFilePath, json);
// Check if drive metadata changed (name, capacity)
var metadataChanged = _lastConfig == null ||
_lastConfig.DriveName != config.DriveName ||
_lastConfig.DriveTotalMB != config.DriveTotalMB ||
_lastConfig.DriveFreeMB != config.DriveFreeMB;
if (metadataChanged && _mirrorFs != null)
{
var volumeName = config.DriveName ?? "Dev Drive";
var totalBytes = (long)(config.DriveTotalMB * 1024 * 1024);
var freeBytes = (long)(config.DriveFreeMB * 1024 * 1024);
_mirrorFs.UpdateMetadata(volumeName, totalBytes, freeBytes);
Console.WriteLine($"{LogPrefix} Updated drive metadata: {volumeName}, {config.DriveTotalMB} MB total, {config.DriveFreeMB} MB free");
}
SyncSymlinksFromConfig(config);
_lastConfig = config;
}
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error processing devDriveConfig.json change: {ex.Message}");
}
}
private static DevDriveConfig? ReadConfig(string path)
{
try
{
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<DevDriveConfig>(json);
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error reading config: {ex.Message}");
return null;
}
}
private static void SyncSymlinksFromConfig(DevDriveConfig config)
{
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Syncing symlinks (mklink /d):");
var existingEntries = Directory.GetFileSystemEntries(_devDrivePath)
.Where(p => !Path.GetFileName(p).Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))
.Where(p => !Path.GetFileName(p).Equals("devDriveConfig.json", StringComparison.OrdinalIgnoreCase))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var configPaths = config.Paths
.Select(p => Path.Combine(_devDrivePath, p.Name))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var entry in existingEntries)
{
if (!configPaths.Contains(entry))
{
RemoveSymlink(entry);
}
}
foreach (var pathEntry in config.Paths)
{
var symlinkPath = Path.Combine(_devDrivePath, pathEntry.Name);
CreateSymlink(symlinkPath, pathEntry.Path);
}
Console.WriteLine();
}
private static void EnsureDevConfigFromConfig(DevDriveConfig config)
{
try
{
_isUpdatingDevConfig = true;
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_devConfigPath, json);
Console.WriteLine($"{LogPrefix} Created devDriveConfig.json from index.json");
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error creating devDriveConfig.json: {ex.Message}");
}
}
private static void CreateSymlink(string symlinkPath, string targetPath)
{
try
{
if (Directory.Exists(symlinkPath) || File.Exists(symlinkPath))
{
var attr = File.GetAttributes(symlinkPath);
if (attr.HasFlag(FileAttributes.ReparsePoint))
{
var existingTarget = new FileInfo(symlinkPath).LinkTarget;
if (existingTarget?.Equals(targetPath, StringComparison.OrdinalIgnoreCase) == true)
{
Console.WriteLine($"{LogPrefix} {Path.GetFileName(symlinkPath)} -> {targetPath} (unchanged)");
return;
}
RemoveSymlink(symlinkPath);
}
else
{
Console.WriteLine($"{LogPrefix} Skipping {Path.GetFileName(symlinkPath)} - not a symlink");
return;
}
}
var isDirectory = Directory.Exists(targetPath);
var args = isDirectory
? $"/c mklink /d \"{symlinkPath}\" \"{targetPath}\""
: $"/c mklink \"{symlinkPath}\" \"{targetPath}\"";
var result = RunCommand("cmd.exe", args);
if (result.ExitCode == 0)
{
Console.WriteLine($"{LogPrefix} {Path.GetFileName(symlinkPath)} -> {targetPath}");
}
else
{
Console.WriteLine($"{LogPrefix} Failed: {Path.GetFileName(symlinkPath)} - {result.Error}");
}
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error: {Path.GetFileName(symlinkPath)} - {ex.Message}");
}
}
private static void RemoveSymlink(string path)
{
try
{
var attr = File.GetAttributes(path);
if (attr.HasFlag(FileAttributes.Directory))
{
Directory.Delete(path, false);
}
else
{
File.Delete(path);
}
Console.WriteLine($"{LogPrefix} Removed: {Path.GetFileName(path)}");
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error removing {Path.GetFileName(path)}: {ex.Message}");
}
}
private static void CleanupSymlinks()
{
if (!Directory.Exists(_devDrivePath))
{
return;
}
Console.WriteLine($"{LogPrefix} Cleaning up symlinks...");
var entries = Directory.GetFileSystemEntries(_devDrivePath)
.Where(p => !Path.GetFileName(p).Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))
.Where(p => !Path.GetFileName(p).Equals("devDriveConfig.json", StringComparison.OrdinalIgnoreCase));
foreach (var entry in entries)
{
try
{
var attr = File.GetAttributes(entry);
if (attr.HasFlag(FileAttributes.ReparsePoint))
{
if (attr.HasFlag(FileAttributes.Directory))
{
Directory.Delete(entry, false);
}
else
{
File.Delete(entry);
}
Console.WriteLine($"{LogPrefix} Removed: {Path.GetFileName(entry)}");
}
}
catch
{
}
}
}
private static (int ExitCode, string Output, string Error) RunCommand(string fileName, string arguments)
{
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
process.Start();
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
process.WaitForExit();
return (process.ExitCode, output.Trim(), error.Trim());
}
}