| /******************************************************************************* |
| * Copyright (c) 2016 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 <loskutov@gmx.de> - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.ui.editors.tests; |
| |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import java.nio.file.Files; |
| import java.nio.file.attribute.FileTime; |
| import java.util.Arrays; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| import org.eclipse.swt.widgets.Display; |
| |
| import org.eclipse.core.internal.localstore.FileSystemResourceManager; |
| import org.eclipse.core.internal.resources.File; |
| import org.eclipse.core.internal.resources.Workspace; |
| |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.ILog; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IFolder; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IWorkspaceRunnable; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.resources.WorkspaceJob; |
| |
| import org.eclipse.core.filebuffers.tests.ResourceHelper; |
| |
| import org.eclipse.jface.action.IStatusLineManager; |
| |
| import org.eclipse.ui.IEditorPart; |
| import org.eclipse.ui.IWorkbench; |
| import org.eclipse.ui.IWorkbenchPage; |
| import org.eclipse.ui.IWorkbenchWindow; |
| import org.eclipse.ui.PlatformUI; |
| import org.eclipse.ui.ide.IDE; |
| import org.eclipse.ui.internal.dialogs.EventLoopProgressMonitor; |
| import org.eclipse.ui.intro.IIntroManager; |
| import org.eclipse.ui.intro.IIntroPart; |
| |
| import org.eclipse.ui.editors.text.FileDocumentProvider; |
| |
| /** |
| * Test checking UI deadlock on modifying the FileDocumentProvider's underlined |
| * file. |
| * |
| * @since 3.10 |
| */ |
| public class FileDocumentProviderTest { |
| |
| private File file; |
| private AtomicBoolean stoppedByTest; |
| private AtomicBoolean stopLockingFlag; |
| private LockJob lockJob; |
| private LockJob2 lockJob2; |
| private FileDocumentProviderMock fileProvider; |
| private FileSystemResourceManager fsManager; |
| private IEditorPart editor; |
| private IWorkbenchPage page; |
| |
| @Before |
| public void setUp() throws Exception { |
| closeIntro(PlatformUI.getWorkbench()); |
| IFolder folder = ResourceHelper.createFolder("FileDocumentProviderTestProject/test"); |
| file = (File) ResourceHelper.createFile(folder, "file.txt", ""); |
| assertTrue(file.exists()); |
| fsManager = file.getLocalManager(); |
| assertTrue(fsManager.fastIsSynchronized(file)); |
| stopLockingFlag = new AtomicBoolean(false); |
| stoppedByTest = new AtomicBoolean(false); |
| fileProvider = new FileDocumentProviderMock(); |
| lockJob = new LockJob("Locking workspace", file, stopLockingFlag, stoppedByTest); |
| lockJob2 = new LockJob2("Locking workspace", file, stopLockingFlag, stoppedByTest); |
| |
| // We need the editor only to get the default editor status line manager |
| IWorkbench workbench = PlatformUI.getWorkbench(); |
| page = workbench.getActiveWorkbenchWindow().getActivePage(); |
| editor = IDE.openEditor(page, file); |
| TestUtil.runEventLoop(); |
| |
| IStatusLineManager statusLineManager = editor.getEditorSite().getActionBars().getStatusLineManager(); |
| // This is default monitor which almost all editors are using |
| IProgressMonitor progressMonitor = statusLineManager.getProgressMonitor(); |
| assertNotNull(progressMonitor); |
| assertFalse(progressMonitor instanceof NullProgressMonitor); |
| assertFalse(progressMonitor instanceof EventLoopProgressMonitor); |
| |
| // Because this monitor is not EventLoopProgressMonitor, it will not |
| // process UI events while waiting on workspace lock |
| fileProvider.setProgressMonitor(progressMonitor); |
| |
| TestUtil.waitForJobs(500, 5000); |
| Job[] jobs = Job.getJobManager().find(null); |
| String jobsList = Arrays.toString(jobs); |
| System.out.println("Still running jobs: " + jobsList); |
| if (!Job.getJobManager().isIdle()) { |
| jobs = Job.getJobManager().find(null); |
| for (Job job : jobs) { |
| System.out.println("Going to cancel: " + job.getName() + " / " + job); |
| job.cancel(); |
| } |
| } |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| stopLockingFlag.set(true); |
| lockJob.cancel(); |
| lockJob2.cancel(); |
| if (editor != null) { |
| page.closeEditor(editor, false); |
| } |
| ResourceHelper.deleteProject(file.getProject().getName()); |
| TestUtil.runEventLoop(); |
| TestUtil.cleanUp(); |
| } |
| |
| @Test |
| public void testRefreshFileWhileWorkspaceIsLocked1() throws Exception { |
| // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=482354 |
| assertNotNull("Test must run in UI thread", Display.getCurrent()); |
| |
| // Start workspace job which will lock workspace operations on file via |
| // rule |
| lockJob.schedule(); |
| |
| // touch the file of the editor |
| makeSureResourceIsOutOfDate(); |
| |
| // Put an UI event in the queue which will stop the workspace lock job |
| // after a delay so that we can verify the UI events are still |
| // dispatched after the call to refreshFile() below |
| Display.getCurrent().timerExec(500, () -> { |
| stopLockingFlag.set(true); |
| System.out.println("UI event dispatched, lock removed"); |
| }); |
| |
| // Original code will lock UI thread here because it will try to acquire |
| // resource lock and no one will process UI events anymore |
| fileProvider.refreshFile(file); |
| |
| System.out.println("Busy wait terminated, UI thread is operable again!"); |
| assertFalse("Test deadlocked while waiting on resource lock", stoppedByTest.get()); |
| assertTrue(stopLockingFlag.get()); |
| } |
| |
| @Test |
| public void testRefreshFileWhileWorkspaceIsLocked2() throws Exception { |
| // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=482354 |
| assertNotNull("Test must run in UI thread", Display.getCurrent()); |
| |
| // Start workspace job which will lock workspace operations on file via |
| // rule |
| lockJob.schedule(); |
| |
| // touch the file of the editor |
| makeSureResourceIsOutOfDate(); |
| |
| // Put an UI event in the queue which will stop the workspace lock job |
| // after a delay so that we can verify the UI events are still |
| // dispatched after the call to refreshFile() below |
| Display.getCurrent().timerExec(500, () -> { |
| stopLockingFlag.set(true); |
| System.out.println("UI event dispatched, lock removed"); |
| }); |
| |
| // Original code will lock UI thread here because it will try to acquire |
| // resource lock and no one will process UI events anymore |
| fileProvider.refreshFile(file, fileProvider.getProgressMonitor()); |
| |
| System.out.println("Busy wait terminated, UI thread is operable again!"); |
| |
| assertFalse("Test deadlocked while waiting on resource lock", stoppedByTest.get()); |
| assertTrue(stopLockingFlag.get()); |
| } |
| |
| @Test |
| public void testValidateStateForFileWhileWorkspaceIsLocked() throws Exception { |
| assertNotNull("Test must run in UI thread", Display.getCurrent()); |
| |
| // Start workspace job which will lock workspace operations on file |
| lockJob2.schedule(); |
| |
| Thread.sleep(100); |
| |
| // Put an UI event in the queue which will stop the workspace lock job |
| // after a delay |
| Display.getCurrent().timerExec(600, () -> { |
| stopLockingFlag.set(true); |
| System.out.println("UI event dispatched, lock removed"); |
| }); |
| |
| // Original code will lock UI thread here because it will try to acquire |
| // workspace lock and no one will process UI events anymore |
| fileProvider.validateState(editor.getEditorInput(), editor.getSite().getShell()); |
| |
| System.out.println("Busy wait terminated, UI thread is operable again!"); |
| assertFalse("Test deadlocked while waiting on resource lock", stoppedByTest.get()); |
| assertTrue(stopLockingFlag.get()); |
| } |
| |
| /* |
| * Set current time stamp via java.nio to make sure |
| * org.eclipse.core.internal.resources.File.refreshLocal(int, |
| * IProgressMonitor) will call super.refreshLocal(IResource.DEPTH_ZERO, |
| * monitor) and so lock the UI by trying to access resource locked by the |
| * job |
| */ |
| private void makeSureResourceIsOutOfDate() throws Exception { |
| int count = 0; |
| Files.setLastModifiedTime(file.getLocation().toFile().toPath(), |
| FileTime.fromMillis(System.currentTimeMillis())); |
| // Give the file system a chance to have a *different* timestamp |
| Thread.sleep(100); |
| while (fsManager.fastIsSynchronized(file) && count < 1000) { |
| Files.setLastModifiedTime(file.getLocation().toFile().toPath(), |
| FileTime.fromMillis(System.currentTimeMillis())); |
| Thread.sleep(10); |
| count++; |
| } |
| System.out.println("Managed to update file after " + count + " attempts"); |
| assertFalse(fsManager.fastIsSynchronized(file)); |
| } |
| |
| static void logError(String message, Exception ex) { |
| String PLUGIN_ID = "org.eclipse.jface.text"; //$NON-NLS-1$ |
| ILog log = Platform.getLog(Platform.getBundle(PLUGIN_ID)); |
| log.log(new Status(IStatus.ERROR, PLUGIN_ID, IStatus.OK, message, ex)); |
| } |
| |
| static void closeIntro(final IWorkbench wb) { |
| IWorkbenchWindow window = wb.getActiveWorkbenchWindow(); |
| if (window != null) { |
| IIntroManager im = wb.getIntroManager(); |
| IIntroPart intro = im.getIntro(); |
| if (intro != null) { |
| im.closeIntro(intro); |
| } |
| } |
| } |
| } |
| |
| class FileDocumentProviderMock extends FileDocumentProvider { |
| |
| /** |
| * Overridden to make public accessible for the test |
| */ |
| @Override |
| public void refreshFile(IFile file) throws CoreException { |
| System.out.println("Will try to refresh file now: " + file); |
| super.refreshFile(file); |
| } |
| |
| /** |
| * Overridden to make public accessible for the test |
| */ |
| @Override |
| public void refreshFile(IFile file, IProgressMonitor m) throws CoreException { |
| System.out.println("Will try to refresh file (with monitor: " + m + " ) now: " + file); |
| super.refreshFile(file, m); |
| } |
| } |
| |
| /** Emulates what SVN plugin jobs are doing */ |
| class LockJob extends WorkspaceJob { |
| |
| final IResource resource; |
| AtomicBoolean stopFlag; |
| AtomicBoolean stoppedByTest; |
| |
| public LockJob(String name, IResource resource, AtomicBoolean stopFlag, AtomicBoolean stoppedByTest) { |
| super(name); |
| this.stopFlag = stopFlag; |
| this.stoppedByTest = stoppedByTest; |
| setUser(true); |
| setSystem(true); |
| this.resource = resource; |
| } |
| |
| public IStatus run2() { |
| long startTime = System.currentTimeMillis(); |
| // Wait maximum 5 minutes |
| int maxWaitTime = 5 * 60 * 1000; |
| long stopTime = startTime + maxWaitTime; |
| |
| System.out.println("Starting the busy wait while holding lock on: " + resource.getFullPath()); |
| try { |
| while (!stopFlag.get()) { |
| try { |
| if (System.currentTimeMillis() > stopTime) { |
| FileDocumentProviderTest.logError("Tiemout occured while waiting on test to finish", |
| new IllegalStateException()); |
| stoppedByTest.set(true); |
| return Status.CANCEL_STATUS; |
| } |
| Thread.sleep(100); |
| } catch (InterruptedException e) { |
| stoppedByTest.set(true); |
| FileDocumentProviderTest.logError("Lock job was interrupted while waiting on test to finish", e); |
| e.printStackTrace(); |
| return Status.CANCEL_STATUS; |
| } |
| } |
| } finally { |
| System.out.println("Lock task terminated"); |
| } |
| return Status.OK_STATUS; |
| } |
| |
| @Override |
| public boolean belongsTo(Object family) { |
| return super.belongsTo(family) || LockJob.class == family; |
| } |
| |
| @Override |
| public String toString() { |
| return super.toString() + " on " + resource; |
| } |
| |
| @Override |
| public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException { |
| IWorkspaceRunnable workspaceRunnable = pm -> { |
| try { |
| run2(); |
| } catch (Exception e) { |
| // Re-throw as OperationCanceledException, which will be |
| // caught and re-thrown as InterruptedException below. |
| throw new OperationCanceledException(e.getMessage()); |
| } |
| // CoreException and OperationCanceledException are propagated |
| }; |
| ResourcesPlugin.getWorkspace().run(workspaceRunnable, |
| resource, IResource.NONE, monitor); |
| |
| return monitor.isCanceled() ? Status.CANCEL_STATUS : Status.OK_STATUS; |
| } |
| |
| } |
| |
| /** Emulates what AutoBuildJob is doing */ |
| class LockJob2 extends Job { |
| |
| final IResource resource; |
| AtomicBoolean stopFlag; |
| AtomicBoolean stoppedByTest; |
| |
| public LockJob2(String name, IResource resource, AtomicBoolean stopFlag, AtomicBoolean stoppedByTest) { |
| super(name); |
| this.stopFlag = stopFlag; |
| this.stoppedByTest = stoppedByTest; |
| setUser(true); |
| setSystem(true); |
| this.resource = resource; |
| setRule(ResourcesPlugin.getWorkspace().getRoot()); |
| } |
| |
| @Override |
| public boolean belongsTo(Object family) { |
| return super.belongsTo(family) || LockJob2.class == family; |
| } |
| |
| @Override |
| public IStatus run(IProgressMonitor monitor) { |
| Workspace workspace = (Workspace) ResourcesPlugin.getWorkspace(); |
| try { |
| workspace.prepareOperation(getRule(), monitor); |
| workspace.beginOperation(true); |
| run2(); |
| } catch (CoreException e) { |
| FileDocumentProviderTest.logError(e.getMessage(), e); |
| } finally { |
| try { |
| workspace.endOperation(getRule(), false); |
| } catch (CoreException e) { |
| FileDocumentProviderTest.logError(e.getMessage(), e); |
| } |
| } |
| return monitor.isCanceled() ? Status.CANCEL_STATUS : Status.OK_STATUS; |
| } |
| |
| public IStatus run2() { |
| long startTime = System.currentTimeMillis(); |
| // Wait maximum 5 minutes |
| int maxWaitTime = 5 * 60 * 1000; |
| long stopTime = startTime + maxWaitTime; |
| |
| System.out.println("Starting the busy wait while holding lock on: " + resource.getFullPath()); |
| try { |
| while (!stopFlag.get()) { |
| try { |
| if (System.currentTimeMillis() > stopTime) { |
| FileDocumentProviderTest.logError("Tiemout occured while waiting on test to finish", |
| new IllegalStateException()); |
| stoppedByTest.set(true); |
| return Status.CANCEL_STATUS; |
| } |
| Thread.sleep(100); |
| } catch (InterruptedException e) { |
| stoppedByTest.set(true); |
| FileDocumentProviderTest.logError("Lock job was interrupted while waiting on test to finish", e); |
| e.printStackTrace(); |
| return Status.CANCEL_STATUS; |
| } |
| } |
| } finally { |
| System.out.println("Lock task terminated"); |
| } |
| return Status.OK_STATUS; |
| } |
| } |