/* * Copyright 2003-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 jetbrains.mps.smodel; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.command.UndoConfirmationPolicy; import com.intellij.openapi.components.ApplicationComponent; import jetbrains.mps.ide.project.ProjectHelper; import jetbrains.mps.project.MPSProject; import jetbrains.mps.project.Project; import jetbrains.mps.smodel.undo.DefaultUndoContext; import jetbrains.mps.smodel.undo.UndoContext; import jetbrains.mps.util.Computable; import jetbrains.mps.util.ComputeRunnable; import jetbrains.mps.util.Reference; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.annotations.Immutable; import org.jetbrains.mps.openapi.repository.CommandListener; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import static java.math.BigDecimal.valueOf; import static java.util.concurrent.TimeUnit.MILLISECONDS; /** * We access IDEA locking mechanism here in order to prevent different way of acquiring locks * We always first acquire IDEA's lock and only then acquire MPS's lock */ public final class WorkbenchModelAccess extends ModelAccess implements Disposable, ApplicationComponent { private static final int WAIT_FOR_WRITE_LOCK_MILLIS = 200; private static final String IDEA_WRITE_LOCK_FAIL = "Failed to acquire the IDEA write lock after having waited for %.3f s"; private final EDTExecutor myEDTExecutor = new EDTExecutor(); private final WriteActionTracker myWriteActionTracker; private final TryRunPlatformWriteHelper myTryPlatformWriteHelper; protected WorkbenchModelAccess() { myWriteActionTracker = new WriteActionTracker(); myTryPlatformWriteHelper = new TryRunPlatformWriteHelper(myWriteActionTracker); } @Override public void dispose() { myEDTExecutor.dispose(); myTryPlatformWriteHelper.dispose(); } @Override public void runReadAction(final Runnable r) { if (canRead()) { r.run(); return; } ApplicationManager.getApplication().runReadAction(() -> { getReadLock().lock(); try { r.run(); } finally { getReadLock().unlock(); } }); } @Override public <T> T runReadAction(final Computable<T> c) { if (canRead()) { return c.compute(); } ComputeRunnable<T> r = new ComputeRunnable<>(c); runReadAction(r); return r.getResult(); } @Override public void runWriteAction(final Runnable r) { if (canWrite()) { r.run(); return; } assertNotWriteFromRead(); Runnable runnable = () -> { getWriteLock().lock(); try { clearRepositoryStateCaches(); myWriteActionDispatcher.run(r); } finally { getWriteLock().unlock(); } }; if (isInEDT()) { try { myWriteActionTracker.writeActionScheduled(); ApplicationManager.getApplication().runWriteAction(runnable); } finally { myWriteActionTracker.writeActionProcessed(); } } else { ApplicationManager.getApplication().runReadAction(runnable); } } @Override public <T> T runWriteAction(final Computable<T> c) { if (canWrite()) { return c.compute(); } assertNotWriteFromRead(); ComputeRunnable<T> r = new ComputeRunnable<>(c); runWriteAction(r); return r.getResult(); } private void assertNotWriteFromRead() { if (canRead()) { throw new IllegalStateException("deadlock prevention: do not start write action from read"); } } @Override public void flushEventQueue() { myEDTExecutor.flushEventsQueue(); } @Override public void runReadInEDT(Runnable r) { myEDTExecutor.scheduleRead(() -> tryRead(r)); } @Override public void runWriteInEDT(Runnable r) { myEDTExecutor.scheduleWrite(() -> tryWrite(r)); } @Override public void runCommandInEDT(@NotNull Runnable r, @NotNull Project project) { myEDTExecutor.scheduleCommand(() -> tryWriteInCommand(r, (MPSProject) project), project); } @Override public boolean isInEDT() { return ApplicationManager.getApplication().isDispatchThread(); } @Override public boolean tryRead(final Runnable r) { if (canRead()) { r.run(); return true; } return ApplicationManager.getApplication().runReadAction((com.intellij.openapi.util.Computable<Boolean>) () -> { if (getReadLock().tryLock()) { try { r.run(); } finally { getReadLock().unlock(); } return true; } else { return false; } }); } @Override public <T> T tryRead(final Computable<T> c) { if (canRead()) { return c.compute(); } ComputeRunnable<T> r = new ComputeRunnable<>(c); if (tryRead(r)) { return r.getResult(); } return null; } private boolean tryWrite(final Runnable r) { Computable<Boolean> c = () -> { r.run(); return true; }; Boolean res = tryWrite(c); return res != null ? res : false; } private <T> T tryWrite(final Computable<T> c) { if (canWrite()) { return c.compute(); } // idea.Computable, not mps.Computable to facilitate direct Application.runReadAction call below com.intellij.openapi.util.Computable<T> computable = () -> { try { if (getWriteLock().tryLock(WAIT_FOR_WRITE_LOCK_MILLIS, MILLISECONDS)) { try { clearRepositoryStateCaches(); return myWriteActionDispatcher.compute(c); } finally { getWriteLock().unlock(); } } else { return null; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); LOG.error("Interrupted while trying to access the MPS write lock", e); return null; } }; if (isInEDT()) { TaskTimer taskTimer = new TaskTimer(); taskTimer.start(); try { return myTryPlatformWriteHelper.tryWrite(computable); } catch (WriteTimeOutException e) { throw new TimeOutRuntimeException(String.format(IDEA_WRITE_LOCK_FAIL, taskTimer.secondsElapsed()), e); } } else { return ApplicationManager.getApplication().runReadAction(computable); } } /** * not thread-safe */ private static final class TaskTimer { private long myStartNanos; public void start() { myStartNanos = System.nanoTime(); } BigDecimal secondsElapsed() { return valueOf(System.nanoTime()) .subtract(valueOf(myStartNanos)) .divide(valueOf(1e9), BigDecimal.ROUND_DOWN); } } private boolean tryWriteInCommand(final Runnable r, @NotNull final MPSProject project) { ApplicationManager.getApplication().assertIsDispatchThread(); Reference<Boolean> lockGranted = new Reference<>(); com.intellij.openapi.project.Project ideaProject = project.getProject(); TaskTimer taskTimer = new TaskTimer(); try { myTryPlatformWriteHelper.tryCommand(ideaProject, () -> { try { if (getWriteLock().tryLock(WAIT_FOR_WRITE_LOCK_MILLIS, MILLISECONDS)) { try { clearRepositoryStateCaches(); myWriteActionDispatcher.run(new CommandRunnable(() -> { r.run(); lockGranted.set(true); }, project)); } finally { getWriteLock().unlock(); } } } catch (InterruptedException ie) { Thread.currentThread().interrupt(); LOG.error("Interrupted while trying to access the MPS write lock", ie); } }); } catch (WriteTimeOutException e) { throw new TimeOutRuntimeException(String.format(IDEA_WRITE_LOCK_FAIL, taskTimer.secondsElapsed()), e); } return lockGranted.get(); } @Override public void executeCommand(Runnable r, @Nullable Project project) { if (project == null) { project = CurrentProjectAccessUtil.getMPSProjectFromUI(); } String name = "MPS Execute Command", groupId = null; boolean confirmUndo = false; if (r instanceof UndoRunnable) { UndoRunnable ur = (UndoRunnable) r; name = ur.getName(); groupId = ur.getGroupId(); confirmUndo = ur.shallConfirmUndo(); } runWriteActionInCommand(r, name, groupId, confirmUndo, project); } private void runWriteActionInCommand(Runnable r, String name, Object groupId, boolean requestUndoConfirmation, Project project) { CommandProcessor.getInstance().executeCommand(ProjectHelper.toIdeaProject(project), new CommandRunnable(r, project), name, groupId, requestUndoConfirmation ? UndoConfirmationPolicy.REQUEST_CONFIRMATION : UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION); } @Override public void runUndoTransparentCommand(Runnable r) { runUndoTransparentCommand(r, CurrentProjectAccessUtil.getMPSProjectFromUI()); } @Override public void runUndoTransparentCommand(Runnable r, Project project) { if (myCommandLevel > 0) { throw new IllegalStateException("undo transparent action cannot be invoked in a command"); } CommandProcessor.getInstance().runUndoTransparentAction(new CommandRunnable(r, project)); } @Override public boolean isInsideCommand() { return canWrite() && myCommandLevel > 0; } @Override public boolean hasScheduledWrites() { return myWriteActionTracker.hasScheduledWrites() || super.hasScheduledWrites(); } //--------command events listening private List<CommandListener> myListeners = new ArrayList<>(); private final Object myListenersLock = new Object(); private int myCommandLevel = 0; private void incCommandLevel(Runnable command) { checkWriteAccess(); if (myCommandLevel != 0) { // LOG.error("command level>0", new Exception()); } else { UndoContext context; if (command instanceof UndoContext) { context = (UndoContext) command; } else { context = new DefaultUndoContext(); } UndoHelper.getInstance().startCommand(context); onCommandStarted(); } myCommandLevel++; } private void decCommandLevel(Project p) { checkWriteAccess(); myCommandLevel--; if (myCommandLevel == 0) { UndoHelper.getInstance().flushCommand(p); onCommandFinished(); } } @Override public void addCommandListener(CommandListener l) { synchronized (myListenersLock) { myListeners.add(l); } } @Override public void removeCommandListener(CommandListener l) { synchronized (myListenersLock) { myListeners.remove(l); } } @Override protected void onCommandStarted() { super.onCommandStarted(); ArrayList<CommandListener> listeners; synchronized (myListenersLock) { listeners = new ArrayList<>(myListeners); } for (CommandListener l : listeners) { try { l.commandStarted(); } catch (Throwable t) { LOG.error(null, t); } } } @Override protected void onCommandFinished() { ArrayList<CommandListener> listeners; synchronized (myListenersLock) { listeners = new ArrayList<>(myListeners); } for (CommandListener l : listeners) { try { l.commandFinished(); } catch (Throwable t) { LOG.error(null, t); } } super.onCommandFinished(); } @Override public void initComponent() { // not allowing to substitute alien model accesses here assert instance() instanceof DefaultModelAccess; setInstance(this); } @Override public void disposeComponent() { setInstance(new DefaultModelAccess()); dispose(); } @NotNull @Override public String getComponentName() { return getClass().getSimpleName(); } @Immutable private final class CommandRunnable implements Runnable { private final Runnable myRunnable; private final Project myProject; CommandRunnable(Runnable r, Project project) { myRunnable = r; myProject = project; } @Override public void run() { WorkbenchModelAccess.this.runWriteAction(() -> { incCommandLevel(myRunnable); try { myRunnable.run(); } finally { decCommandLevel(myProject); } }); } } }