diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/actions/OpenWorkspaceAction.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/actions/OpenWorkspaceAction.java
index 26b23e36f79..7a128f3f142 100644
--- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/actions/OpenWorkspaceAction.java
+++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/actions/OpenWorkspaceAction.java
@@ -14,6 +14,7 @@
package org.eclipse.ui.internal.ide.actions;
import java.net.MalformedURLException;
+import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -240,10 +241,16 @@ public void restart(String workspacePath) {
private boolean canRestartWithWorkspace(String workspacePath) throws IllegalStateException {
Path selectedWorkspace = Path.of(workspacePath);
try {
- String workspaceLockDetails = WorkspaceLock.getWorkspaceLockDetails(selectedWorkspace.toUri().toURL());
- if (workspaceLockDetails == null) {
+ URL url = selectedWorkspace.toUri().toURL();
+ if (!WorkspaceLock.isWorkspaceLocked(url)) {
return true;
}
+ String workspaceLockDetails = WorkspaceLock.getWorkspaceLockDetails(url);
+ if (workspaceLockDetails == null) {
+ // can only happen if the workspace is locked by an older Eclipse
+ // which doesn't write lock details
+ workspaceLockDetails = ""; //$NON-NLS-1$
+ }
WorkspaceLock.showWorkspaceLockedDialog(window.getShell(), workspacePath, workspaceLockDetails);
} catch (MalformedURLException e) {
MessageDialog.openError(window.getShell(), WorkbenchMessages.OpenWorkspaceAction_invalidWorkspacePath,
diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkspaceLock.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkspaceLock.java
index 501366f362a..c5014735ec9 100644
--- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkspaceLock.java
+++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/WorkspaceLock.java
@@ -13,9 +13,14 @@
*******************************************************************************/
package org.eclipse.ui.internal;
+import java.io.File;
+import java.io.IOException;
import java.io.InputStream;
+import java.io.RandomAccessFile;
import java.net.URISyntaxException;
import java.net.URL;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
@@ -101,7 +106,8 @@ public static String getWorkspaceLockDetails(URL workspaceUrl) {
}
/**
- * Returns the lock file.
+ * Returns the lock info file (the file where information about last
+ * workspace lock owner is saved).
*
* @param workspaceUrl the URL of selected workspace
* @return the path to the .lock_info file within the specified
@@ -117,6 +123,25 @@ public static Path getLockInfoFile(URL workspaceUrl) {
}
}
+ /**
+ * Returns the workspace lock file for given workspace URL (the file which is
+ * actually locked if an Eclipse application is using this workspace).
+ *
+ * @param workspaceUrl the URL of selected workspace
+ * @return the path to the .lock file within the specified
+ * workspace, or null if the workspace URL cannot be
+ * converted to a valid URI
+ */
+ public static Path getLockFile(URL workspaceUrl) {
+ // See org.eclipse.osgi.internal.location.BasicLocation.DEFAULT_LOCK_FILENAME
+ Path lockFile = Path.of(".metadata", ".lock"); //$NON-NLS-1$ //$NON-NLS-2$
+ try {
+ return Path.of(URIUtil.toURI(workspaceUrl)).resolve(lockFile);
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ }
+
/**
* Opens an error dialog indicating that the selected workspace is locked by
* another Eclipse instance.
@@ -133,11 +158,58 @@ public static Path getLockInfoFile(URL workspaceUrl) {
*/
public static void showWorkspaceLockedDialog(Shell shell, String workspacePath, String workspaceLockOwner) {
String lockMessage = NLS.bind(WorkbenchMessages.IDEApplication_workspaceCannotLockMessage2, workspacePath);
- String wsLockedError = lockMessage + System.lineSeparator() + System.lineSeparator()
- + NLS.bind(WorkbenchMessages.IDEApplication_workspaceLockMessage, workspaceLockOwner);
-
+ String wsLockedError = lockMessage;
+ if (workspaceLockOwner != null && !workspaceLockOwner.isBlank()) {
+ String lockDetails = NLS.bind(WorkbenchMessages.IDEApplication_workspaceLockMessage, workspaceLockOwner);
+ wsLockedError += System.lineSeparator() + System.lineSeparator() + lockDetails;
+ }
MessageDialog.openError(shell,
WorkbenchMessages.IDEApplication_workspaceCannotLockTitle, wsLockedError);
}
+ /**
+ * Checks if the given workspace path is locked by another Eclipse instance.
+ *
+ * @param workspaceUrl the URL of workspace to check for lock
+ * @return true if the workspace is locked, false
+ * otherwise
+ */
+ public static boolean isWorkspaceLocked(URL workspaceUrl) {
+ Path lockFile = getLockFile(workspaceUrl);
+ if (lockFile == null || !Files.exists(lockFile)) {
+ return false;
+ }
+ return isLocked(lockFile.toFile());
+ }
+
+ /**
+ * Follows the same strategy as
+ * org.eclipse.osgi.internal.location.Locker_JavaNio#isLocked(),
+ * trying to lock a file using Java NIO to check if the file is locked or not
+ * already.
+ *
+ * @return true if the file is definitely locked by any process,
+ * false otherwise
+ */
+ private static boolean isLocked(File lockFile) {
+ try (RandomAccessFile temp = new RandomAccessFile(lockFile, "rw")) { //$NON-NLS-1$
+ try {
+ try (FileLock tempLock = temp.getChannel().tryLock(0, 1, false)) {
+ if (tempLock != null) {
+ // able to lock: it was not locked before
+ return false;
+ }
+ // is locked by some process
+ return true;
+ }
+ } catch (OverlappingFileLockException e) {
+ // is locked by some process
+ return true;
+ }
+ } catch (IOException e) {
+ // assume not locked if we have any troubles getting access to it
+ return false;
+ }
+ }
+
}
diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/UiTestSuite.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/UiTestSuite.java
index ab0aeffaf71..ec0f9006590 100644
--- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/UiTestSuite.java
+++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/UiTestSuite.java
@@ -108,7 +108,8 @@
WorkbenchDatabindingTest.class,
ChooseWorkspaceDialogTests.class,
ViewerItemsLimitTest.class,
- OpenCloseTest.class
+ OpenCloseTest.class,
+ WorkspaceLockTest.class
})
public class UiTestSuite {
}
diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/WorkspaceLockTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/WorkspaceLockTest.java
new file mode 100644
index 00000000000..19225375621
--- /dev/null
+++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/WorkspaceLockTest.java
@@ -0,0 +1,143 @@
+/*******************************************************************************
+ * Copyright (c) 2026 Andrey Loskutov .
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Andrey Loskutov - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.ui.tests;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.net.URL;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Properties;
+
+import org.eclipse.core.tests.harness.FileSystemHelper;
+import org.eclipse.ui.internal.WorkspaceLock;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link org.eclipse.ui.internal.WorkspaceLock}
+ */
+public class WorkspaceLockTest {
+
+ Path tempDir = FileSystemHelper.getRandomLocation().toPath().resolve("WorkspaceLockTest");
+
+ /**
+ * Test method for {@link org.eclipse.ui.internal.WorkspaceLock#getWorkspaceLockDetails(java.net.URL)}.
+ */
+ @Test
+ public void testGetWorkspaceLockDetails() throws Exception {
+ URL workspaceUrl = tempDir.toUri().toURL();
+
+ // Test when no lock info file exists
+ String details = WorkspaceLock.getWorkspaceLockDetails(workspaceUrl);
+ assertNull(details, "Should return null when no lock info file exists");
+
+ // Create .metadata/.lock_info with properties
+ Path metadataDir = tempDir.resolve(".metadata");
+ Files.createDirectories(metadataDir);
+ Path lockInfoFile = metadataDir.resolve(".lock_info");
+
+ Properties props = new Properties();
+ props.setProperty(WorkspaceLock.USER, "testuser");
+ props.setProperty(WorkspaceLock.HOST, "testhost");
+ props.setProperty(WorkspaceLock.DISPLAY, ":0");
+ props.setProperty(WorkspaceLock.PROCESS_ID, "1234");
+ try (var os = Files.newOutputStream(lockInfoFile)) {
+ props.store(os, null);
+ }
+
+ details = WorkspaceLock.getWorkspaceLockDetails(workspaceUrl);
+ assertNotNull(details, "Should return details when lock info file exists");
+ assertTrue(details.contains("testuser"), "Should contain user info");
+ assertTrue(details.contains("testhost"), "Should contain host info");
+ assertTrue(details.contains(":0"), "Should contain display info");
+ assertTrue(details.contains("1234"), "Should contain PID info");
+ }
+
+ /**
+ * Test method for {@link org.eclipse.ui.internal.WorkspaceLock#getLockInfoFile(java.net.URL)}.
+ */
+ @Test
+ public void testGetLockInfoFile() throws Exception {
+ URL workspaceUrl = tempDir.toUri().toURL();
+ Path lockInfoFile = WorkspaceLock.getLockInfoFile(workspaceUrl);
+ assertNotNull(lockInfoFile, "Should return a path");
+ assertEquals(tempDir.resolve(".metadata").resolve(".lock_info"), lockInfoFile,
+ "Should point to .metadata/.lock_info");
+ }
+
+ /**
+ * Test method for {@link org.eclipse.ui.internal.WorkspaceLock#getLockFile(java.net.URL)}.
+ */
+ @Test
+ public void testGetLockFile() throws Exception {
+ URL workspaceUrl = tempDir.toUri().toURL();
+ Path lockFile = WorkspaceLock.getLockFile(workspaceUrl);
+ assertNotNull(lockFile, "Should return a path");
+ assertEquals(tempDir.resolve(".metadata").resolve(".lock"), lockFile, "Should point to .metadata/.lock");
+ }
+
+ /**
+ * Test method for {@link org.eclipse.ui.internal.WorkspaceLock#isWorkspaceLocked(java.net.URL)}.
+ */
+ @Test
+ public void testIsWorkspaceLocked() throws Exception {
+ URL workspaceUrl = tempDir.toUri().toURL();
+
+ // Test when no lock file exists
+ assertFalse(WorkspaceLock.isWorkspaceLocked(workspaceUrl), "Should not be locked when no lock file exists");
+
+ // Create .metadata/.lock file & lock it
+ Path metadataDir = tempDir.resolve(".metadata");
+ Files.createDirectories(metadataDir);
+ Path lockFile = metadataDir.resolve(".lock");
+ try (RandomAccessFile lock = lock(lockFile.toFile())) {
+ assertTrue(WorkspaceLock.isWorkspaceLocked(workspaceUrl), "Should be locked");
+ }
+ }
+
+ /**
+ * Mimics
+ * {@linkplain org.eclipse.osgi.internal.location.Locker_JavaNio#lock(File)}
+ * locking a file using Java NIO and returning the RandomAccessFile if
+ * successful, null if the file is already locked by another process.
+ */
+ static RandomAccessFile lock(File lockFile) {
+ try {
+ RandomAccessFile raFile = new RandomAccessFile(lockFile, "rw");
+ try {
+ FileLock lock = raFile.getChannel().tryLock(0, 1, false);
+ if (lock == null) {
+ raFile.close();
+ return null;
+ }
+ return raFile;
+ } catch (OverlappingFileLockException e) {
+ raFile.close();
+ return null;
+ }
+ } catch (IOException e) {
+ // already locked by some process, should not happen in test
+ return null;
+ }
+ }
+}