From 67e12fd2d511f7c6f087690af096e0e9b9cb2b41 Mon Sep 17 00:00:00 2001 From: Patrick Ziegler Date: Tue, 8 Sep 2026 22:36:16 +0200 Subject: [PATCH] Make sure linked preferences remain in-sync after external file change When a preference file is changed by an external process (e.g. a Git update), it may happen that the preference node for projects that link to this file are not updated properly. Those projects will then continue working with outdated preferences, until either the application is restarted or the preference file is manually updated. --- .../localstore/RefreshLocalVisitor.java | 9 ++- .../resources/ProjectPreferencesTest.java | 78 ++++++++++++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/RefreshLocalVisitor.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/RefreshLocalVisitor.java index ee136683f1c..9dbe5773b49 100644 --- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/RefreshLocalVisitor.java +++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/RefreshLocalVisitor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2015 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -174,6 +174,13 @@ protected void resourceChanged(UnifiedTreeNode node, Resource target) { if (info == null) { return; } + if (target.getType() == IResource.FILE && (target.isLinked() || !target.equals(node.getResource()))) { + try { + ((File) target).updateMetadataFiles(); + } catch (CoreException e) { + errors.merge(e.getStatus()); + } + } target.getLocalManager().updateLocalSync(info, node.getLastModified()); info.incrementContentId(); // forget content-related caching flags diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/resources/ProjectPreferencesTest.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/resources/ProjectPreferencesTest.java index 4b3dfb79583..2c3a4332039 100644 --- a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/resources/ProjectPreferencesTest.java +++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/resources/ProjectPreferencesTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2004, 2025 IBM Corporation and others. + * Copyright (c) 2004, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -25,11 +25,13 @@ import static org.eclipse.core.tests.resources.ResourceTestUtil.waitForBuild; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -53,6 +55,7 @@ import org.eclipse.core.internal.preferences.EclipsePreferences; import org.eclipse.core.internal.resources.ProjectPreferences; import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; @@ -70,6 +73,7 @@ import org.eclipse.core.runtime.preferences.IPreferencesService; import org.eclipse.core.runtime.preferences.IScopeContext; import org.eclipse.core.runtime.preferences.InstanceScope; +import org.eclipse.core.tests.harness.FileSystemHelper; import org.eclipse.core.tests.resources.util.WorkspaceResetExtension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -1477,4 +1481,76 @@ public void testSettingsFolderCreatedOutsideWorkspace() throws CoreException, Ba assertEquals(value, prefs1.get(key, null)); } + @Test + public void testSymLinkedProjectPreferences() throws CoreException, BackingStoreException, IOException { + assumeTrue(FileSystemHelper.canCreateSymLinks(), "only relevant for platforms supporting symbolic links"); + doTestLinkedProjectPreferences((prefFile1, prefFile2) -> { + IPath prefPath1 = prefFile1.getLocation(); + IPath prefPath2 = prefFile2.getLocation(); + FileSystemHelper.createSymLink(prefPath2.toFile().getParentFile(), prefPath2.lastSegment(), + prefPath1.toOSString(), true); + prefFile2.refreshLocal(IResource.DEPTH_INFINITE, createTestMonitor()); + assertTrue(prefFile2.exists()); + assertFalse(prefFile2.isLinked()); + }); + } + + @Test + public void testLinkedProjectPreferences() throws CoreException, BackingStoreException, IOException { + doTestLinkedProjectPreferences((prefFile1, prefFile2) -> { + prefFile2.createLink(prefFile1.getLocationURI(), IResource.ALLOW_MISSING_LOCAL, createTestMonitor()); + assertTrue(prefFile2.exists()); + assertTrue(prefFile2.isLinked()); + }); + } + + @Test + public void testLinkedProjectPreferencesFolder() throws CoreException, BackingStoreException, IOException { + doTestLinkedProjectPreferences((prefFile1, prefFile2) -> { + IFolder prefFolder1 = (IFolder) prefFile1.getParent(); + IFolder prefFolder2 = (IFolder) prefFile2.getParent(); + prefFolder2.delete(true, createTestMonitor()); + prefFolder2.createLink(prefFolder1.getLocationURI(), IResource.ALLOW_MISSING_LOCAL, createTestMonitor()); + assertTrue(prefFolder2.exists()); + assertTrue(prefFolder2.isLinked()); + }); + } + + private void doTestLinkedProjectPreferences(FailableBiConsumer consumer) + throws CoreException, BackingStoreException, IOException { + String nodeName = "testNode"; + String prefName = "key"; + + IProject project1 = getProject(createUniqueString()); + project1.create(createTestMonitor()); + project1.open(createTestMonitor()); + + IEclipsePreferences prefs1 = new ProjectScope(project1).getNode(nodeName); + prefs1.putInt(prefName, 1); + prefs1.flush(); + + IProject project2 = getProject(createUniqueString()); + project2.create(createTestMonitor()); + project2.open(createTestMonitor()); + + IFile prefFile1 = getFileInWorkspace(project1, nodeName); + IFile prefFile2 = getFileInWorkspace(project2, nodeName); + consumer.accept(prefFile1, prefFile2); + + IEclipsePreferences prefs2 = new ProjectScope(project2).getNode(nodeName); + // Simple check whether shared preferences work + assertEquals(1, prefs2.getInt(prefName, -1), "Linked preferences no longer in sync"); + + // Synchronize shared preferences after external file change + Files.writeString(prefFile1.getLocation().toPath(), "%s=4".formatted(prefName)); + touchInFilesystem(prefFile1); + assertNotEquals(4, prefs2.getInt(prefName, -1), "Linked preferences updated without refresh"); + prefFile1.refreshLocal(IResource.DEPTH_ZERO, createTestMonitor()); + assertEquals(4, prefs2.getInt(prefName, -1), "Linked preferences no longer in sync"); + } + + private static interface FailableBiConsumer { + void accept(T t, U u) throws IOException, CoreException; + } + }