Made unreal work and make the fake drive update with the json so you can edit the fake config and it writes to the real drive.

This commit is contained in:
2026-01-07 09:16:57 +11:00
parent 9070768fe7
commit dafeeb0f15
9 changed files with 919 additions and 1387 deletions

View File

@@ -7,53 +7,66 @@ using DokanNet.Logging;
namespace DevDrive;
/// <summary>
/// Entry point for the virtual drive application.
/// 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 DefaultMountPoint = "A:\\";
private const string DefaultConfigDrive = "C:\\PhysicalDrive\\";
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 readonly ManualResetEvent ShutdownEvent = new(false);
private static MirrorFileSystem? _mirrorFs;
private static bool _isMounted;
private static string _configDrive = DefaultConfigDrive;
private static string _configFilePath = "";
private static string _mountPoint = DefaultMountPoint;
private static FloppyConfig? _lastConfig;
private static readonly object MountLock = new();
private static bool _isUpdatingDevConfig;
private static bool _isConfigDriveLetter;
/// <summary>
/// Main entry point.
/// </summary>
/// <param name="args">Command line arguments. [0] = mount point, [1] = config drive.</param>
/// <param name="args">Command line arguments. [0] = drive letter (e.g. "A:"), [1] = config path.</param>
public static void Main(string[] args)
{
_mountPoint = args.Length > 0 ? args[0] : DefaultMountPoint;
_configDrive = args.Length > 1 ? args[1] : DefaultConfigDrive;
_driveLetter = args.Length > 0 ? args[0].TrimEnd('\\') : DefaultDriveLetter;
_configPath = args.Length > 1 ? args[1] : DefaultConfigPath;
if (!_mountPoint.EndsWith("\\"))
if (!_driveLetter.EndsWith(":"))
{
_mountPoint += "\\";
_driveLetter += ":";
}
if (!_configDrive.EndsWith("\\"))
_mountPoint = _driveLetter + "\\";
if (!_configPath.EndsWith("\\"))
{
_configDrive += "\\";
_configPath += "\\";
}
_configFilePath = Path.Combine(_configDrive, ConfigFile);
_isConfigDriveLetter = _configDrive.Length <= 3 && _configDrive[1] == ':';
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");
Console.WriteLine($"{LogPrefix} ==================");
Console.WriteLine($"{LogPrefix} Watching for config drive: {_configDrive}");
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} Mount point: {_mountPoint}");
Console.WriteLine($"{LogPrefix} Root folder: {_devDrivePath}");
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Press Ctrl+C to exit.");
Console.WriteLine();
@@ -68,18 +81,20 @@ public static class Program
try
{
_dokan = new Dokan(new ConsoleLogger(LogPrefix));
EnsureRootFolder();
SetupDevConfigWatcher();
if (_isConfigDriveLetter)
{
StartWmiWatchers();
}
else
{
StartFileSystemWatcher();
StartVolumeWatchers();
}
CheckAndMountIfReady();
ShutdownEvent.WaitOne();
}
catch (Exception ex)
@@ -89,17 +104,54 @@ public static class Program
}
finally
{
UnmountFloppy();
CleanupSymlinks();
UnmountDrive();
_devConfigWatcher?.Dispose();
_dokan?.Dispose();
}
}
private static void StartFileSystemWatcher()
private static void EnsureRootFolder()
{
StartVolumeWatchers();
if (!Directory.Exists(_devDrivePath))
{
Directory.CreateDirectory(_devDrivePath);
Console.WriteLine($"{LogPrefix} Created root folder: {_devDrivePath}");
}
}
Console.WriteLine($"{LogPrefix} Volume watchers started for {_configDrive}");
Console.WriteLine($"{LogPrefix} Watching for: {ConfigFile}");
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()
@@ -125,17 +177,19 @@ public static class Program
removeWatcher.EventArrived += (s, e) =>
{
Console.WriteLine($"{LogPrefix} Volume removed");
lock (MountLock)
lock (SyncLock)
{
if (_isMounted && !IsConfigAccessible())
{
Console.WriteLine($"{LogPrefix} Config drive no longer accessible, unmounting...");
UnmountFloppy();
UnmountDrive();
_lastConfig = null;
}
}
};
removeWatcher.Start();
Console.WriteLine($"{LogPrefix} Volume watchers started for {_configPath}");
}
catch (Exception ex)
{
@@ -143,46 +197,6 @@ public static class Program
}
}
private static bool IsConfigAccessible()
{
try
{
if (!Directory.Exists(_configDrive))
{
return false;
}
var _ = Directory.GetFiles(_configDrive);
return File.Exists(_configFilePath);
}
catch
{
return false;
}
}
private static void StartWmiWatchers()
{
var driveLetter = _configDrive.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 OnDriveInserted(object sender, EventArrivedEventArgs e)
{
try
@@ -196,12 +210,12 @@ public static class Program
}
var insertedDrive = deviceId + "\\";
if (string.Equals(insertedDrive, _configDrive, StringComparison.OrdinalIgnoreCase))
if (string.Equals(insertedDrive, _configPath, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Drive {deviceId} inserted!");
Thread.Sleep(500);
CheckAndMountIfReady();
}
@@ -225,18 +239,18 @@ public static class Program
}
var removedDrive = deviceId + "\\";
if (string.Equals(removedDrive, _configDrive, StringComparison.OrdinalIgnoreCase))
if (string.Equals(removedDrive, _configPath, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Drive {deviceId} removed!");
lock (MountLock)
lock (SyncLock)
{
if (_isMounted)
{
Console.WriteLine($"{LogPrefix} Config drive disconnected, unmounting...");
UnmountFloppy();
UnmountDrive();
_lastConfig = null;
}
}
@@ -250,14 +264,14 @@ public static class Program
private static void CheckAndMountIfReady()
{
lock (MountLock)
lock (SyncLock)
{
var driveExists = Directory.Exists(_configDrive);
var driveExists = Directory.Exists(_configPath);
var configExists = driveExists && File.Exists(_configFilePath);
if (!driveExists)
{
Console.WriteLine($"{LogPrefix} Waiting for {_configDrive} to be connected...");
Console.WriteLine($"{LogPrefix} Waiting for {_configPath} to be connected...");
return;
}
@@ -272,114 +286,55 @@ public static class Program
if (config == null || config.Paths.Count == 0)
{
Console.WriteLine($"{LogPrefix} Config empty or invalid");
if (_isMounted)
{
UnmountFloppy();
UnmountDrive();
_lastConfig = null;
}
return;
}
var configChanged = !ConfigEquals(_lastConfig, config);
if (!_isMounted || configChanged)
if (!_isMounted)
{
if (_isMounted && configChanged)
{
Console.WriteLine($"{LogPrefix} Config changed, remounting...");
UnmountFloppy();
}
MountFloppy(config);
EnsureDevConfigFromConfig(config);
MountDrive();
SyncSymlinksFromConfig(config);
_lastConfig = config;
}
}
}
private static FloppyConfig? ReadConfig(string configFilePath)
private static void MountDrive()
{
try
{
var json = File.ReadAllText(configFilePath);
return JsonSerializer.Deserialize<FloppyConfig>(json);
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error reading config: {ex.Message}");
return null;
}
}
private static bool ConfigEquals(FloppyConfig? a, FloppyConfig? b)
{
if (a == null || b == null)
{
return a == b;
}
if (a.Paths.Count != b.Paths.Count)
{
return false;
}
for (var i = 0; i < a.Paths.Count; i++)
{
if (a.Paths[i].Name != b.Paths[i].Name || a.Paths[i].Path != b.Paths[i].Path)
{
return false;
}
}
return true;
}
private static void MountFloppy(FloppyConfig config)
{
if (_dokan == null)
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
{
var symlinks = new Dictionary<string, string>();
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Creating symlinks:");
foreach (var entry in config.Paths)
{
var symlinkPath = "\\" + entry.Name;
symlinks[symlinkPath] = entry.Path;
Console.WriteLine($"{LogPrefix} {entry.Name} -> {entry.Path}");
}
Console.WriteLine();
var totalBytes = (long)(config.DriveTotalMB * 1024 * 1024);
var freeBytes = (long)(config.DriveFreeMB * 1024 * 1024);
Console.WriteLine($"{LogPrefix} Drive size: {config.DriveTotalMB} MB total, {config.DriveFreeMB} MB free");
var floppyFs = new FloppyFileSystem(symlinks, config.DriveName, totalBytes, freeBytes);
_mirrorFs = new MirrorFileSystem(_devDrivePath, volumeName, totalBytes, freeBytes);
var dokanBuilder = new DokanInstanceBuilder(_dokan)
.ConfigureOptions(options =>
{
options.Options = DokanOptions.MountManager | DokanOptions.CurrentSession;
options.Options = DokanOptions.FixedDrive | DokanOptions.CurrentSession;
options.MountPoint = _mountPoint;
options.SingleThread = false;
options.AllocationUnitSize = FloppyStorage.AllocationUnitSize;
options.SectorSize = FloppyStorage.SectorSize;
});
_dokanInstance = dokanBuilder.Build(floppyFs);
_dokanInstance = dokanBuilder.Build(_mirrorFs);
_isMounted = true;
Console.WriteLine($"{LogPrefix} Mounted at {_mountPoint}");
var iconPath = string.IsNullOrWhiteSpace(config.DriveIcon)
? "%SystemRoot%\\System32\\shell32.dll,6"
: config.DriveIcon;
DriveIconHelper.SetDriveIcon(_mountPoint, iconPath, config.DriveName);
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)
{
@@ -388,7 +343,7 @@ public static class Program
}
}
private static void UnmountFloppy()
private static void UnmountDrive()
{
if (!_isMounted)
{
@@ -397,16 +352,287 @@ public static class Program
try
{
DriveIconHelper.RemoveDriveIcon();
_dokan?.RemoveMountPoint(_mountPoint);
_dokanInstance?.Dispose();
_dokanInstance = null;
_mirrorFs = null;
_isMounted = false;
Console.WriteLine($"{LogPrefix} Unmounted");
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());
}
}