using System.Management; using System.Runtime.Versioning; using System.Text.Json; using DokanNet; using DokanNet.Logging; namespace DevDrive; /// /// Entry point for the DevDrive application. /// Uses Dokan to mount a virtual drive with fake capacity and real mklink symlinks. /// [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; /// /// Main entry point. /// /// Command line arguments. [0] = drive letter (e.g. "A:"), [1] = config path. 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; if (config.StartupApps.Count > 0) { var driveName = config.DriveName ?? "Dev Drive"; Task.Run(() => StartupDialog.ShowIfNeeded(config.StartupApps, driveName)); } } } } 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(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(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()); } }