From 14f5ce1502b82fe98f97571ac511cb28a7a3df67 Mon Sep 17 00:00:00 2001 From: mark-sil <83427558+mark-sil@users.noreply.github.com> Date: Thu, 2 Jul 2026 11:36:26 -0400 Subject: [PATCH] Fix LT-21913: Prevent crash when restoring over an open project Restoring a backup over a project that was open (and therefore file-locked) overwrote the project's data and then crashed. Fix it in two places: - RestoreCurrentProject now checks if the project is locked before doing anything destructive (overwriting files). - ExecuteWithAppsShutDown adds a narrow safety net that catches a LcmFileLockedException when reopening. This covering the small window where the project could get locked between the up-front check and the reopen. Any other StartupException is left to propagate so unexpected failures still generate a crash report. Co-Authored-By: Claude Opus 4.8 --- Src/Common/FieldWorks/FieldWorks.cs | 40 +++++++++++++++++++ .../Properties/Resources.Designer.cs | 9 +++++ .../FieldWorks/Properties/Resources.resx | 4 ++ 3 files changed, 53 insertions(+) diff --git a/Src/Common/FieldWorks/FieldWorks.cs b/Src/Common/FieldWorks/FieldWorks.cs index 3680193fb8..1947cc444c 100644 --- a/Src/Common/FieldWorks/FieldWorks.cs +++ b/Src/Common/FieldWorks/FieldWorks.cs @@ -38,6 +38,7 @@ using SIL.LCModel.Core.WritingSystems; using SIL.LCModel.Core.KernelInterfaces; using SIL.LCModel; +using SIL.LCModel.DomainServices; using SIL.LCModel.DomainServices.BackupRestore; using SIL.LCModel.DomainServices.DataMigration; using SIL.LCModel.Infrastructure; @@ -2539,6 +2540,24 @@ internal static void HandleRestoreRequest(Form dialogOwner, FwRestoreProjectSett private static void RestoreCurrentProject(FwRestoreProjectSettings restoreSettings, Form dialogOwner) { + // If the project we are about to restore already exists and is open (its data + // file is locked) in some other program, then block the restore before we overwrite any + // files. Otherwise we would destroy the opened project's data and eventually crash trying to + // reopen the locked project (LT-21913). + // We skip this check when 'this' process is the one that has the project open, since we already + // prompted the user and ExecuteWithAppsShutDown will release our own lock before restoring. + var projectPath = restoreSettings.Settings.FullProjectPath; + var weHaveProjectOpen = s_projectId != null && + s_projectId.IsSameLocalProject(new ProjectId(projectPath)); + if (!weHaveProjectOpen && restoreSettings.Settings.ProjectExists && + ProjectLockingService.IsProjectLocked(projectPath)) + { + MessageBox.Show(dialogOwner, + string.Format(Properties.Resources.ksCannotRestoreProjectInUse, restoreSettings.Settings.ProjectName), + Properties.Resources.ksErrorCaption, MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + // When we get here we can safely do the backup of the project because we either // have no cache (and no other process has this project open), or we are the // process that has this project open. (FWR-3344) @@ -3806,6 +3825,27 @@ private static void ExecuteWithAppsShutDown(Func action) s_projectId = projId; // Process needs to know its project } + catch (StartupException e) when (e.InnerException is LcmFileLockedException) + { + // The action (e.g. a restore) succeeded, but the project could not be reopened + // afterwards because it is now open (and its file locked) in another program + // (LT-21913). This is a safety net for the narrow window where the project gets + // locked after RestoreCurrentProject's up-front check but before we reopen it + // here. Inform the user with a message box rather than crashing with the green + // error-reporting dialog. Any other StartupException is left to propagate so + // that unexpected failures still generate a crash report. + if (s_cache != null) + { + s_cache.Dispose(); + s_cache = null; + } + // Dispose the partially-created application so that we end up with no running + // apps (which lets the process shut down cleanly, just as if the action had + // failed or been cancelled). + GracefullyShutDownApp(s_flexApp); + MessageBox.Show(e.Message, Properties.Resources.ksErrorCaption, + MessageBoxButtons.OK, MessageBoxIcon.Error); + } finally { s_allowFinalShutdown = allowFinalShutdownOrigValue; // mustn't suppress any longer (unless we already were). diff --git a/Src/Common/FieldWorks/Properties/Resources.Designer.cs b/Src/Common/FieldWorks/Properties/Resources.Designer.cs index b107eca209..7cd77eefc6 100644 --- a/Src/Common/FieldWorks/Properties/Resources.Designer.cs +++ b/Src/Common/FieldWorks/Properties/Resources.Designer.cs @@ -136,6 +136,15 @@ internal static string ksCancelButton { return ResourceManager.GetString("ksCancelButton", resourceCulture); } } + + /// + /// Looks up a localized string similar to The project '{0}' cannot be restored because it is currently open in another program. Please close the project everywhere it is open and try the restore again.. + /// + internal static string ksCannotRestoreProjectInUse { + get { + return ResourceManager.GetString("ksCannotRestoreProjectInUse", resourceCulture); + } + } /// /// Looks up a localized string similar to FieldWorks is unable to move the following projects to the new location.. diff --git a/Src/Common/FieldWorks/Properties/Resources.resx b/Src/Common/FieldWorks/Properties/Resources.resx index beb161b02f..2f026df868 100644 --- a/Src/Common/FieldWorks/Properties/Resources.resx +++ b/Src/Common/FieldWorks/Properties/Resources.resx @@ -234,6 +234,10 @@ Ensure that this folder is shared and that you have permission to access it. Do you want to continue with the restore? + + The project '{0}' cannot be restored because it is currently open in another program. Please close the project everywhere it is open and try the restore again. + Message shown when a restore is attempted over a project that is still open (and its data file is locked) in another program. {0} is the project name. + Could not change Projects location