/* * Copyright 2000-2016 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.application; import com.google.common.base.MoreObjects; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ex.ApplicationEx; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.registry.Registry; import com.intellij.util.concurrency.Semaphore; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Map; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicLong; /** * @author peter */ public class TransactionGuardImpl extends TransactionGuard { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.application.TransactionGuardImpl"); private final Queue<Transaction> myQueue = new LinkedBlockingQueue<>(); private final Map<ModalityState, TransactionIdImpl> myModality2Transaction = ContainerUtil.createConcurrentWeakMap(); /** * Remembers the value of {@link #myWritingAllowed} at the start of each modality. If writing wasn't allowed at that moment * (e.g. inside SwingUtilities.invokeLater), it won't be allowed for all dialogs inside such modality, even from user activity. */ private final Map<ModalityState, Boolean> myWriteSafeModalities = ContainerUtil.createConcurrentWeakMap(); private TransactionIdImpl myCurrentTransaction; private boolean myWritingAllowed; private boolean myErrorReported; private static boolean ourTestingTransactions; public TransactionGuardImpl() { myWriteSafeModalities.put(ModalityState.NON_MODAL, true); } @NotNull private Queue<Transaction> getQueue(@Nullable TransactionIdImpl transaction) { while (transaction != null && transaction.myFinished) { transaction = transaction.myParent; } return transaction == null ? myQueue : transaction.myQueue; } private void pollQueueLater() { invokeLater(() -> { Queue<Transaction> queue = getQueue(myCurrentTransaction); Transaction next = queue.peek(); if (next != null && canRunTransactionNow(next, false)) { queue.remove(); runSyncTransaction(next); } }); } private void runSyncTransaction(@NotNull Transaction transaction) { ApplicationManager.getApplication().assertIsDispatchThread(); if (Disposer.isDisposed(transaction.parentDisposable)) return; boolean wasWritingAllowed = myWritingAllowed; myWritingAllowed = true; myCurrentTransaction = new TransactionIdImpl(myCurrentTransaction); try { transaction.runnable.run(); } finally { Queue<Transaction> queue = getQueue(myCurrentTransaction.myParent); queue.addAll(myCurrentTransaction.myQueue); if (!queue.isEmpty()) { pollQueueLater(); } myWritingAllowed = wasWritingAllowed; myCurrentTransaction.myFinished = true; myCurrentTransaction = myCurrentTransaction.myParent; } } @Override public void submitTransaction(@NotNull Disposable parentDisposable, @Nullable TransactionId expectedContext, @NotNull Runnable _transaction) { final TransactionIdImpl expectedId = (TransactionIdImpl)expectedContext; final Transaction transaction = new Transaction(_transaction, expectedId, parentDisposable); final Application app = ApplicationManager.getApplication(); final boolean isDispatchThread = app.isDispatchThread(); Runnable runnable = () -> { if (canRunTransactionNow(transaction, isDispatchThread)) { runSyncTransaction(transaction); } else { getQueue(expectedId).offer(transaction); pollQueueLater(); } }; if (isDispatchThread) { runnable.run(); } else { invokeLater(runnable); } } private boolean canRunTransactionNow(Transaction transaction, boolean sync) { if (sync && !myWritingAllowed) { return false; } TransactionIdImpl currentId = myCurrentTransaction; if (currentId == null) { return true; } return transaction.expectedContext != null && currentId.myStartCounter <= transaction.expectedContext.myStartCounter; } @Override public void submitTransactionAndWait(@NotNull final Runnable runnable) throws ProcessCanceledException { Application app = ApplicationManager.getApplication(); if (app.isDispatchThread()) { Transaction transaction = new Transaction(runnable, getContextTransaction(), app); if (!canRunTransactionNow(transaction, true)) { String message = "Cannot run synchronous submitTransactionAndWait from invokeLater. " + "Please use asynchronous submit*Transaction. " + "See TransactionGuard FAQ for details.\nTransaction: " + runnable; if (!isWriteSafeModality(ModalityState.current())) { message += "\nUnsafe modality: " + ModalityState.current(); } LOG.error(message); } runSyncTransaction(transaction); return; } if (app.isReadAccessAllowed()) { throw new IllegalStateException("submitTransactionAndWait should not be invoked from a read action"); } final Semaphore semaphore = new Semaphore(); semaphore.down(); final Throwable[] exception = {null}; submitTransaction(Disposer.newDisposable("never disposed"), getContextTransaction(), () -> { try { runnable.run(); } catch (Throwable e) { exception[0] = e; } finally { semaphore.up(); } }); semaphore.waitFor(); if (exception[0] != null) { throw new RuntimeException(exception[0]); } } /** * An absolutely guru method!<p/> * * Executes the given code and marks it as a user activity, to allow write actions to be run without requiring transactions. * This is only to be called from UI infrastructure, during InputEvent processing and wrap the point where the control * goes to custom input event handlers for the first time.<p/> * * If you wish to invoke some actionPerformed, * please consider using {@code ActionManager.tryToExecute()} instead, or ensure in some other way that the action is enabled * and can be invoked in the current modality state. */ public void performUserActivity(Runnable activity) { ApplicationManager.getApplication().assertIsDispatchThread(); AccessToken token = startActivity(true); try { activity.run(); } finally { token.finish(); } } /** * An absolutely guru method, only intended to be used from Swing event processing. Please consult Peter if you think you need to invoke this. */ @NotNull public AccessToken startActivity(boolean userActivity) { myErrorReported = false; boolean allowWriting = userActivity && isWriteSafeModality(ModalityState.current()); if (myWritingAllowed == allowWriting) { return AccessToken.EMPTY_ACCESS_TOKEN; } ApplicationManager.getApplication().assertIsDispatchThread(); final boolean prev = myWritingAllowed; myWritingAllowed = allowWriting; return new AccessToken() { @Override public void finish() { myWritingAllowed = prev; } }; } public boolean isWriteSafeModality(ModalityState state) { return Boolean.TRUE.equals(myWriteSafeModalities.get(state)); } public void assertWriteActionAllowed() { ApplicationManager.getApplication().assertIsDispatchThread(); if (areAssertionsEnabled() && !myWritingAllowed && !myErrorReported) { // please assign exceptions here to Peter LOG.error(reportWriteUnsafeContext(ModalityState.current())); myErrorReported = true; } } private String reportWriteUnsafeContext(@NotNull ModalityState modality) { return "Write-unsafe context! Model changes are allowed from write-safe contexts only. " + "Please ensure you're using invokeLater/invokeAndWait with a correct modality state (not \"any\"). " + "See TransactionGuard documentation for details." + "\n current modality=" + modality + "\n known modalities=" + myWriteSafeModalities; } @Override public void assertWriteSafeContext(@NotNull ModalityState modality) { if (!isWriteSafeModality(modality) && areAssertionsEnabled()) { // please assign exceptions here to Peter LOG.error(reportWriteUnsafeContext(modality)); } } private static boolean areAssertionsEnabled() { Application app = ApplicationManager.getApplication(); if (app.isUnitTestMode() && !ourTestingTransactions) { return false; } if (app instanceof ApplicationEx && !((ApplicationEx)app).isLoaded()) { return false; } return Registry.is("ide.require.transaction.for.model.changes", false); } @Override public void submitTransactionLater(@NotNull final Disposable parentDisposable, @NotNull final Runnable transaction) { final TransactionIdImpl id = getContextTransaction(); final ModalityState startModality = ModalityState.defaultModalityState(); invokeLater(() -> { boolean allowWriting = ModalityState.current() == startModality; AccessToken token = startActivity(allowWriting); try { submitTransaction(parentDisposable, id, transaction); } finally { token.finish(); } }); } private static void invokeLater(Runnable runnable) { ApplicationManager.getApplication().invokeLater(runnable, ModalityState.any(), Condition.FALSE); } @Override public TransactionIdImpl getContextTransaction() { if (!ApplicationManager.getApplication().isDispatchThread()) { return myModality2Transaction.get(ModalityState.defaultModalityState()); } return myWritingAllowed ? myCurrentTransaction : null; } public void enteredModality(@NotNull ModalityState modality) { TransactionIdImpl contextTransaction = getContextTransaction(); if (contextTransaction != null) { myModality2Transaction.put(modality, contextTransaction); } myWriteSafeModalities.put(modality, myWritingAllowed); } @Nullable public TransactionIdImpl getModalityTransaction(@NotNull ModalityState modalityState) { return myModality2Transaction.get(modalityState); } @NotNull public Runnable wrapLaterInvocation(@NotNull final Runnable runnable, @NotNull ModalityState modalityState) { if (isWriteSafeModality(modalityState)) { return new Runnable() { @Override public void run() { ApplicationManager.getApplication().assertIsDispatchThread(); final boolean prev = myWritingAllowed; myWritingAllowed = true; try { runnable.run(); } finally { myWritingAllowed = prev; } } @Override public String toString() { return runnable.toString(); } }; } return runnable; } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("currentTransaction", myCurrentTransaction) .add("writingAllowed", myWritingAllowed) .toString(); } public static void setTestingTransactions(boolean testingTransactions) { ourTestingTransactions = testingTransactions; } private static class Transaction { @NotNull final Runnable runnable; @Nullable final TransactionIdImpl expectedContext; @NotNull final Disposable parentDisposable; Transaction(@NotNull Runnable runnable, @Nullable TransactionIdImpl expectedContext, @NotNull Disposable parentDisposable) { this.runnable = runnable; this.expectedContext = expectedContext; this.parentDisposable = parentDisposable; } } private static class TransactionIdImpl implements TransactionId { private static final AtomicLong ourTransactionCounter = new AtomicLong(); final long myStartCounter = ourTransactionCounter.getAndIncrement(); final Queue<Transaction> myQueue = new LinkedBlockingQueue<>(); boolean myFinished; final TransactionIdImpl myParent; public TransactionIdImpl(@Nullable TransactionIdImpl parent) { myParent = parent; } @Override public String toString() { return "Transaction " + myStartCounter + (myFinished ? "(finished)" : ""); } } }