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; + } + } +}