public static Session Configure(SingleInstanceMainForm singleInstanceMainForm, ISettingsProvider settingsProvider, Localizer defaultLocalizer, Dictionary <LocalizedStringKey, string> defaultLocalizerDictionary, Icon applicationIcon) { var session = new Session(singleInstanceMainForm, settingsProvider, defaultLocalizer, defaultLocalizerDictionary, applicationIcon); // Use nullcheck on AutoSave to check if the initialization sequence completed. if (session.AutoSave != null) { Current = session; } else { session.Dispose(); } return(Current); }
private Session(SingleInstanceMainForm singleInstanceMainForm, ISettingsProvider settingsProvider, Localizer defaultLocalizer, Dictionary <LocalizedStringKey, string> defaultLocalizerDictionary, Icon applicationIcon) { if (settingsProvider == null) { throw new ArgumentNullException(nameof(settingsProvider)); } if (singleInstanceMainForm == null) { throw new ArgumentNullException(nameof(singleInstanceMainForm)); } if (!singleInstanceMainForm.IsHandleCreated || singleInstanceMainForm.Disposing || singleInstanceMainForm.IsDisposed) { throw new InvalidOperationException($"{nameof(singleInstanceMainForm)} should have its handle created."); } // May be null. this.defaultLocalizerDictionary = defaultLocalizerDictionary; ApplicationIcon = applicationIcon; // This depends on ExecutableFileName. DeveloperMode = new SettingProperty <bool>( new SettingKey(SettingKey.ToSnakeCase(nameof(DeveloperMode))), PType.CLR.Boolean, new SettingComment($"Enables tools which assist with {ExecutableFileNameWithoutExtension} development and debugging.")); // Attempt to load default settings. DefaultSettings = SettingsFile.Create( Path.Combine(ExecutableFolder, DefaultSettingsFileName), settingsProvider.CreateBuiltIn(this)); // Save name of LOCALAPPDATA subfolder for persistent files. AppDataSubFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), GetDefaultSetting(SharedSettings.AppDataSubFolderName)); #if DEBUG // In debug mode, generate default json configuration files from hard coded settings. DeployRuntimeConfigurationFiles(); #endif // Scan Languages subdirectory to load localizers. var langFolderName = GetDefaultSetting(SharedSettings.LangFolderName); registeredLocalizers = Localizers.ScanLocalizers(this, Path.Combine(ExecutableFolder, langFolderName)); LangSetting = new SettingProperty <FileLocalizer>( new SettingKey(LangSettingKey), new PType.KeyedSet <FileLocalizer>(registeredLocalizers)); // Now attempt to get exclusive write access to the .lock file so it becomes a safe mutex. string lockFileName = Path.Combine(AppDataSubFolder, LockFileName); FileStreamPair autoSaveFiles = null; // Retry a maximum number of times. int remainingRetryAttempts = MaxRetryAttemptsForLockFile; while (remainingRetryAttempts >= 0) { try { // Create the folder on startup. Directory.CreateDirectory(AppDataSubFolder); // If this call doesn't throw, exclusive access to the mutex file is obtained. // Then this process is the first instance. // Use a buffer size of 24 rather than the default 4096. lockFile = new FileStream( lockFileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read, WindowHandleLengthInBytes + MagicLengthInBytes); try { // Immediately empty the file. lockFile.SetLength(0); // Write the window handle to the lock file. // Use BitConverter to convert to and from byte[]. byte[] buffer = BitConverter.GetBytes(singleInstanceMainForm.Handle.ToInt64()); lockFile.Write(buffer, 0, WindowHandleLengthInBytes); // Generate a magic GUID for this instance. // The byte array has a length of 16. TodaysMagic = Guid.NewGuid().ToByteArray(); lockFile.Write(TodaysMagic, 0, MagicLengthInBytes); lockFile.Flush(); autoSaveFiles = OpenAutoSaveFileStreamPair(new AutoSaveFileNamePair(AutoSaveFileName1, AutoSaveFileName2)); // Loop exit point 1: successful write to lockFile. Can auto-save. break; } catch { ReleaseLockFile(lockFile); lockFile = null; } } catch { // Not the first instance. // Then try opening the lock file as read-only. try { // If opening as read-only succeeds, read the contents as bytes. FileStream existingLockFile = new FileStream( lockFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, WindowHandleLengthInBytes + MagicLengthInBytes); using (existingLockFile) { byte[] lockFileBytes = new byte[WindowHandleLengthInBytes + MagicLengthInBytes]; int totalBytesRead = 0; int remainingBytes = WindowHandleLengthInBytes + MagicLengthInBytes; while (remainingBytes > 0) { int bytesRead = existingLockFile.Read(lockFileBytes, totalBytesRead, remainingBytes); if (bytesRead == 0) { break; // Unexpected EOF? } totalBytesRead += bytesRead; remainingBytes -= bytesRead; } // For checking that EOF has been reached, evaluate ReadByte() == -1. if (remainingBytes == 0 && existingLockFile.ReadByte() == -1) { // Parse out the remote window handle and the magic bytes it is expecting. long longValue = BitConverter.ToInt64(lockFileBytes, 0); HandleRef remoteWindowHandle = new HandleRef(null, new IntPtr(longValue)); byte[] remoteExpectedMagic = new byte[MagicLengthInBytes]; Array.Copy(lockFileBytes, 8, remoteExpectedMagic, 0, MagicLengthInBytes); // Tell the singleInstanceMainForm that another instance is active. // Not a clean design to have callbacks going back and forth. // Hard to refactor since we're inside a loop. singleInstanceMainForm.NotifyExistingInstance(remoteWindowHandle, remoteExpectedMagic); // Reference remoteWindowHandle here so it won't be GC'ed until method returns. GC.KeepAlive(remoteWindowHandle); // Loop exit point 2: successful read of the lock file owned by an existing instance. return; } } } catch { } } // If any of the above steps fail, this might be caused by the other instance // shutting down for example, or still being in its startup phase. // In this case, sleep for 100 ms and retry the whole process. Thread.Sleep(PauseTimeBeforeLockRetry); remainingRetryAttempts--; // Loop exit point 3: proceed without auto-saving settings if even after 2 seconds the lock file couldn't be accessed. // This can happen for example if the first instance is running as Administrator and this instance is not. } try { AutoSave = new SettingsAutoSave(settingsProvider.CreateAutoSaveSchema(this), autoSaveFiles); // After creating the auto-save file, look for a local preferences file. // Create a working copy with correct schema first. SettingCopy localSettingsCopy = new SettingCopy(settingsProvider.CreateLocalSettingsSchema(this)); // And then create the local settings file which can overwrite values in default settings. LocalSettings = SettingsFile.Create( Path.Combine(AppDataSubFolder, GetDefaultSetting(SharedSettings.LocalPreferencesFileName)), localSettingsCopy); if (TryGetAutoSaveValue(LangSetting, out FileLocalizer localizer)) { currentLocalizer = localizer; } else { // Select best fit. currentLocalizer = Localizers.BestFit(registeredLocalizers); } // Fall back onto defaults if still null. currentLocalizer = currentLocalizer ?? defaultLocalizer ?? Localizer.Default; } catch { // Must dispose here, because Dispose() is never called if an exception is thrown in a constructor. ReleaseLockFile(lockFile); throw; } }