From 39449cb38e51c2554dbfa7fa8a45d2be7ca6d87e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:17:50 +0000 Subject: [PATCH 1/2] fix: migrate legacy dir.logs log4j property Fixes a crash on boot caused by dir.logs set in log4j2.properties, which is not supported in log4j2. Cannot use normal migration infra since log4j is initialized early in the boot processs (before classpath is fully set up). Issue: https://github.com/OpenIntegrationEngine/engine/issues/267 Signed-off-by: Mitch Gaffigan --- .../server/launcher/MirthLauncher.java | 27 +++++++++++++++ .../server/launcher/MirthLauncherTest.java | 34 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 server/test/com/mirth/connect/server/launcher/MirthLauncherTest.java diff --git a/server/src/com/mirth/connect/server/launcher/MirthLauncher.java b/server/src/com/mirth/connect/server/launcher/MirthLauncher.java index c34919a83..d5d94b3b1 100644 --- a/server/src/com/mirth/connect/server/launcher/MirthLauncher.java +++ b/server/src/com/mirth/connect/server/launcher/MirthLauncher.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -40,11 +41,13 @@ public class MirthLauncher { private static final String EXTENSIONS_DIR = "./extensions"; private static final String SERVER_LAUNCHER_LIB_DIR = "./server-launcher-lib"; private static final String MIRTH_PROPERTIES_FILE = "./conf/mirth.properties"; + private static final String LOG4J_PROPERTIES_FILE = "./conf/log4j2.properties"; private static final String PROPERTY_APP_DATA_DIR = "dir.appdata"; private static final String PROPERTY_INCLUDE_CUSTOM_LIB = "server.includecustomlib"; private static final String[] LOG4J_JAR_FILES = { "./server-lib/log4j/log4j-core-2.25.3.jar", "./server-lib/log4j/log4j-api-2.25.3.jar", "./server-lib/log4j/log4j-1.2-api-2.25.3.jar" }; + private static final String INVALID_LOG4J_PROPERTY = "dir.logs"; private static String appDataDir = null; @@ -53,6 +56,8 @@ public class MirthLauncher { public static void main(String[] args) { JarFile mirthClientCoreJarFile = null; try { + sanitizeLog4jConfiguration(new File(LOG4J_PROPERTIES_FILE)); + List classpathUrls = new ArrayList<>(); // Always add log4j for (String log4jJar : LOG4J_JAR_FILES) { @@ -128,6 +133,28 @@ public static void main(String[] args) { } } + private static void sanitizeLog4jConfiguration(File propertiesFile) { + if (!propertiesFile.exists() || !propertiesFile.isFile()) { + return; + } + + try { + List lines = FileUtils.readLines(propertiesFile, StandardCharsets.UTF_8); + if (lines.removeIf(MirthLauncher::isInvalidLog4jPropertyLine)) { + FileUtils.writeLines(propertiesFile, StandardCharsets.UTF_8.name(), lines, System.lineSeparator(), false); + } + } catch (IOException e) { + System.err.println("Failed to sanitize Log4j configuration: " + propertiesFile.getAbsolutePath()); + e.printStackTrace(); + } + } + + private static boolean isInvalidLog4jPropertyLine(String line) { + String trimmedLine = line.trim(); + int equalsIndex = trimmedLine.indexOf('='); + return equalsIndex >= 0 && trimmedLine.substring(0, equalsIndex).trim().equals(INVALID_LOG4J_PROPERTY); + } + // if we have an uninstall file, uninstall the listed extensions private static void uninstallPendingExtensions() throws Exception { File extensionsDir = new File(EXTENSIONS_DIR); diff --git a/server/test/com/mirth/connect/server/launcher/MirthLauncherTest.java b/server/test/com/mirth/connect/server/launcher/MirthLauncherTest.java new file mode 100644 index 000000000..0d5ecc97b --- /dev/null +++ b/server/test/com/mirth/connect/server/launcher/MirthLauncherTest.java @@ -0,0 +1,34 @@ +package com.mirth.connect.server.launcher; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.junit.Test; + +public class MirthLauncherTest { + + @Test + public void testSanitizeLog4jRemovesDirLogs() throws Exception { + File file = File.createTempFile("log4j2", ".properties"); + file.deleteOnExit(); + Files.writeString(file.toPath(), String.join(System.lineSeparator(), + "rootLogger = ERROR,stdout,fout", + "dir.logs = logs", + "property.log.dir = logs", + "appender.rolling.fileName = ${log.dir}/mirth.log"), StandardCharsets.UTF_8); + + Method method = MirthLauncher.class.getDeclaredMethod("sanitizeLog4jConfiguration", File.class); + method.setAccessible(true); + method.invoke(null, file); + + String fileContents = Files.readString(file.toPath(), StandardCharsets.UTF_8); + assertFalse(fileContents.contains("dir.logs")); + assertTrue(fileContents.contains("property.log.dir = logs")); + assertTrue(fileContents.contains("appender.rolling.fileName = ${log.dir}/mirth.log")); + } +} From cf6f4d5d9f5cf60c62bb65535cfc1f8f5a154043 Mon Sep 17 00:00:00 2001 From: Mitch Gaffigan Date: Fri, 27 Mar 2026 12:28:26 -0500 Subject: [PATCH 2/2] Clean up log4j migration code, add more tests Signed-off-by: Mitch Gaffigan --- .../server/launcher/Log4jMigrations.java | 43 +++++++ .../server/launcher/MirthLauncher.java | 26 +---- .../server/launcher/Log4jMigrationsTest.java | 108 ++++++++++++++++++ .../server/launcher/MirthLauncherTest.java | 34 ------ 4 files changed, 152 insertions(+), 59 deletions(-) create mode 100644 server/src/com/mirth/connect/server/launcher/Log4jMigrations.java create mode 100644 server/test/com/mirth/connect/server/launcher/Log4jMigrationsTest.java delete mode 100644 server/test/com/mirth/connect/server/launcher/MirthLauncherTest.java diff --git a/server/src/com/mirth/connect/server/launcher/Log4jMigrations.java b/server/src/com/mirth/connect/server/launcher/Log4jMigrations.java new file mode 100644 index 000000000..77833e290 --- /dev/null +++ b/server/src/com/mirth/connect/server/launcher/Log4jMigrations.java @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package com.mirth.connect.server.launcher; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.apache.commons.io.FileUtils; + +public class Log4jMigrations { + private static final String INVALID_LOG4J_PROPERTY = "dir.logs"; + + private Log4jMigrations() {} + + public static void migrateConfiguration(File propertiesFile) { + if (!propertiesFile.exists() || !propertiesFile.isFile()) { + return; + } + + try { + List lines = FileUtils.readLines(propertiesFile, StandardCharsets.UTF_8); + if (stripDirLogs(lines)) { + FileUtils.writeLines(propertiesFile, StandardCharsets.UTF_8.name(), lines, System.lineSeparator(), false); + } + } catch (IOException e) { + System.err.println("Failed to migrate Log4j configuration: " + propertiesFile.getAbsolutePath()); + e.printStackTrace(); + } + } + + private static boolean stripDirLogs(List lines) { + return lines.removeIf(line -> isPropertyLine(line, INVALID_LOG4J_PROPERTY)); + } + + private static boolean isPropertyLine(String line, String propertyName) { + String trimmedLine = line.trim(); + int equalsIndex = trimmedLine.indexOf('='); + return equalsIndex >= 0 && trimmedLine.substring(0, equalsIndex).trim().equals(propertyName); + } +} \ No newline at end of file diff --git a/server/src/com/mirth/connect/server/launcher/MirthLauncher.java b/server/src/com/mirth/connect/server/launcher/MirthLauncher.java index d5d94b3b1..cd3d343fd 100644 --- a/server/src/com/mirth/connect/server/launcher/MirthLauncher.java +++ b/server/src/com/mirth/connect/server/launcher/MirthLauncher.java @@ -15,7 +15,6 @@ import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -47,7 +46,6 @@ public class MirthLauncher { private static final String[] LOG4J_JAR_FILES = { "./server-lib/log4j/log4j-core-2.25.3.jar", "./server-lib/log4j/log4j-api-2.25.3.jar", "./server-lib/log4j/log4j-1.2-api-2.25.3.jar" }; - private static final String INVALID_LOG4J_PROPERTY = "dir.logs"; private static String appDataDir = null; @@ -56,7 +54,7 @@ public class MirthLauncher { public static void main(String[] args) { JarFile mirthClientCoreJarFile = null; try { - sanitizeLog4jConfiguration(new File(LOG4J_PROPERTIES_FILE)); + Log4jMigrations.migrateConfiguration(new File(LOG4J_PROPERTIES_FILE)); List classpathUrls = new ArrayList<>(); // Always add log4j @@ -133,28 +131,6 @@ public static void main(String[] args) { } } - private static void sanitizeLog4jConfiguration(File propertiesFile) { - if (!propertiesFile.exists() || !propertiesFile.isFile()) { - return; - } - - try { - List lines = FileUtils.readLines(propertiesFile, StandardCharsets.UTF_8); - if (lines.removeIf(MirthLauncher::isInvalidLog4jPropertyLine)) { - FileUtils.writeLines(propertiesFile, StandardCharsets.UTF_8.name(), lines, System.lineSeparator(), false); - } - } catch (IOException e) { - System.err.println("Failed to sanitize Log4j configuration: " + propertiesFile.getAbsolutePath()); - e.printStackTrace(); - } - } - - private static boolean isInvalidLog4jPropertyLine(String line) { - String trimmedLine = line.trim(); - int equalsIndex = trimmedLine.indexOf('='); - return equalsIndex >= 0 && trimmedLine.substring(0, equalsIndex).trim().equals(INVALID_LOG4J_PROPERTY); - } - // if we have an uninstall file, uninstall the listed extensions private static void uninstallPendingExtensions() throws Exception { File extensionsDir = new File(EXTENSIONS_DIR); diff --git a/server/test/com/mirth/connect/server/launcher/Log4jMigrationsTest.java b/server/test/com/mirth/connect/server/launcher/Log4jMigrationsTest.java new file mode 100644 index 000000000..126c5f3da --- /dev/null +++ b/server/test/com/mirth/connect/server/launcher/Log4jMigrationsTest.java @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Mitch Gaffigan + +package com.mirth.connect.server.launcher; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.PosixFilePermission; +import java.util.EnumSet; +import java.util.Set; + +import org.junit.Assume; +import org.junit.Test; + +public class Log4jMigrationsTest { + + @Test + public void testMigrateLog4jRemovesDirLogs() throws Exception { + File file = File.createTempFile("log4j2", ".properties"); + file.deleteOnExit(); + Files.writeString(file.toPath(), String.join(System.lineSeparator(), + "rootLogger = ERROR,stdout,fout", + "dir.logs = logs", + "property.log.dir = logs", + "appender.rolling.fileName = ${log.dir}/mirth.log"), StandardCharsets.UTF_8); + + Log4jMigrations.migrateConfiguration(file); + + String fileContents = Files.readString(file.toPath(), StandardCharsets.UTF_8); + assertFalse(fileContents.contains("dir.logs")); + assertTrue(fileContents.contains("property.log.dir = logs")); + assertTrue(fileContents.contains("appender.rolling.fileName = ${log.dir}/mirth.log")); + } + + @Test + public void testMigrateLog4jDoesNotRewriteCleanFile() throws Exception { + File file = File.createTempFile("log4j2-clean", ".properties"); + file.deleteOnExit(); + String contents = String.join(System.lineSeparator(), + "rootLogger = ERROR,stdout,fout", + "property.log.dir = logs", + "appender.rolling.fileName = ${log.dir}/mirth.log"); + Files.writeString(file.toPath(), contents, StandardCharsets.UTF_8); + + FileTime expectedModifiedTime = FileTime.fromMillis(1_700_000_000_000L); + Files.setLastModifiedTime(file.toPath(), expectedModifiedTime); + + Log4jMigrations.migrateConfiguration(file); + + assertEquals(expectedModifiedTime, Files.getLastModifiedTime(file.toPath())); + assertEquals(contents, Files.readString(file.toPath(), StandardCharsets.UTF_8)); + } + + @Test + public void testMigrateLog4jFailsGracefullyWithReadOnlyFile() throws Exception { + File file = File.createTempFile("log4j2-readonly", ".properties"); + file.deleteOnExit(); + String contents = String.join(System.lineSeparator(), + "rootLogger = ERROR,stdout,fout", + "dir.logs = logs", + "property.log.dir = logs"); + Path path = file.toPath(); + Files.writeString(path, contents, StandardCharsets.UTF_8); + + Assume.assumeTrue(Files.getFileStore(path).supportsFileAttributeView("posix")); + + ByteArrayOutputStream errBytes = new ByteArrayOutputStream(); + PrintStream originalErr = System.err; + Set originalPermissions = Files.getPosixFilePermissions(path); + + try { + Files.setPosixFilePermissions(path, EnumSet.of(PosixFilePermission.OWNER_READ)); + assertFalse(Files.isWritable(path)); + System.setErr(new PrintStream(errBytes, true, StandardCharsets.UTF_8.name())); + + Log4jMigrations.migrateConfiguration(file); + } finally { + Files.setPosixFilePermissions(path, originalPermissions); + System.setErr(originalErr); + } + + String errOutput = errBytes.toString(StandardCharsets.UTF_8.name()); + assertTrue(errOutput.contains("Failed to migrate Log4j configuration")); + assertEquals(contents, Files.readString(path, StandardCharsets.UTF_8)); + } + + @Test + public void testMigrateLog4jIgnoresMissingFile() throws Exception { + File parentDir = Files.createTempDirectory("log4j2-missing").toFile(); + parentDir.deleteOnExit(); + File file = new File(parentDir, "missing-log4j2.properties"); + + assertFalse(file.exists()); + + Log4jMigrations.migrateConfiguration(file); + + assertFalse(file.exists()); + } +} \ No newline at end of file diff --git a/server/test/com/mirth/connect/server/launcher/MirthLauncherTest.java b/server/test/com/mirth/connect/server/launcher/MirthLauncherTest.java deleted file mode 100644 index 0d5ecc97b..000000000 --- a/server/test/com/mirth/connect/server/launcher/MirthLauncherTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.mirth.connect.server.launcher; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.io.File; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; - -import org.junit.Test; - -public class MirthLauncherTest { - - @Test - public void testSanitizeLog4jRemovesDirLogs() throws Exception { - File file = File.createTempFile("log4j2", ".properties"); - file.deleteOnExit(); - Files.writeString(file.toPath(), String.join(System.lineSeparator(), - "rootLogger = ERROR,stdout,fout", - "dir.logs = logs", - "property.log.dir = logs", - "appender.rolling.fileName = ${log.dir}/mirth.log"), StandardCharsets.UTF_8); - - Method method = MirthLauncher.class.getDeclaredMethod("sanitizeLog4jConfiguration", File.class); - method.setAccessible(true); - method.invoke(null, file); - - String fileContents = Files.readString(file.toPath(), StandardCharsets.UTF_8); - assertFalse(fileContents.contains("dir.logs")); - assertTrue(fileContents.contains("property.log.dir = logs")); - assertTrue(fileContents.contains("appender.rolling.fileName = ${log.dir}/mirth.log")); - } -}