WEB-13145 "Single instance only" not respected appcode/146.1753 clion/146.1754 dbe/146.1749 idea/146.1750 phpstorm/146.1752 pycharm/146.1751 rubymine/146.1755
authorSergey Simonchik <sergey.simonchik@jetbrains.com>
Thu, 5 May 2016 19:21:44 +0000 (22:21 +0300)
committerSergey Simonchik <sergey.simonchik@jetbrains.com>
Thu, 5 May 2016 19:22:10 +0000 (22:22 +0300)
platform/lang-impl/src/com/intellij/execution/impl/ExecutionManagerImpl.java
platform/lang-impl/testSources/com/intellij/execution/impl/ExecutionManagerTest.java [new file with mode: 0644]
platform/lang-impl/testSources/com/intellij/execution/impl/FakeConfigurationFactory.java [new file with mode: 0644]
platform/lang-impl/testSources/com/intellij/execution/impl/FakeConfigurationType.java [new file with mode: 0644]
platform/lang-impl/testSources/com/intellij/execution/impl/FakeProcessHandler.java [new file with mode: 0644]
platform/lang-impl/testSources/com/intellij/execution/impl/FakeProgramRunner.java [new file with mode: 0644]
platform/lang-impl/testSources/com/intellij/execution/impl/FakeRunConfiguration.java [new file with mode: 0644]

index 06c716221c7cb48edf80dd16309bd44e559d4fbc..82f7d430db52ea5f0f312cd8e20f580fd876d54b 100644 (file)
@@ -36,6 +36,7 @@ import com.intellij.openapi.Disposable;
 import com.intellij.openapi.actionSystem.DataContext;
 import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
 import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ModalityState;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.progress.ProcessCanceledException;
@@ -59,10 +60,7 @@ import org.jetbrains.annotations.Nullable;
 import org.jetbrains.annotations.TestOnly;
 
 import javax.swing.*;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 
 public class ExecutionManagerImpl extends ExecutionManager implements Disposable {
   public static final Key<Object> EXECUTION_SESSION_ID_KEY = Key.create("EXECUTION_SESSION_ID_KEY");
@@ -72,7 +70,8 @@ public class ExecutionManagerImpl extends ExecutionManager implements Disposable
   private static final ProcessHandler[] EMPTY_PROCESS_HANDLERS = new ProcessHandler[0];
 
   private final Project myProject;
-  private final Alarm awaitingTerminationAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
+  private final Alarm myAwaitingTerminationAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
+  private final Map<RunProfile, ExecutionEnvironment> myAwaitingRunProfiles = ContainerUtil.newHashMap();
   private final List<Trinity<RunContentDescriptor, RunnerAndConfigurationSettings, Executor>> myRunningConfigurations =
     ContainerUtil.createLockFreeCopyOnWriteList();
   private RunContentManagerImpl myContentManager;
@@ -479,26 +478,46 @@ public class ExecutionManagerImpl extends ExecutionManager implements Disposable
       }
     }
 
-    awaitingTerminationAlarm.addRequest(new Runnable() {
+    if (myAwaitingRunProfiles.get(environment.getRunProfile()) == environment) {
+      // defense from rerunning exactly the same ExecutionEnvironment
+      return;
+    }
+    myAwaitingRunProfiles.put(environment.getRunProfile(), environment);
+
+    awaitTermination(new Runnable() {
       @Override
       public void run() {
+        if (myAwaitingRunProfiles.get(environment.getRunProfile()) != environment) {
+          // a new rerun has been requested before starting this one, ignore this rerun
+          return;
+        }
         if ((DumbService.getInstance(myProject).isDumb() && !Registry.is("dumb.aware.run.configurations")) || ExecutorRegistry.getInstance().isStarting(environment)) {
-          awaitingTerminationAlarm.addRequest(this, 100);
+          awaitTermination(this, 100);
           return;
         }
 
         for (RunContentDescriptor descriptor : runningOfTheSameType) {
           ProcessHandler processHandler = descriptor.getProcessHandler();
           if (processHandler != null && !processHandler.isProcessTerminated()) {
-            awaitingTerminationAlarm.addRequest(this, 100);
+            awaitTermination(this, 100);
             return;
           }
         }
+        myAwaitingRunProfiles.remove(environment.getRunProfile());
         start(environment);
       }
     }, 50);
   }
 
+  private void awaitTermination(@NotNull Runnable request, long delayMillis) {
+    if (ApplicationManager.getApplication().isUnitTestMode()) {
+      ApplicationManager.getApplication().invokeLater(request, ModalityState.any());
+    }
+    else {
+      myAwaitingTerminationAlarm.addRequest(request, delayMillis);
+    }
+  }
+
   @TestOnly
   public void setForceCompilationInTests(boolean forceCompilationInTests) {
     myForceCompilationInTests = forceCompilationInTests;
diff --git a/platform/lang-impl/testSources/com/intellij/execution/impl/ExecutionManagerTest.java b/platform/lang-impl/testSources/com/intellij/execution/impl/ExecutionManagerTest.java
new file mode 100644 (file)
index 0000000..7557f77
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * 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.execution.impl;
+
+import com.intellij.execution.RunManagerConfig;
+import com.intellij.execution.RunnerAndConfigurationSettings;
+import com.intellij.execution.executors.DefaultRunExecutor;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.runners.ExecutionEnvironmentBuilder;
+import com.intellij.execution.ui.RunContentDescriptor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Conditions;
+import com.intellij.testFramework.LightPlatformTestCase;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class ExecutionManagerTest extends LightPlatformTestCase {
+
+  private boolean myRestartRequiresConfirmation;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    RunManagerConfig config = RunManagerImpl.getInstanceImpl(getProject()).getConfig();
+    myRestartRequiresConfirmation = config.isRestartRequiresConfirmation();
+    config.setRestartRequiresConfirmation(false);
+  }
+
+  @Override
+  public void tearDown() throws Exception {
+    try {
+      RunManagerConfig config = RunManagerImpl.getInstanceImpl(getProject()).getConfig();
+      config.setRestartRequiresConfirmation(myRestartRequiresConfirmation);
+    }
+    finally {
+      super.tearDown();
+    }
+  }
+
+  public void testRerunSingleton() throws Exception {
+    Project project = getProject();
+    ExecutionManagerImpl executionManager = ExecutionManagerImpl.getInstance(project);
+
+    FakeRunConfiguration rc = new FakeRunConfiguration(project, true);
+    RunnerAndConfigurationSettingsImpl settings = new RunnerAndConfigurationSettingsImpl(
+      RunManagerImpl.getInstanceImpl(project), rc, false
+    );
+    settings.setSingleton(true);
+
+    ExecutionEnvironment env1 = createEnv(project, settings);
+    executionManager.restartRunProfile(env1);
+    UIUtil.dispatchAllInvocationEvents();
+    ProcessHandler processHandler1 = getProcessHandler(executionManager);
+
+    ExecutionEnvironment env2 = createEnv(project, settings);
+    executionManager.restartRunProfile(env2);
+    UIUtil.dispatchInvocationEvent();
+
+    ProcessHandler processHandler2 = getProcessHandler(executionManager);
+    assertTrue(processHandler1 == processHandler2);
+    assertTrue(processHandler1.isProcessTerminating());
+
+    ExecutionEnvironment env3 = createEnv(project, settings);
+    executionManager.restartRunProfile(env3);
+    UIUtil.dispatchAllInvocationEvents();
+
+    FakeProcessHandler processHandler3 = getProcessHandler(executionManager);
+    assertTrue(processHandler1 != processHandler3);
+
+    assertTrue(!processHandler3.isProcessTerminating() && !processHandler3.isProcessTerminated());
+    processHandler3.killProcess();
+  }
+
+  @NotNull
+  private static FakeProcessHandler getProcessHandler(@NotNull ExecutionManagerImpl executionManager) {
+    List<RunContentDescriptor> descriptors = executionManager.getRunningDescriptors(Conditions.alwaysTrue());
+    assertEquals(1, descriptors.size());
+    RunContentDescriptor descriptor = ContainerUtil.getFirstItem(descriptors);
+    assertNotNull(descriptor);
+    ProcessHandler processHandler = descriptor.getProcessHandler();
+    assertNotNull(processHandler);
+    return (FakeProcessHandler)processHandler;
+  }
+
+  @NotNull
+  private static ExecutionEnvironment createEnv(@NotNull Project project, @NotNull RunnerAndConfigurationSettings settings) {
+    return new ExecutionEnvironmentBuilder(project, DefaultRunExecutor.getRunExecutorInstance())
+      .runnerAndSettings(FakeProgramRunner.INSTANCE, settings)
+      .build();
+  }
+}
diff --git a/platform/lang-impl/testSources/com/intellij/execution/impl/FakeConfigurationFactory.java b/platform/lang-impl/testSources/com/intellij/execution/impl/FakeConfigurationFactory.java
new file mode 100644 (file)
index 0000000..066660a
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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.execution.impl;
+
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+public class FakeConfigurationFactory extends ConfigurationFactory {
+
+  public static final FakeConfigurationFactory INSTANCE = new FakeConfigurationFactory();
+
+  public FakeConfigurationFactory() {
+    super(new FakeConfigurationType());
+  }
+
+  @NotNull
+  @Override
+  public RunConfiguration createTemplateConfiguration(@NotNull Project project) {
+    throw new UnsupportedOperationException("Not Implemented");
+  }
+}
+
+
+
diff --git a/platform/lang-impl/testSources/com/intellij/execution/impl/FakeConfigurationType.java b/platform/lang-impl/testSources/com/intellij/execution/impl/FakeConfigurationType.java
new file mode 100644 (file)
index 0000000..026a663
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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.execution.impl;
+
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.ConfigurationType;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+
+public class FakeConfigurationType implements ConfigurationType {
+  @Override
+  public String getDisplayName() {
+    return "Fake";
+  }
+
+  @Override
+  public String getConfigurationTypeDescription() {
+    return "Fake";
+  }
+
+  @Override
+  public Icon getIcon() {
+    return null;
+  }
+
+  @Override
+  @NotNull
+  public String getId() {
+    return FakeConfigurationType.class.getSimpleName();
+  }
+
+  @Override
+  public ConfigurationFactory[] getConfigurationFactories() {
+    return new ConfigurationFactory[0];
+  }
+}
diff --git a/platform/lang-impl/testSources/com/intellij/execution/impl/FakeProcessHandler.java b/platform/lang-impl/testSources/com/intellij/execution/impl/FakeProcessHandler.java
new file mode 100644 (file)
index 0000000..60eaadc
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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.execution.impl;
+
+import com.intellij.execution.KillableProcess;
+import com.intellij.execution.process.ProcessHandler;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.OutputStream;
+
+public class FakeProcessHandler extends ProcessHandler implements KillableProcess {
+
+  private final boolean mySurviveSoftKill;
+
+  public FakeProcessHandler(boolean surviveSoftKill) {
+    mySurviveSoftKill = surviveSoftKill;
+  }
+
+  @Override
+  protected void destroyProcessImpl() {
+    if (!mySurviveSoftKill) {
+      notifyProcessTerminated(0);
+    }
+  }
+
+  @Override
+  protected void detachProcessImpl() {
+    notifyProcessTerminated(0);
+  }
+
+  @Override
+  public boolean detachIsDefault() {
+    return false;
+  }
+
+  @Nullable
+  @Override
+  public OutputStream getProcessInput() {
+    return null;
+  }
+
+  @Override
+  public boolean canKillProcess() {
+    return true;
+  }
+
+  @Override
+  public void killProcess() {
+    notifyProcessTerminated(0);
+  }
+}
diff --git a/platform/lang-impl/testSources/com/intellij/execution/impl/FakeProgramRunner.java b/platform/lang-impl/testSources/com/intellij/execution/impl/FakeProgramRunner.java
new file mode 100644 (file)
index 0000000..fcce2a1
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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.execution.impl;
+
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.ExecutionResult;
+import com.intellij.execution.configurations.RunProfile;
+import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.runners.GenericProgramRunner;
+import com.intellij.execution.runners.RunContentBuilder;
+import com.intellij.execution.ui.RunContentDescriptor;
+import org.jetbrains.annotations.NotNull;
+
+public class FakeProgramRunner extends GenericProgramRunner {
+
+  public static final FakeProgramRunner INSTANCE = new FakeProgramRunner();
+
+  @NotNull
+  @Override
+  public String getRunnerId() {
+    return "MockProgramRunner";
+  }
+
+  @Override
+  protected RunContentDescriptor doExecute(@NotNull RunProfileState state, @NotNull ExecutionEnvironment env) throws ExecutionException {
+    ExecutionResult executionResult = state.execute(env.getExecutor(), this);
+    if (executionResult == null) {
+      return null;
+    }
+    return new RunContentBuilder(executionResult, env).showRunContent(env.getContentToReuse());
+  }
+
+  @Override
+  public boolean canRun(@NotNull String executorId, @NotNull RunProfile profile) {
+    return true;
+  }
+}
diff --git a/platform/lang-impl/testSources/com/intellij/execution/impl/FakeRunConfiguration.java b/platform/lang-impl/testSources/com/intellij/execution/impl/FakeRunConfiguration.java
new file mode 100644 (file)
index 0000000..b77a3b4
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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.execution.impl;
+
+import com.intellij.execution.DefaultExecutionResult;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.ExecutionResult;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.LocatableConfigurationBase;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.runners.ProgramRunner;
+import com.intellij.openapi.options.SettingsEditor;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class FakeRunConfiguration extends LocatableConfigurationBase {
+
+  private static AtomicInteger CREATED_INSTANCES = new AtomicInteger(0);
+
+  private final boolean mySurviveSoftKill;
+
+  protected FakeRunConfiguration(@NotNull Project project, boolean surviveSoftKill) {
+    super(project, FakeConfigurationFactory.INSTANCE, nextName());
+    mySurviveSoftKill = surviveSoftKill;
+  }
+
+  private static String nextName() {
+    return "Fake #" + CREATED_INSTANCES.incrementAndGet();
+  }
+
+  @NotNull
+  @Override
+  public SettingsEditor<? extends RunConfiguration> getConfigurationEditor() {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  @Nullable
+  @Override
+  public RunProfileState getState(@NotNull Executor executor, @NotNull ExecutionEnvironment environment) throws ExecutionException {
+    return new FakeRunProfileState();
+  }
+
+  public class FakeRunProfileState implements RunProfileState {
+    @Nullable
+    @Override
+    public ExecutionResult execute(Executor executor, @NotNull ProgramRunner runner) throws ExecutionException {
+      return new DefaultExecutionResult(null, new FakeProcessHandler(mySurviveSoftKill));
+    }
+  }
+}