diff --git a/platform/platform-impl/src/com/intellij/openapi/application/impl/AnyThreadWriteThreadingSupport.kt b/platform/platform-impl/src/com/intellij/openapi/application/impl/AnyThreadWriteThreadingSupport.kt index 8ab10f70e55a..d2879ffbea0b 100644 --- a/platform/platform-impl/src/com/intellij/openapi/application/impl/AnyThreadWriteThreadingSupport.kt +++ b/platform/platform-impl/src/com/intellij/openapi/application/impl/AnyThreadWriteThreadingSupport.kt @@ -142,6 +142,9 @@ internal object AnyThreadWriteThreadingSupport: ThreadingSupport { @Volatile private var myWriteAcquired: Thread? = null + @Volatile + private var myWriteIntentAcquired: Thread? = null + override fun getPermitAsContextElement(baseContext: CoroutineContext, shared: Boolean): CoroutineContext { if (!isLockStoredInContext) { return EmptyCoroutineContext @@ -247,7 +250,10 @@ internal object AnyThreadWriteThreadingSupport: ThreadingSupport { } when (ts.permit) { - null -> ts.acquire(getWriteIntentPermit()) + null -> { + ts.acquire(getWriteIntentPermit()) + myWriteIntentAcquired = Thread.currentThread() + } is ReadPermit -> error("WriteIntentReadAction can not be called from ReadAction") is WriteIntentPermit -> { // Volatile read @@ -271,6 +277,7 @@ internal object AnyThreadWriteThreadingSupport: ThreadingSupport { ThreadingAssertions.setImplicitLockOnEDT(prevImplicitLock) if (release) { ts.release() + myWriteIntentAcquired = null } if (releaseSecondary) { mySecondaryPermits.get().removeLast().release() @@ -282,10 +289,30 @@ internal object AnyThreadWriteThreadingSupport: ThreadingSupport { override fun isWriteIntentLocked(): Boolean { val ts = myState.get() - return ts.hasWrite || ts.hasWriteIntent + val shared = ts.sharedLock + if (shared == null) { + return ts.hasWrite || ts.hasWriteIntent + } + else { + return myWriteIntentAcquired == Thread.currentThread() + } } - override fun isReadAccessAllowed(): Boolean = getThreadState().hasPermit + override fun isReadAccessAllowed(): Boolean { + val threadState = getThreadState() + val shared = threadState.sharedLock + if (shared == null) { + // Having any permit (r/w/wi) without the presence of the second lock means that this thread has _some_ permission, + // and even the weakest possible permission allows read access + return threadState.hasPermit + } + else { + // When there is the second lock installed, it is not enough to look at the primary lock: + // the current thread now has inherited write intent permit, which should not give read access. + // Otherwise, it would be impossible to upgrade WI to W + return !mySecondaryPermits.get().isNullOrEmpty() + } + } override fun executeOnPooledThread(action: Runnable, expired: BooleanSupplier): Future<*> { val actionDecorated = decorateRunnable(action) diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/ApplicationImplTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/ApplicationImplTest.java index f114cfaaec47..0c3c28c11bc7 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/ApplicationImplTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/ApplicationImplTest.java @@ -402,7 +402,7 @@ public class ApplicationImplTest extends LightPlatformTestCase { ProgressManager.getInstance().runProcessWithProgressSynchronously((ThrowableComputable)() -> { try { assertFalse(ApplicationManager.getApplication().holdsReadLock()); - assertEquals(isLockStoredInContext(), ApplicationManager.getApplication().isReadAccessAllowed()); + assertFalse(ApplicationManager.getApplication().isReadAccessAllowed()); assertFalse(ApplicationManager.getApplication().isDispatchThread()); ApplicationManager.getApplication().assertIsNonDispatchThread(); diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/progress/RunWithModalProgressBlockingTest.kt b/platform/platform-tests/testSrc/com/intellij/openapi/progress/RunWithModalProgressBlockingTest.kt index f48cc6534a48..0bc174eb2f01 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/progress/RunWithModalProgressBlockingTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/openapi/progress/RunWithModalProgressBlockingTest.kt @@ -26,6 +26,7 @@ import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger +import java.lang.Runnable import kotlin.coroutines.ContinuationInterceptor import kotlin.time.Duration.Companion.seconds @@ -446,6 +447,57 @@ class RunWithModalProgressBlockingTest : ModalCoroutineTest() { assertTrue { application.isReadAccessAllowed } } } + + @Suppress("ForbiddenInSuspectContextMethod") + @Test + fun `RA and WA are mutually exclusive inside modal progress`(): Unit = timeoutRunBlocking(context = Dispatchers.EDT) { + val writeCoroutineStarted = Job() + val writeActionCanStart = Job() + + val inWaCounter = AtomicBoolean(false) + + writeIntentReadAction { + runWithModalProgressBlocking { + launch(Dispatchers.EDT) { + writeCoroutineStarted.complete() + writeActionCanStart.join() + writeAction { + inWaCounter.set(true) + } + } + ReadAction.nonBlocking { + writeCoroutineStarted.asCompletableFuture().join() + writeActionCanStart.complete() + Thread.sleep(100) + assertFalse(inWaCounter.get()) + }.executeSynchronously() + } + } + } + + @Test + fun `read access is not allowed by default within modal progress`() = timeoutRunBlocking(context = Dispatchers.EDT) { + writeIntentReadAction { + runWithModalProgressBlocking { + assertFalse(ApplicationManager.getApplication().isReadAccessAllowed) + readAction { + assertTrue(ApplicationManager.getApplication().isReadAccessAllowed) + } + } + } + } + + @Test + fun `write intent read access is not allowed by default within modal progress`() = timeoutRunBlocking(context = Dispatchers.EDT) { + writeIntentReadAction { + runWithModalProgressBlocking { + assertFalse(ApplicationManager.getApplication().isWriteIntentLockAcquired) + withContext(Dispatchers.EDT) { + assertTrue(ApplicationManager.getApplication().isWriteIntentLockAcquired) + } + } + } + } } private fun CoroutineScope.runWithModalProgressBlockingCoroutine(action: suspend CoroutineScope.() -> Unit): Job {