/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.progress.impl;
import com.google.common.collect.ConcurrentHashMultiset;
import com.intellij.concurrency.JobScheduler;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ex.ApplicationEx;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.ThrowableComputable;
import com.intellij.openapi.wm.ex.ProgressIndicatorEx;
import com.intellij.util.containers.ConcurrentLongObjectMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.SmartHashSet;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import java.util.*;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public class CoreProgressManager extends ProgressManager implements Disposable {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.progress.impl.CoreProgressManager");
static final int CHECK_CANCELED_DELAY_MILLIS = 10;
final AtomicInteger myCurrentUnsafeProgressCount = new AtomicInteger(0);
private final AtomicInteger myCurrentModalProgressCount = new AtomicInteger(0);
private static final boolean ENABLED = !"disabled".equals(System.getProperty("idea.ProcessCanceledException"));
private static CheckCanceledHook ourCheckCanceledHook;
private ScheduledFuture<?> myCheckCancelledFuture; // guarded by threadsUnderIndicator
// indicator -> threads which are running under this indicator. guarded by threadsUnderIndicator.
private static final Map<ProgressIndicator, Set<Thread>> threadsUnderIndicator = new THashMap<>();
// the active indicator for the thread id
private static final ConcurrentLongObjectMap<ProgressIndicator> currentIndicators = ContainerUtil.createConcurrentLongObjectMap();
// top-level indicators for the thread id
private static final ConcurrentLongObjectMap<ProgressIndicator> threadTopLevelIndicators = ContainerUtil.createConcurrentLongObjectMap();
// threads which are running under canceled indicator
static final Set<Thread> threadsUnderCanceledIndicator = new THashSet<>(); // guarded by threadsUnderIndicator
private static volatile boolean shouldCheckCanceled;
/** active (i.e. which have {@link #executeProcessUnderProgress(Runnable, ProgressIndicator)} method running) indicators
* which are not inherited from {@link StandardProgressIndicator}.
* for them an extra processing thread (see {@link #myCheckCancelledFuture}) has to be run
* to call their non-standard {@link ProgressIndicator#checkCanceled()} method periodically.
*/
// multiset here (instead of a set) is for simplifying add/remove indicators on process-with-progress start/end with possibly identical indicators.
private static final Collection<ProgressIndicator> nonStandardIndicators = ConcurrentHashMultiset.create();
/** true if running in non-cancelable section started with
* {@link #startNonCancelableSection()} or {@link #executeNonCancelableSection(Runnable)} in this thread
*/
private static final ThreadLocal<Boolean> isInNonCancelableSection = new ThreadLocal<>(); // do not supply initial value to conserve memory
// must be under threadsUnderIndicator lock
private void startBackgroundNonStandardIndicatorsPing() {
if (myCheckCancelledFuture == null) {
myCheckCancelledFuture = JobScheduler.getScheduler().scheduleWithFixedDelay(() -> {
for (ProgressIndicator indicator : nonStandardIndicators) {
try {
indicator.checkCanceled();
}
catch (ProcessCanceledException e) {
indicatorCanceled(indicator);
}
}
}, 0, CHECK_CANCELED_DELAY_MILLIS, TimeUnit.MILLISECONDS);
}
}
// must be under threadsUnderIndicator lock
private void stopBackgroundNonStandardIndicatorsPing() {
if (myCheckCancelledFuture != null) {
myCheckCancelledFuture.cancel(true);
myCheckCancelledFuture = null;
}
}
@Override
public void dispose() {
synchronized (threadsUnderIndicator) {
stopBackgroundNonStandardIndicatorsPing();
}
}
static boolean isThreadUnderIndicator(@NotNull ProgressIndicator indicator, @NotNull Thread thread) {
synchronized (threadsUnderIndicator) {
Set<Thread> threads = threadsUnderIndicator.get(indicator);
return threads != null && threads.contains(thread);
}
}
public static boolean runCheckCanceledHooks(@Nullable ProgressIndicator indicator) {
CheckCanceledHook hook = ourCheckCanceledHook;
return hook != null && hook.runHook(indicator);
}
@Override
protected void doCheckCanceled() throws ProcessCanceledException {
if (!shouldCheckCanceled) return;
final ProgressIndicator progress = getProgressIndicator();
if (progress != null && ENABLED) {
progress.checkCanceled();
}
else {
runCheckCanceledHooks(progress);
}
}
@Override
public boolean hasProgressIndicator() {
return getProgressIndicator() != null;
}
@Override
public boolean hasUnsafeProgressIndicator() {
return myCurrentUnsafeProgressCount.get() > 0;
}
@Override
public boolean hasModalProgressIndicator() {
return myCurrentModalProgressCount.get() > 0;
}
@Override
public void runProcess(@NotNull final Runnable process, final ProgressIndicator progress) {
executeProcessUnderProgress(() -> {
try {
try {
if (progress != null && !progress.isRunning()) {
progress.start();
}
}
catch (RuntimeException e) {
throw e;
}
catch (Throwable e) {
throw new RuntimeException(e);
}
process.run();
}
finally {
if (progress != null && progress.isRunning()) {
progress.stop();
if (progress instanceof ProgressIndicatorEx) {
((ProgressIndicatorEx)progress).processFinish();
}
}
}
}, progress);
}
@Override
public <T> T runProcess(@NotNull final Computable<T> process, ProgressIndicator progress) throws ProcessCanceledException {
final Ref<T> ref = new Ref<>();
runProcess(() -> ref.set(process.compute()), progress);
return ref.get();
}
@Override
public void executeNonCancelableSection(@NotNull Runnable runnable) {
if (isInNonCancelableSection()) {
runnable.run();
}
else {
try {
isInNonCancelableSection.set(Boolean.TRUE);
executeProcessUnderProgress(runnable, NonCancelableIndicator.INSTANCE);
}
finally {
isInNonCancelableSection.remove();
}
}
}
@Override
public void setCancelButtonText(String cancelButtonText) {
}
@Override
public boolean runProcessWithProgressSynchronously(@NotNull Runnable process,
@NotNull @Nls String progressTitle,
boolean canBeCanceled,
@Nullable Project project) {
return runProcessWithProgressSynchronously(process, progressTitle, canBeCanceled, project, null);
}
@Override
public <T, E extends Exception> T runProcessWithProgressSynchronously(@NotNull final ThrowableComputable<T, E> process,
@NotNull @Nls String progressTitle,
boolean canBeCanceled,
@Nullable Project project) throws E {
final AtomicReference<T> result = new AtomicReference<>();
final AtomicReference<Throwable> exception = new AtomicReference<>();
runProcessWithProgressSynchronously(new Task.Modal(project, progressTitle, canBeCanceled) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
try {
T compute = process.compute();
result.set(compute);
}
catch (Throwable t) {
exception.set(t);
}
}
}, null);
//noinspection ThrowableResultOfMethodCallIgnored
Throwable t = exception.get();
if (t != null) {
if (t instanceof Error) throw (Error)t;
if (t instanceof RuntimeException) throw (RuntimeException)t;
@SuppressWarnings("unchecked") E e = (E)t;
throw e;
}
return result.get();
}
@Override
public boolean runProcessWithProgressSynchronously(@NotNull final Runnable process,
@NotNull @Nls String progressTitle,
boolean canBeCanceled,
@Nullable Project project,
@Nullable JComponent parentComponent) {
Task.Modal task = new Task.Modal(project, progressTitle, canBeCanceled) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
process.run();
}
};
return runProcessWithProgressSynchronously(task, parentComponent);
}
@Override
public void runProcessWithProgressAsynchronously(@NotNull Project project,
@NotNull @Nls String progressTitle,
@NotNull Runnable process,
@Nullable Runnable successRunnable,
@Nullable Runnable canceledRunnable) {
runProcessWithProgressAsynchronously(project, progressTitle, process, successRunnable, canceledRunnable, PerformInBackgroundOption.DEAF);
}
@Override
public void runProcessWithProgressAsynchronously(@NotNull Project project,
@NotNull @Nls String progressTitle,
@NotNull final Runnable process,
@Nullable final Runnable successRunnable,
@Nullable final Runnable canceledRunnable,
@NotNull PerformInBackgroundOption option) {
runProcessWithProgressAsynchronously(new Task.Backgroundable(project, progressTitle, true, option) {
@Override
public void run(@NotNull final ProgressIndicator indicator) {
process.run();
}
@Override
public void onCancel() {
if (canceledRunnable != null) {
canceledRunnable.run();
}
}
@Override
public void onSuccess() {
if (successRunnable != null) {
successRunnable.run();
}
}
});
}
@Override
public void run(@NotNull final Task task) {
if (task.isHeadless()) {
if (ApplicationManager.getApplication().isDispatchThread()) {
runProcessWithProgressSynchronously(task, null);
}
else {
runProcessWithProgressInCurrentThread(task, new EmptyProgressIndicator(), ModalityState.defaultModalityState());
}
}
else if (task.isModal()) {
runSynchronously(task.asModal());
}
else {
final Task.Backgroundable backgroundable = task.asBackgroundable();
if (backgroundable.isConditionalModal() && !backgroundable.shouldStartInBackground()) {
runSynchronously(task);
}
else {
runAsynchronously(backgroundable);
}
}
}
private void runSynchronously(@NotNull final Task task) {
if (ApplicationManager.getApplication().isDispatchThread()) {
runProcessWithProgressSynchronously(task, null);
}
else {
ApplicationManager.getApplication().invokeAndWait(() -> runProcessWithProgressSynchronously(task, null));
}
}
private void runAsynchronously(@NotNull final Task.Backgroundable task) {
if (ApplicationManager.getApplication().isDispatchThread()) {
runProcessWithProgressAsynchronously(task);
}
else {
ApplicationManager.getApplication().invokeLater(() -> runProcessWithProgressAsynchronously(task), ModalityState.defaultModalityState());
}
}
@NotNull
public Future<?> runProcessWithProgressAsynchronously(@NotNull Task.Backgroundable task) {
return runProcessWithProgressAsynchronously(task, new EmptyProgressIndicator(), null);
}
@NotNull
public Future<?> runProcessWithProgressAsynchronously(@NotNull final Task.Backgroundable task,
@NotNull final ProgressIndicator progressIndicator,
@Nullable final Runnable continuation) {
return runProcessWithProgressAsynchronously(task, progressIndicator, continuation, ModalityState.defaultModalityState());
}
@NotNull
public Future<?> runProcessWithProgressAsynchronously(@NotNull final Task.Backgroundable task,
@NotNull final ProgressIndicator progressIndicator,
@Nullable final Runnable continuation,
@NotNull final ModalityState modalityState) {
if (progressIndicator instanceof Disposable) {
Disposer.register(ApplicationManager.getApplication(), (Disposable)progressIndicator);
}
final Runnable process = new TaskRunnable(task, progressIndicator, continuation);
Runnable action = new TaskContainer(task) {
@Override
public void run() {
boolean processCanceled = false;
Throwable exception = null;
try {
runProcess(process, progressIndicator);
}
catch (ProcessCanceledException e) {
processCanceled = true;
}
catch (Throwable e) {
exception = e;
}
final boolean finalCanceled = processCanceled || progressIndicator.isCanceled();
final Throwable finalException = exception;
ApplicationManager.getApplication().invokeLater(() -> finishTask(task, finalCanceled, finalException), modalityState);
}
};
return ApplicationManager.getApplication().executeOnPooledThread(action);
}
public boolean runProcessWithProgressSynchronously(@NotNull final Task task, @Nullable final JComponent parentComponent) {
final Ref<Throwable> exceptionRef = new Ref<>();
TaskContainer taskContainer = new TaskContainer(task) {
@Override
public void run() {
try {
new TaskRunnable(task, getProgressIndicator()).run();
}
catch (ProcessCanceledException e) {
throw e;
}
catch (Throwable e) {
exceptionRef.set(e);
}
}
};
ApplicationEx application = (ApplicationEx)ApplicationManager.getApplication();
boolean result = application.runProcessWithProgressSynchronously(taskContainer, task.getTitle(), task.isCancellable(),
task.getProject(), parentComponent, task.getCancelText());
finishTask(task, !result, exceptionRef.get());
return result;
}
public void runProcessWithProgressInCurrentThread(@NotNull final Task task,
@NotNull final ProgressIndicator progressIndicator,
@NotNull final ModalityState modalityState) {
if (progressIndicator instanceof Disposable) {
Disposer.register(ApplicationManager.getApplication(), (Disposable)progressIndicator);
}
final Runnable process = new TaskRunnable(task, progressIndicator);
boolean processCanceled = false;
Throwable exception = null;
try {
runProcess(process, progressIndicator);
}
catch (ProcessCanceledException e) {
processCanceled = true;
}
catch (Throwable e) {
exception = e;
}
if (ApplicationManager.getApplication().isDispatchThread()) {
finishTask(task, processCanceled || progressIndicator.isCanceled(), exception);
}
else {
final boolean finalCanceled = processCanceled;
final Throwable finalException = exception;
ApplicationManager.getApplication().invokeAndWait(
() -> finishTask(task, finalCanceled || progressIndicator.isCanceled(), finalException), modalityState);
}
}
static void finishTask(@NotNull Task task, boolean canceled, @Nullable Throwable error) {
try {
if (error != null) {
task.onThrowable(error);
}
else if (canceled) {
task.onCancel();
}
else {
task.onSuccess();
}
}
finally {
task.onFinished();
}
}
@Override
public void runProcessWithProgressAsynchronously(@NotNull Task.Backgroundable task, @NotNull ProgressIndicator progressIndicator) {
runProcessWithProgressAsynchronously(task, progressIndicator, null);
}
@Override
public ProgressIndicator getProgressIndicator() {
return getCurrentIndicator(Thread.currentThread());
}
@Override
public void executeProcessUnderProgress(@NotNull Runnable process, ProgressIndicator progress) throws ProcessCanceledException {
boolean modal = progress != null && progress.isModal();
if (modal) myCurrentModalProgressCount.incrementAndGet();
if (progress == null) myCurrentUnsafeProgressCount.incrementAndGet();
try {
ProgressIndicator oldIndicator = null;
boolean set = progress != null && progress != (oldIndicator = getProgressIndicator());
if (set) {
Thread currentThread = Thread.currentThread();
setCurrentIndicator(currentThread, progress);
try {
registerIndicatorAndRun(progress, currentThread, oldIndicator, process);
}
finally {
setCurrentIndicator(currentThread, oldIndicator);
}
}
else {
process.run();
}
}
finally {
if (progress == null) myCurrentUnsafeProgressCount.decrementAndGet();
if (modal) myCurrentModalProgressCount.decrementAndGet();
}
}
@Override
public boolean runInReadActionWithWriteActionPriority(@NotNull Runnable action, @Nullable ProgressIndicator indicator) {
ApplicationManager.getApplication().runReadAction(action);
return true;
}
private void registerIndicatorAndRun(@NotNull ProgressIndicator indicator,
@NotNull Thread currentThread,
ProgressIndicator oldIndicator,
@NotNull Runnable process) {
List<Set<Thread>> threadsUnderThisIndicator = new ArrayList<>();
synchronized (threadsUnderIndicator) {
for (ProgressIndicator thisIndicator = indicator; thisIndicator != null; thisIndicator = thisIndicator instanceof WrappedProgressIndicator ? ((WrappedProgressIndicator)thisIndicator).getOriginalProgressIndicator() : null) {
Set<Thread> underIndicator = threadsUnderIndicator.get(thisIndicator);
if (underIndicator == null) {
underIndicator = new SmartHashSet<>();
threadsUnderIndicator.put(thisIndicator, underIndicator);
}
boolean alreadyUnder = !underIndicator.add(currentThread);
threadsUnderThisIndicator.add(alreadyUnder ? null : underIndicator);
boolean isStandard = thisIndicator instanceof StandardProgressIndicator;
if (!isStandard) {
nonStandardIndicators.add(thisIndicator);
startBackgroundNonStandardIndicatorsPing();
}
if (thisIndicator.isCanceled()) {
threadsUnderCanceledIndicator.add(currentThread);
}
else {
threadsUnderCanceledIndicator.remove(currentThread);
}
}
updateShouldCheckCanceled();
}
try {
process.run();
}
finally {
synchronized (threadsUnderIndicator) {
ProgressIndicator thisIndicator = null;
// order doesn't matter
for (int i = 0; i < threadsUnderThisIndicator.size(); i++) {
thisIndicator = i == 0 ? indicator : ((WrappedProgressIndicator)thisIndicator).getOriginalProgressIndicator();
Set<Thread> underIndicator = threadsUnderThisIndicator.get(i);
boolean removed = underIndicator != null && underIndicator.remove(currentThread);
if (removed && underIndicator.isEmpty()) {
threadsUnderIndicator.remove(thisIndicator);
}
boolean isStandard = thisIndicator instanceof StandardProgressIndicator;
if (!isStandard) {
nonStandardIndicators.remove(thisIndicator);
if (nonStandardIndicators.isEmpty()) {
stopBackgroundNonStandardIndicatorsPing();
}
}
// by this time oldIndicator may have been canceled
if (oldIndicator != null && oldIndicator.isCanceled()) {
threadsUnderCanceledIndicator.add(currentThread);
}
else {
threadsUnderCanceledIndicator.remove(currentThread);
}
}
updateShouldCheckCanceled();
}
}
}
@SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod")
protected final void updateShouldCheckCanceled() {
ourCheckCanceledHook = createCheckCanceledHook();
if (ourCheckCanceledHook != null) {
shouldCheckCanceled = true;
return;
}
synchronized (threadsUnderIndicator) {
shouldCheckCanceled = !threadsUnderCanceledIndicator.isEmpty();
}
}
@Nullable
protected CheckCanceledHook createCheckCanceledHook() {
return null;
}
@Override
protected void indicatorCanceled(@NotNull ProgressIndicator indicator) {
// mark threads running under this indicator as canceled
synchronized (threadsUnderIndicator) {
Set<Thread> threads = threadsUnderIndicator.get(indicator);
if (threads != null) {
for (Thread thread : threads) {
boolean underCancelledIndicator = false;
for (ProgressIndicator currentIndicator = getCurrentIndicator(thread);
currentIndicator != null;
currentIndicator = currentIndicator instanceof WrappedProgressIndicator ?
((WrappedProgressIndicator)currentIndicator).getOriginalProgressIndicator() : null) {
if (currentIndicator == indicator) {
underCancelledIndicator = true;
break;
}
}
if (underCancelledIndicator) {
threadsUnderCanceledIndicator.add(thread);
//noinspection AssignmentToStaticFieldFromInstanceMethod
shouldCheckCanceled = true;
}
}
}
}
}
@TestOnly
public static boolean isCanceledThread(@NotNull Thread thread) {
synchronized (threadsUnderIndicator) {
return threadsUnderCanceledIndicator.contains(thread);
}
}
@NotNull
@Override
public final NonCancelableSection startNonCancelableSection() {
LOG.warn("Use executeNonCancelableSection() instead");
if (isInNonCancelableSection()) return NonCancelableSection.EMPTY;
final ProgressIndicator myOld = getProgressIndicator();
final Thread currentThread = Thread.currentThread();
final NonCancelableIndicator nonCancelor = new NonCancelableIndicator() {
@Override
public void done() {
setCurrentIndicator(currentThread, myOld);
isInNonCancelableSection.remove();
}
};
isInNonCancelableSection.set(Boolean.TRUE);
setCurrentIndicator(currentThread, nonCancelor);
return nonCancelor;
}
@Override
public boolean isInNonCancelableSection() {
return isInNonCancelableSection.get() != null;
}
@NotNull
public static ModalityState getCurrentThreadProgressModality() {
ProgressIndicator indicator = threadTopLevelIndicators.get(Thread.currentThread().getId());
ModalityState modality = indicator == null ? null : indicator.getModalityState();
return modality != null ? modality : ModalityState.NON_MODAL;
}
private static void setCurrentIndicator(@NotNull Thread currentThread, ProgressIndicator indicator) {
long id = currentThread.getId();
if (indicator == null) {
currentIndicators.remove(id);
threadTopLevelIndicators.remove(id);
}
else {
currentIndicators.put(id, indicator);
if (!threadTopLevelIndicators.containsKey(id)) {
threadTopLevelIndicators.put(id, indicator);
}
}
}
private static ProgressIndicator getCurrentIndicator(@NotNull Thread thread) {
return currentIndicators.get(thread.getId());
}
protected abstract static class TaskContainer implements Runnable {
private final Task myTask;
protected TaskContainer(@NotNull Task task) {
myTask = task;
}
@NotNull
public Task getTask() {
return myTask;
}
@Override
public String toString() {
return myTask.toString();
}
}
static class TaskRunnable extends TaskContainer {
private final ProgressIndicator myIndicator;
private final Runnable myContinuation;
TaskRunnable(@NotNull Task task, @NotNull ProgressIndicator indicator) {
this(task, indicator, null);
}
TaskRunnable(@NotNull Task task, @NotNull ProgressIndicator indicator, @Nullable Runnable continuation) {
super(task);
myIndicator = indicator;
myContinuation = continuation;
}
@Override
public void run() {
try {
getTask().run(myIndicator);
}
finally {
try {
if (myIndicator instanceof ProgressIndicatorEx) {
((ProgressIndicatorEx)myIndicator).finish(getTask());
}
}
finally {
if (myContinuation != null) {
myContinuation.run();
}
}
}
}
}
@FunctionalInterface
protected interface CheckCanceledHook {
/**
* @param indicator the indicator whose {@link ProgressIndicator#checkCanceled()} was called, or null if a non-progressive thread performed {@link ProgressManager#checkCanceled()}
* @return true if the hook has done anything that might take some time.
*/
boolean runHook(@Nullable ProgressIndicator indicator);
}
}