org.eclipse.jface.operation.ModalContext Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2000, 2016 IBM Corporation and others.
*
* 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:
* IBM Corporation - initial API and implementation
* Sergey Prigogin (Google) - [464838] ModalContext.run method should call done() on the progress monitor passed to the constructor
*******************************************************************************/
package org.eclipse.jface.operation;
import java.lang.reflect.InvocationTargetException;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.ProgressMonitorWrapper;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.util.Policy;
import org.eclipse.pde.api.tools.annotations.NoExtend;
import org.eclipse.pde.api.tools.annotations.NoInstantiate;
import org.eclipse.swt.widgets.Display;
/**
* Utility class for supporting modal operations. The runnable passed to the
* run
method is executed in a separate thread, depending on the
* value of the passed fork argument. If the runnable is executed in a separate
* thread then the current thread either waits until the new thread ends or, if
* the current thread is the UI thread, it polls the SWT event queue and
* dispatches each event.
*
* This class is not intended to be subclassed.
*
*/
@NoInstantiate
@NoExtend
public class ModalContext {
/**
* Indicates whether ModalContext is in debug mode; false
by
* default.
*/
private static boolean debug = false;
/**
* The number of nested modal runs, or 0 if not inside a modal run. This is
* global state.
*/
private static int modalLevel = 0;
/**
* Indicates whether operations should be run in a separate thread. Defaults
* to true. For internal debugging use, set to false to run operations in
* the calling thread.
*/
private static boolean runInSeparateThread = true;
/**
* Thread which runs the modal context.
*/
private static class ModalContextThread extends Thread {
/**
* The operation to be run.
*/
private IRunnableWithProgress runnable;
/**
* The exception thrown by the operation starter.
*/
private Throwable throwable;
/**
* The progress monitor used for progress and cancellation.
*/
private IProgressMonitor progressMonitor;
/**
* The display used for event dispatching.
*/
private Display display;
/**
* Indicates whether to continue event queue dispatching.
*/
private volatile boolean continueEventDispatching = true;
/**
* The thread that forked this modal context thread.
*
* @since 3.1
*/
private Thread callingThread;
/**
* Creates a new modal context.
*
* @param operation
* the runnable to run
* @param monitor
* the progress monitor to use to display progress and
* receive requests for cancellation
* @param display
* the display to be used to read and dispatch events
*/
private ModalContextThread(IRunnableWithProgress operation,
IProgressMonitor monitor, Display display) {
super("ModalContext"); //$NON-NLS-1$
Assert.isTrue(monitor != null && display != null);
runnable = operation;
progressMonitor = new AccumulatingProgressMonitor(monitor, display);
this.display = display;
this.callingThread = Thread.currentThread();
}
@Override
public void run() {
try {
if (runnable != null) {
runnable.run(progressMonitor);
}
} catch (ThreadDeath e) {
// Make sure to propagate ThreadDeath, or threads will never
// fully terminate.
throw e;
} catch (InvocationTargetException | InterruptedException | RuntimeException | Error e) {
throwable = e;
} finally {
progressMonitor.done();
// Notify the operation of change of thread of control.
if (runnable instanceof IThreadListener) {
Throwable exception = invokeThreadListener(((IThreadListener) runnable), callingThread);
// Forward it if we don't already have one
if (exception != null && throwable == null)
throwable = exception;
}
// Make sure that all events in the asynchronous event queue
// are dispatched.
display.syncExec(() -> {
// Do nothing.
});
// Stop event dispatching.
continueEventDispatching = false;
// Force the event loop to return from sleep() so that
// it stops event dispatching.
display.asyncExec(null);
}
}
/**
* Processes events or waits until this modal context thread terminates.
*/
public void block() {
if (display == Display.getCurrent()) {
int exceptionCount = 0;
while (continueEventDispatching) {
// Run the event loop. Handle any uncaught exceptions caused
// by UI events.
try {
if (!display.readAndDispatch()) {
display.sleep();
}
exceptionCount = 0;
}
// ThreadDeath is a normal error when the thread is dying.
// We must propagate it in order for it to properly
// terminate.
catch (ThreadDeath e) {
throw (e);
}
// For all other exceptions, log the problem.
catch (Throwable t) {
if (t instanceof VirtualMachineError) {
throw (VirtualMachineError) t;
}
exceptionCount++;
// We're counting exceptions in client code, such as asyncExecs,
// so be generous about how many may fail consecutively before we
// give up.
if (exceptionCount > 50 || display.isDisposed()) {
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else if (t instanceof Error) {
throw (Error) t;
} else {
throw new RuntimeException(t);
}
}
Policy.getLog().log(new Status(IStatus.ERROR, Policy.JFACE,
"Unhandled event loop exception during blocked modal context.", t)); //$NON-NLS-1$
}
}
} else {
try {
join();
} catch (InterruptedException e) {
throwable = e;
}
}
}
}
/**
* Returns whether the first progress monitor is the same as, or a wrapper
* around, the second progress monitor.
*
* @param monitor1
* the first progress monitor
* @param monitor2
* the second progress monitor
* @return true
if the first is the same as, or a wrapper
* around, the second
* @see ProgressMonitorWrapper
*/
public static boolean canProgressMonitorBeUsed(IProgressMonitor monitor1, IProgressMonitor monitor2) {
if (monitor1 == monitor2) {
return true;
}
while (monitor1 instanceof ProgressMonitorWrapper) {
monitor1 = ((ProgressMonitorWrapper) monitor1).getWrappedProgressMonitor();
if (monitor1 == monitor2) {
return true;
}
}
return false;
}
/**
* Checks with the given progress monitor and throws
* InterruptedException
if it has been canceled.
*
* Code in a long-running operation should call this method regularly so that a
* request to cancel will be honored.
*
*
* Convenience for:
*
*
*
* if (monitor.isCanceled())
* throw new InterruptedException();
*
*
*
* @param monitor the progress monitor
* @exception InterruptedException if canceling the operation has been requested
* @see IProgressMonitor#isCanceled()
*/
public static void checkCanceled(IProgressMonitor monitor) throws InterruptedException {
if (monitor.isCanceled()) {
throw new InterruptedException();
}
}
/**
* Returns the currently active modal context thread, or null if no modal
* context is active.
*/
private static ModalContextThread getCurrentModalContextThread() {
Thread t = Thread.currentThread();
if (t instanceof ModalContextThread) {
return (ModalContextThread) t;
}
return null;
}
/**
* Returns the modal nesting level.
*
* The modal nesting level increases by one each time the
* ModalContext.run
method is called within the dynamic scope
* of another call to ModalContext.run
.
*
*
* @return the modal nesting level, or 0
if this method is
* called outside the dynamic scope of any invocation of
* ModalContext.run
*/
public static int getModalLevel() {
return modalLevel;
}
/**
* Returns whether the given thread is running a modal context.
*
* @param thread
* The thread to be checked
* @return true
if the given thread is running a modal
* context, false
if not
*/
public static boolean isModalContextThread(Thread thread) {
return thread instanceof ModalContextThread;
}
/**
* Runs the given runnable in a modal context, passing it a progress
* monitor.
*
* The modal nesting level is increased by one from the perspective of the
* given runnable.
*
*
* If the supplied operation implements IThreadListener
, it
* will be notified of any thread changes required to execute the operation.
* Specifically, the operation will be notified of the thread that will call
* its run
method before it is called, and will be notified of
* the change of control back to the thread calling this method when the
* operation completes. These thread change notifications give the operation
* an opportunity to transfer any thread-local state to the execution thread
* before control is transferred to the new thread.
*
*
* @param operation
* the runnable to run
* @param fork
* true
if the runnable should run in a separate
* thread, and false
if in the same thread
* @param monitor
* the progress monitor to use to display progress and receive
* requests for cancellation
* @param display
* the display to be used to read and dispatch events
* @exception InvocationTargetException
* if the run method must propagate a checked exception, it
* should wrap it inside an
* InvocationTargetException
; runtime exceptions
* and errors are automatically wrapped in an
* InvocationTargetException
by this method
* @exception InterruptedException
* if the operation detects a request to cancel, using
* IProgressMonitor.isCanceled()
, it should exit
* by throwing InterruptedException
; this method
* propagates the exception
*/
public static void run(IRunnableWithProgress operation, boolean fork,
IProgressMonitor monitor, Display display)
throws InvocationTargetException, InterruptedException {
Assert.isTrue(operation != null && monitor != null);
modalLevel++;
try {
monitor.setCanceled(false);
// Is the runnable supposed to be execute in the same thread.
if (!fork || !runInSeparateThread) {
runInCurrentThread(operation, monitor);
} else {
ModalContextThread t = getCurrentModalContextThread();
if (t != null) {
Assert.isTrue(canProgressMonitorBeUsed(monitor,
t.progressMonitor));
runInCurrentThread(operation, monitor);
} else {
t = new ModalContextThread(operation, monitor, display);
Throwable listenerException = null;
if (operation instanceof IThreadListener) {
listenerException = invokeThreadListener((IThreadListener) operation, t);
}
if (listenerException == null) {
t.start();
t.block();
} else if (t.throwable == null)
t.throwable = listenerException;
Throwable throwable = t.throwable;
if (throwable != null) {
if (debug
&& !(throwable instanceof InterruptedException)
&& !(throwable instanceof OperationCanceledException)) {
System.err.println("Exception in modal context operation:"); //$NON-NLS-1$
throwable.printStackTrace();
System.err.println("Called from:"); //$NON-NLS-1$
// Don't create the InvocationTargetException on the
// throwable,
// otherwise it will print its stack trace (from the
// other thread).
new InvocationTargetException(null).printStackTrace();
}
if (throwable instanceof InvocationTargetException e) {
throw e;
} else if (throwable instanceof InterruptedException e) {
throw e;
} else if (throwable instanceof OperationCanceledException) {
InterruptedException interruptedException = new InterruptedException(throwable.getMessage());
interruptedException.initCause(throwable);
throw interruptedException;
} else {
throw new InvocationTargetException(throwable);
}
}
}
}
} finally {
modalLevel--;
}
}
/**
* Invoke the ThreadListener if there are any errors or RuntimeExceptions
* return them.
*
* @param switchingThread
* the {@link Thread} being switched to
*/
static Throwable invokeThreadListener(IThreadListener listener, Thread switchingThread) {
try {
listener.threadChange(switchingThread);
} catch (ThreadDeath e) {
// Make sure to propagate ThreadDeath, or threads will never
// fully terminate
throw e;
} catch (RuntimeException | Error e) {
return e;
}
return null;
}
/**
* Run a runnable. Convert all thrown exceptions to either
* InterruptedException or InvocationTargetException
*/
private static void runInCurrentThread(IRunnableWithProgress runnable, IProgressMonitor progressMonitor)
throws InterruptedException, InvocationTargetException {
try {
if (runnable != null) {
runnable.run(progressMonitor);
}
} catch (OperationCanceledException e) {
InterruptedException interruptedException = new InterruptedException(e.getLocalizedMessage());
interruptedException.initCause(e);
throw interruptedException;
} catch (InvocationTargetException | InterruptedException | ThreadDeath e) {
// Make sure to propagate ThreadDeath, or threads will never fully
// terminate.
throw e;
} catch (RuntimeException | Error e) {
throw new InvocationTargetException(e);
}
}
/**
* Sets whether ModalContext is running in debug mode.
*
* @param debugMode
* true
for debug mode, and false
* for normal mode (the default)
*/
public static void setDebugMode(boolean debugMode) {
debug = debugMode;
}
/**
* Sets whether ModalContext may process events (by calling
* Display.readAndDispatch()
) while running operations. By
* default, ModalContext will process events while running operations. Use
* this method to disallow event processing temporarily.
*
* @param allowReadAndDispatch
* true
(the default) if events may be processed
* while running an operation, false
if
* Display.readAndDispatch() should not be called from
* ModalContext.
* @since 3.2
*/
public static void setAllowReadAndDispatch(boolean allowReadAndDispatch) {
// Use a separate thread if and only if it is OK to spin the event loop.
runInSeparateThread = allowReadAndDispatch;
}
}