This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit ebbad914e96176e7ff7efb5f1ed194709083d9dd Author: opencode <[email protected]> AuthorDate: Thu Oct 8 16:43:59 2026 +0200 Restore the previous web application when legacy application migration cannot place the migrated application. migrateLegacyApp() moves the previous version of the application from the deployment location to a temporary location before moving the newly migrated application into place. Previously, if that final move failed, the original code left the previous application stranded in the temporary location: it was neither restored to the deployment location nor cleaned up, so the application disappeared from the deployment location and the stranded copy was never referenced again. The completion of each of the two moves is now tracked explicitly. When the previous application has been moved aside but the migrated application cannot be placed, the previous application is restored to the deployment location and the event is logged. If restoring it also fails, the copy is deliberately retained for manual recovery and a warning naming both locations is logged, rather than risking deletion of the only remaining copy. The previous application copy is only discarded once the migrated application is confirmed to be in place. --- java/org/apache/catalina/startup/HostConfig.java | 55 +++++++++++++++++++++- .../catalina/startup/LocalStrings.properties | 2 + webapps/docs/changelog.xml | 11 +++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/java/org/apache/catalina/startup/HostConfig.java b/java/org/apache/catalina/startup/HostConfig.java index da2baca502..3f2ac540b6 100644 --- a/java/org/apache/catalina/startup/HostConfig.java +++ b/java/org/apache/catalina/startup/HostConfig.java @@ -1235,7 +1235,13 @@ public class HostConfig implements LifecycleListener { */ protected void migrateLegacyApp(File source, File destination) { File tempNew = null; - File tempOld; + File tempOld = null; + // Track which moves completed so cleanup can decide whether the previous web application + // needs to be restored rather than discarded. These are set only when the corresponding + // move returns, so a move that fails part way through (for example a cross-file-system + // copy that cannot delete its source) leaves the flag false. + boolean previousMovedAside = false; + boolean migratedPlaced = false; try { tempNew = File.createTempFile("new", null, host.getLegacyAppBaseFile()); tempOld = File.createTempFile("old", null, host.getLegacyAppBaseFile()); @@ -1256,8 +1262,10 @@ public class HostConfig implements LifecycleListener { // Use rename if (destination.exists()) { Files.move(destination.toPath(), tempOld.toPath()); + previousMovedAside = true; } Files.move(tempNew.toPath(), destination.toPath()); + migratedPlaced = true; // Only delete the previous webapp if everything went fine ExpandWar.delete(tempOld); @@ -1268,6 +1276,51 @@ public class HostConfig implements LifecycleListener { if (tempNew != null && tempNew.exists()) { ExpandWar.delete(tempNew); } + if (tempOld != null) { + if (migratedPlaced) { + // The migrated application is in place. The previous application, if one was + // moved aside, is no longer needed. + ExpandWar.delete(tempOld); + } else if (previousMovedAside && tempOld.exists()) { + // The previous application was moved aside but the migrated application could + // not be placed. Restore the previous application so it is not lost. tempOld is + // known to hold the complete previous application because previousMovedAside is + // only set once the move out of the way returned successfully. + restorePreviousApplication(tempOld, destination); + } + // If the move out of the way never completed (previousMovedAside is false) tempOld + // is either a placeholder, an incomplete copy or, in the rare case of a + // cross-file-system move whose source deletion failed, possibly the only remaining + // copy of the previous application while the destination is incomplete. In that + // situation deleting tempOld could destroy data, so it is retained for manual + // recovery instead of being promoted over the destination. + } + } + } + + + /** + * Restore a previously migrated-out web application that is held in {@code tempOld} back to + * {@code destination}. Any incomplete content left at the destination by a failed move is + * removed first. If the restore cannot be completed, the copy in {@code tempOld} is retained so + * that an administrator can recover the original web application manually. + * + * @param tempOld the temporary location holding the complete previous web application + * @param destination the location the previous web application should be restored to + */ + private void restorePreviousApplication(File tempOld, File destination) { + try { + if (destination.exists()) { + // Remove the partial content left by the failed move of the migrated application. + // The previous application is safely held in tempOld (previousMovedAside is set) so + // this does not risk the original data. + ExpandWar.delete(destination); + } + Files.move(tempOld.toPath(), destination.toPath()); + log.warn(sm.getString("hostConfig.migrateRestore", destination)); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("hostConfig.migrateRestoreError", tempOld, destination), t); } } diff --git a/java/org/apache/catalina/startup/LocalStrings.properties b/java/org/apache/catalina/startup/LocalStrings.properties index 64493719d3..d19bc76a42 100644 --- a/java/org/apache/catalina/startup/LocalStrings.properties +++ b/java/org/apache/catalina/startup/LocalStrings.properties @@ -154,6 +154,8 @@ hostConfig.jmx.register=Register context [{0}] failed hostConfig.jmx.unregister=Unregister context [{0}] failed hostConfig.migrateApp.threaded.error=Error waiting for multi-thread migration of legacy applications to complete hostConfig.migrateError=Migration failure +hostConfig.migrateRestore=The previous web application was restored at [{0}] after the migrated application could not be placed +hostConfig.migrateRestoreError=Unable to restore the previous web application from [{0}] to [{1}]. The copy at [{0}] has been retained for manual recovery. hostConfig.reload=Reloading context [{0}] hostConfig.resourceNotAbsolute=Unable to remove resource from context [{0}] since [{1}] is not an absolute path hostConfig.start=HostConfig: Processing START diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 59d9dec912..51ad82f9d4 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -1962,6 +1962,17 @@ <code>ServletContainerInitializer.onStartup()</code>, misconfiguring the application at runtime. (remm) </fix> + <fix> + When automatic migration of a legacy Java EE web application from + the <code>legacyAppBase</code> to the <code>appBase</code> failed + after the previous version of the application had been moved aside, + the previous application was left in a temporary location from which + it was neither restored nor cleaned up, making it effectively lost. + The previous application is now restored to the deployment location + when the migrated application cannot be placed, and is retained with + a warning in the temporary location only in the rare cases where + restoring it is not possible. (remm) + </fix> </changelog> </subsection> <subsection name="Coyote"> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
