// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.debugger.engine;

import com.intellij.Patches;
import com.intellij.ProjectTopics;
import com.intellij.ReviseWhenPortedToJDK;
import com.intellij.debugger.*;
import com.intellij.debugger.actions.DebuggerAction;
import com.intellij.debugger.engine.evaluation.*;
import com.intellij.debugger.engine.events.DebuggerCommandImpl;
import com.intellij.debugger.engine.events.DebuggerContextCommandImpl;
import com.intellij.debugger.engine.events.SuspendContextCommandImpl;
import com.intellij.debugger.engine.jdi.ThreadReferenceProxy;
import com.intellij.debugger.engine.requests.MethodReturnValueWatcher;
import com.intellij.debugger.engine.requests.RequestManagerImpl;
import com.intellij.debugger.impl.*;
import com.intellij.debugger.impl.attach.PidRemoteConnection;
import com.intellij.debugger.jdi.EmptyConnectorArgument;
import com.intellij.debugger.jdi.StackFrameProxyImpl;
import com.intellij.debugger.jdi.ThreadReferenceProxyImpl;
import com.intellij.debugger.jdi.VirtualMachineProxyImpl;
import com.intellij.debugger.settings.DebuggerSettings;
import com.intellij.debugger.settings.NodeRendererSettings;
import com.intellij.debugger.ui.breakpoints.*;
import com.intellij.debugger.ui.tree.render.*;
import com.intellij.execution.CantRunException;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.ExecutionResult;
import com.intellij.execution.configurations.RemoteConnection;
import com.intellij.execution.process.*;
import com.intellij.execution.runners.ExecutionUtil;
import com.intellij.idea.ActionsBundle;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.JavaSdkType;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ModuleRootEvent;
import com.intellij.openapi.roots.ModuleRootListener;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.openapi.wm.impl.status.StatusBarUtil;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.ui.classFilter.ClassFilter;
import com.intellij.ui.classFilter.DebuggerClassFilterProvider;
import com.intellij.util.Alarm;
import com.intellij.util.EventDispatcher;
import com.intellij.util.ObjectUtils;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.lang.JavaVersion;
import com.intellij.util.ui.UIUtil;
import com.intellij.xdebugger.XDebugSession;
import com.intellij.xdebugger.XDebuggerBundle;
import com.intellij.xdebugger.XSourcePosition;
import com.intellij.xdebugger.impl.XDebugSessionImpl;
import com.intellij.xdebugger.impl.XDebuggerManagerImpl;
import com.intellij.xdebugger.impl.actions.XDebuggerActions;
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil;
import com.jetbrains.jdi.ClassLoaderReferenceImpl;
import com.jetbrains.jdi.MethodImpl;
import com.jetbrains.jdi.VirtualMachineManagerImpl;
import com.sun.jdi.*;
import com.sun.jdi.connect.*;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;
import com.sun.jdi.request.StepRequest;
import one.util.streamex.StreamEx;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

public abstract class DebugProcessImpl extends UserDataHolderBase implements DebugProcess {
  private static final Logger LOG = Logger.getInstance(DebugProcessImpl.class);

  private final Project myProject;
  private final RequestManagerImpl myRequestManager;

  private final Deque<VirtualMachineData> myStashedVirtualMachines = new LinkedList<>();

  private volatile VirtualMachineProxyImpl myVirtualMachineProxy = null;
  protected final EventDispatcher<DebugProcessListener> myDebugProcessDispatcher = EventDispatcher.create(DebugProcessListener.class);
  protected final EventDispatcher<EvaluationListener> myEvaluationDispatcher = EventDispatcher.create(EvaluationListener.class);

  private final List<ProcessListener> myProcessListeners = ContainerUtil.createLockFreeCopyOnWriteList();
  private final StringBuilder myTextBeforeStart = new StringBuilder();

  protected enum State {INITIAL, ATTACHED, DETACHING, DETACHED}

  protected final AtomicReference<State> myState = new AtomicReference<>(State.INITIAL);

  private volatile ExecutionResult myExecutionResult;
  private volatile RemoteConnection myConnection;
  private JavaDebugProcess myXDebugProcess;

  private volatile Map<String, Connector.Argument> myArguments;

  private final List<NodeRenderer> myRenderers = new ArrayList<>();

  // we use null key here
  private final Map<Type, Object> myNodeRenderersMap = new HashMap<>();

  private final SuspendManagerImpl mySuspendManager = new SuspendManagerImpl(this);
  protected CompoundPositionManager myPositionManager = CompoundPositionManager.EMPTY;
  private volatile @NotNull DebuggerManagerThreadImpl myDebuggerManagerThread;

  private final Semaphore myWaitFor = new Semaphore();
  private final AtomicBoolean myIsFailed = new AtomicBoolean(false);
  private final AtomicBoolean myIsStopped = new AtomicBoolean(false);
  protected volatile DebuggerSession mySession;
  @Nullable protected MethodReturnValueWatcher myReturnValueWatcher;
  protected final Disposable myDisposable = Disposer.newDisposable();
  private final Alarm myStatusUpdateAlarm = new Alarm();

  private final ThreadBlockedMonitor myThreadBlockedMonitor = new ThreadBlockedMonitor(this, myDisposable);

  protected DebugProcessImpl(Project project) {
    myProject = project;
    myDebuggerManagerThread = new DebuggerManagerThreadImpl(myDisposable, myProject);
    myRequestManager = new RequestManagerImpl(this);
    NodeRendererSettings.getInstance().addListener(this::reloadRenderers, myDisposable);
    NodeRenderer.EP_NAME.addChangeListener(this::reloadRenderers, myDisposable);
    CompoundRendererProvider.EP_NAME.addChangeListener(this::reloadRenderers, myDisposable);
    addDebugProcessListener(new DebugProcessAdapterImpl() {
      @Override
      public void processAttached(DebugProcessImpl process) {
        reloadRenderers();
      }
    }, myDisposable);
    myDebugProcessDispatcher.addListener(new DebugProcessListener() {
      @Override
      public void paused(@NotNull SuspendContext suspendContext) {
        myThreadBlockedMonitor.stopWatching(
          suspendContext.getSuspendPolicy() != EventRequest.SUSPEND_ALL ? suspendContext.getThread() : null);
      }
    });
  }

  private void reloadRenderers() {
    getManagerThread().invoke(new DebuggerCommandImpl(PrioritizedTask.Priority.HIGH) {
      @Override
      protected void action() {
        myNodeRenderersMap.clear();
        myRenderers.clear();
        try {
          myRenderers.addAll(NodeRendererSettings.getInstance().getAllRenderers(myProject));
        }
        finally {
          DebuggerInvocationUtil.swingInvokeLater(myProject, () -> {
            final DebuggerSession session = mySession;
            if (session != null && session.isAttached()) {
              DebuggerAction.refreshViews(mySession.getXDebugSession());
            }
          });
        }
      }
    });
  }

  @Nullable
  public Pair<Method, Value> getLastExecutedMethod() {
    final MethodReturnValueWatcher watcher = myReturnValueWatcher;
    if (watcher == null) {
      return null;
    }
    final Method method = watcher.getLastExecutedMethod();
    if (method == null) {
      return null;
    }
    return Pair.create(method, watcher.getLastMethodReturnValue());
  }

  public void setWatchMethodReturnValuesEnabled(boolean enabled) {
    if (myReturnValueWatcher != null) {
      myReturnValueWatcher.setEnabled(enabled);
    }
  }

  public boolean canGetMethodReturnValue() {
    return myReturnValueWatcher != null;
  }

  @NotNull
  public CompletableFuture<List<NodeRenderer>> getApplicableRenderers(Type type) {
    return DebuggerUtilsImpl.getApplicableRenderers(myRenderers, type);
  }

  @NotNull
  public CompletableFuture<NodeRenderer> getAutoRendererAsync(@Nullable Type type) {
    DebuggerManagerThreadImpl.assertIsManagerThread();
    // in case evaluation is not possible, force default renderer
    if (!isEvaluationPossible()) {
      return CompletableFuture.completedFuture(getDefaultRenderer(type));
    }

    try {
      Object renderer = myNodeRenderersMap.get(type);
      if (renderer instanceof NodeRenderer) {
        return CompletableFuture.completedFuture((NodeRenderer)renderer);
      }
      else if (renderer instanceof CompletableFuture) {
        //noinspection unchecked
        CompletableFuture<NodeRenderer> future = (CompletableFuture<NodeRenderer>)renderer;
        LOG.assertTrue(!future.isDone(), "Completed future for " + type);
        return future;
      }
      List<NodeRenderer> enabledRenderers = ContainerUtil.filter(myRenderers, NodeRenderer::isEnabled);
      CompletableFuture<NodeRenderer> res = DebuggerUtilsImpl.getFirstApplicableRenderer(enabledRenderers, type)
        .handle((r, throwable) -> {
          DebuggerManagerThreadImpl.assertIsManagerThread();
          if (r == null || throwable != null) {
            r = getDefaultRenderer(type); // do not cache the fallback renderer
            myNodeRenderersMap.remove(type);
          }
          else {
            // TODO: may add a new (possibly incorrect) value after the cleanup in reloadRenderers
            myNodeRenderersMap.put(type, r);
          }
          return r;
        });
      // check if future is already done
      myNodeRenderersMap.putIfAbsent(type, res);
      return res;
    }
    catch (ClassNotPreparedException e) {
      LOG.info(e);
      // use default, but do not cache
      return CompletableFuture.completedFuture(getDefaultRenderer(type));
    }
  }

  @NotNull
  public static NodeRenderer getDefaultRenderer(Value value) {
    return getDefaultRenderer(value != null ? value.type() : null);
  }

  @NotNull
  public static NodeRenderer getDefaultRenderer(Type type) {
    final NodeRendererSettings settings = NodeRendererSettings.getInstance();

    final PrimitiveRenderer primitiveRenderer = settings.getPrimitiveRenderer();
    if (primitiveRenderer.isApplicable(type)) {
      return primitiveRenderer;
    }

    final ArrayRenderer arrayRenderer = settings.getArrayRenderer();
    if (arrayRenderer.isApplicable(type)) {
      return arrayRenderer;
    }

    final ClassRenderer classRenderer = settings.getClassRenderer();
    LOG.assertTrue(classRenderer.isApplicable(type), type.name());
    return classRenderer;
  }

  private static final int ourTraceMask;

  static {
    String traceStr = System.getProperty("idea.debugger.trace");
    int mask = 0;
    if (!StringUtil.isEmpty(traceStr)) {
      StringTokenizer tokenizer = new StringTokenizer(traceStr);
      while (tokenizer.hasMoreTokens()) {
        String token = tokenizer.nextToken();
        if ("SENDS".compareToIgnoreCase(token) == 0) {
          mask |= VirtualMachine.TRACE_SENDS;
        }
        else if ("RAW_SENDS".compareToIgnoreCase(token) == 0) {
          mask |= 0x01000000;
        }
        else if ("RECEIVES".compareToIgnoreCase(token) == 0) {
          mask |= VirtualMachine.TRACE_RECEIVES;
        }
        else if ("RAW_RECEIVES".compareToIgnoreCase(token) == 0) {
          mask |= 0x02000000;
        }
        else if ("EVENTS".compareToIgnoreCase(token) == 0) {
          mask |= VirtualMachine.TRACE_EVENTS;
        }
        else if ("REFTYPES".compareToIgnoreCase(token) == 0) {
          mask |= VirtualMachine.TRACE_REFTYPES;
        }
        else if ("OBJREFS".compareToIgnoreCase(token) == 0) {
          mask |= VirtualMachine.TRACE_OBJREFS;
        }
        else if ("ALL".compareToIgnoreCase(token) == 0) {
          mask |= VirtualMachine.TRACE_ALL;
        }
      }
    }
    ourTraceMask = mask;
  }

  protected int getTraceMask() {
    int mask = ourTraceMask;
    DebugEnvironment environment = mySession.getDebugEnvironment();
    if (environment instanceof DefaultDebugEnvironment) {
      mask |= ((DefaultDebugEnvironment)environment).getTraceMode();
    }
    return mask;
  }

  protected void commitVM(VirtualMachine vm) {
    if (!isInInitialState()) {
      LOG.error("State is invalid " + myState.get());
    }
    DebuggerManagerThreadImpl.assertIsManagerThread();
    myPositionManager = new CompoundPositionManager(new PositionManagerImpl(this));
    myProject.getMessageBus().connect(myDisposable).subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
      @Override
      public void rootsChanged(@NotNull final ModuleRootEvent event) {
        DumbService.getInstance(myProject).runWhenSmart(
          () -> getManagerThread().schedule(PrioritizedTask.Priority.HIGH, () -> {
            myPositionManager.clearCache();
            DebuggerSession session = mySession;
            if (session != null && session.isAttached()) {
              DebuggerUIUtil.invokeLater(() -> session.refresh(true));
            }
          }));
      }
    });
    LOG.debug("*******************VM attached******************");

    vm.setDebugTraceMode(getTraceMask());

    checkVirtualMachineVersion(vm);
    myVirtualMachineProxy = new VirtualMachineProxyImpl(this, vm);
  }

  private void stopConnecting() {
    Map<String, Connector.Argument> arguments = myArguments;
    try {
      if (arguments == null) {
        return;
      }
      if (myConnection.isServerMode()) {
        Connector connector = getConnector();
        if (connector instanceof ListeningConnector) {
          ((ListeningConnector)connector).stopListening(arguments);
        }
      }
    }
    catch (IOException | IllegalConnectorArgumentsException | IllegalArgumentException e) {
      LOG.debug(e);
    }
    catch (ExecutionException e) {
      LOG.error(e);
    }
  }

  @Override
  public void printToConsole(final String text) {
    synchronized (myProcessListeners) {
      if (myExecutionResult == null) {
        myTextBeforeStart.append(text);
      }
      else {
        printToConsoleImpl(text);
      }
    }
  }

  private void printToConsoleImpl(String text) {
    myExecutionResult.getProcessHandler().notifyTextAvailable(text, ProcessOutputTypes.SYSTEM);
  }

  @Override
  public ProcessHandler getProcessHandler() {
    return myExecutionResult != null ? myExecutionResult.getProcessHandler() : null;
  }

  /**
   * @param size the step size. One of {@link StepRequest#STEP_LINE} or {@link StepRequest#STEP_MIN}
   * @param hint may be null
   */
  protected void doStep(final SuspendContextImpl suspendContext, final ThreadReferenceProxyImpl stepThread, int size, int depth,
                        RequestHint hint) {
    if (stepThread == null) {
      return;
    }
    try {
      final ThreadReference stepThreadReference = stepThread.getThreadReference();
      if (LOG.isDebugEnabled()) {
        LOG.debug("DO_STEP: creating step request for " + stepThreadReference);
      }
      deleteStepRequests(stepThreadReference);
      EventRequestManager requestManager = getVirtualMachineProxy().eventRequestManager();
      StepRequest stepRequest = requestManager.createStepRequest(stepThreadReference, size, depth);
      if (!(hint != null && hint.isIgnoreFilters()) && !isPositionFiltered(getLocation(stepThread, suspendContext))) {
        getActiveFilters().forEach(f -> stepRequest.addClassExclusionFilter(f.getPattern()));
      }

      // suspend policy to match the suspend policy of the context:
      // if all threads were suspended, then during stepping all the threads must be suspended
      // if only event thread was suspended, then only this particular thread must be suspended during stepping
      stepRequest.setSuspendPolicy(suspendContext.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD
                                   ? EventRequest.SUSPEND_EVENT_THREAD
                                   : EventRequest.SUSPEND_ALL);

      stepRequest.addCountFilter(1);

      if (hint != null) {
        stepRequest.putProperty("hint", hint);
      }
      DebuggerUtilsAsync.setEnabled(stepRequest, true).whenComplete((__, e) -> {
        if (DebuggerUtilsAsync.unwrap(e) instanceof IllegalThreadStateException) {
          DebuggerUtilsAsync.deleteEventRequest(requestManager, stepRequest);
        }
      });
    }
    catch (ObjectCollectedException ignored) {

    }
  }

  static boolean isPositionFiltered(Location location) {
    List<ClassFilter> activeFilters = getActiveFilters();
    if (!activeFilters.isEmpty()) {
      ReferenceType referenceType = location != null ? location.declaringType() : null;
      if (referenceType != null) {
        String currentClassName = referenceType.name();
        return currentClassName != null && DebuggerUtilsEx.isFiltered(currentClassName, activeFilters);
      }
    }
    return false;
  }

  public static boolean isClassFiltered(@Nullable String name) {
    if (name == null) {
      return false;
    }
    return DebuggerUtilsEx.isFiltered(name, getActiveFilters());
  }

  @NotNull
  private static List<ClassFilter> getActiveFilters() {
    DebuggerSettings settings = DebuggerSettings.getInstance();
    StreamEx<ClassFilter> stream = StreamEx.of(DebuggerClassFilterProvider.EP_NAME.getExtensionList())
      .flatCollection(DebuggerClassFilterProvider::getFilters);
    if (settings.TRACING_FILTERS_ENABLED) {
      stream = stream.prepend(settings.getSteppingFilters());
    }
    return stream.filter(ClassFilter::isEnabled).toList();
  }

  void deleteStepRequests(@Nullable final ThreadReference stepThread) {
    EventRequestManager requestManager = getVirtualMachineProxy().eventRequestManager();
    for (StepRequest request : new ArrayList<>(requestManager.stepRequests())) { // need a copy here to avoid CME
      if (stepThread == null || stepThread.equals(request.thread())) {
        try {
          DebuggerUtilsAsync.deleteEventRequest(requestManager, request);
        }
        catch (ObjectCollectedException ignored) {
        }
        catch (VMDisconnectedException e) {
          throw e;
        }
        catch (Exception e) {
          LOG.error(e); // report all for now
        }
      }
    }
  }

  static int getFrameCount(@Nullable ThreadReferenceProxyImpl thread, @NotNull SuspendContextImpl suspendContext) {
    if (thread != null) {
      if (thread.equals(suspendContext.getThread())) {
        return suspendContext.frameCount();
      }
      else {
        try {
          return thread.frameCount();
        }
        catch (EvaluateException ignored) {
        }
      }
    }
    return 0;
  }

  @Nullable
  static Location getLocation(@Nullable ThreadReferenceProxyImpl thread, @NotNull SuspendContextImpl suspendContext) {
    if (thread != null) {
      if (thread.equals(suspendContext.getThread())) {
        return suspendContext.getLocation();
      }
      else {
        try {
          if (thread.frameCount() > 0) {
            StackFrameProxyImpl stackFrame = thread.frame(0);
            if (stackFrame != null) {
              return stackFrame.location();
            }
          }
        }
        catch (EvaluateException ignored) {
        }
      }
    }
    return null;
  }

  private VirtualMachine createVirtualMachineInt() throws ExecutionException {
    try {
      if (myArguments != null) {
        throw new IOException(JavaDebuggerBundle.message("error.debugger.already.listening"));
      }

      final String port = myConnection.getDebuggerAddress();

      Connector connector = getConnector();
      myArguments = connector.defaultArguments();
      if (myConnection instanceof PidRemoteConnection && !((PidRemoteConnection)myConnection).isFixedAddress()) {
        String pid = ((PidRemoteConnection)myConnection).getPid();
        if (StringUtil.isEmpty(pid)) {
          throw new CantRunException(JavaDebuggerBundle.message("error.no.pid"));
        }
        setConnectorArgument("pid", pid);
      }
      else if (myConnection.isServerMode()) {
        if (myArguments == null) {
          throw new CantRunException(JavaDebuggerBundle.message("error.no.debug.listen.port"));
        }
      }
      else { // is client mode, should attach to already running process
        if (myConnection.isUseSockets()) {
          String debuggerHostName = myConnection.getDebuggerHostName();
          if (debuggerHostName != null) {
            setConnectorArgument("hostname", debuggerHostName);
          }
          if (port == null) {
            throw new CantRunException(JavaDebuggerBundle.message("error.no.debug.attach.port"));
          }
          setConnectorArgument("port", port);
        }
        else {
          if (port == null) {
            throw new CantRunException(JavaDebuggerBundle.message("error.no.shmem.address"));
          }
          setConnectorArgument("name", port);
        }
        setConnectorArgument("timeout", "0"); // wait forever
      }
      return connector instanceof AttachingConnector
             ? attachConnector((AttachingConnector)connector)
             : connectorListen(port, (ListeningConnector)connector);
    }
    catch (IOException e) {
      throw new ExecutionException(processIOException(e, JavaDebuggerBundle.getAddressDisplayName(myConnection)), e);
    }
    catch (IllegalConnectorArgumentsException e) {
      throw new ExecutionException(processError(e), e);
    }
    finally {
      myArguments = null;
    }
  }

  private void setConnectorArgument(String name, String value) {
    Connector.Argument argument = myArguments.get(name);
    if (argument != null) {
      argument.setValue(value);
    }
  }

  @ReviseWhenPortedToJDK("13")
  private VirtualMachine connectorListen(String address, ListeningConnector connector)
    throws CantRunException, IOException, IllegalConnectorArgumentsException {
    if (address == null) {
      throw new CantRunException(JavaDebuggerBundle.message("error.no.debug.listen.port"));
    }
    // zero port number means the caller leaves to debugger to decide at which port to listen
    final Connector.Argument portArg = myConnection.isUseSockets() ? myArguments.get("port") : myArguments.get("name");
    if (portArg != null) {
      portArg.setValue(address);

      // to allow connector to listen on several auto generated addresses
      if (address.isEmpty() || address.equals("0")) {
        EmptyConnectorArgument uniqueArg = new EmptyConnectorArgument("argForUniqueness");
        myArguments.put(uniqueArg.name(), uniqueArg);
      }
    }
    if (Registry.is("debugger.jb.jdi") || Runtime.version().feature() >= 13) {
      setConnectorArgument("localAddress", "*");
    }
    setConnectorArgument("timeout", "0"); // wait forever
    try {
      String listeningAddress = connector.startListening(myArguments);
      String port = StringUtil.substringAfterLast(listeningAddress, ":");
      if (port != null) {
        listeningAddress = port;
      }
      myConnection.setDebuggerAddress(listeningAddress);
      myConnection.setApplicationAddress(listeningAddress);

      myDebugProcessDispatcher.getMulticaster().connectorIsReady();

      return connector.accept(myArguments);
    }
    catch (IllegalArgumentException e) {
      throw new CantRunException(e.getLocalizedMessage());
    }
    finally {
      if (myArguments != null) {
        try {
          connector.stopListening(myArguments);
        }
        catch (IllegalArgumentException | IllegalConnectorArgumentsException ignored) {
          // ignored
        }
      }
    }
  }

  private VirtualMachine attachConnector(AttachingConnector connector)
    throws IOException, IllegalConnectorArgumentsException, CantRunException {
    myDebugProcessDispatcher.getMulticaster().connectorIsReady();
    try {
      return connector.attach(myArguments);
    }
    catch (IllegalArgumentException e) {
      throw new CantRunException(e.getLocalizedMessage());
    }
  }

  public void showStatusText(final @Nls String text) {
    myStatusUpdateAlarm.cancelAllRequests();
    myStatusUpdateAlarm.addRequest(() -> {
      if (!myProject.isDisposed()) {
        StatusBarUtil.setStatusBarInfo(myProject, text);
      }
    }, 50);
  }

  private Connector getConnector() throws ExecutionException {
    if (myConnection instanceof PidRemoteConnection && !((PidRemoteConnection)myConnection).isFixedAddress()) {
      return ((PidRemoteConnection)myConnection).getConnector(this);
    }
    return findConnector(myConnection.isUseSockets(), myConnection.isServerMode());
  }

  @NotNull
  public static Connector findConnector(boolean useSockets, boolean listen) throws ExecutionException {
    String connectorName = (Registry.is("debugger.jb.jdi") ? "com.jetbrains.jdi." : "com.sun.jdi.") +
                           (useSockets ? "Socket" : "SharedMemory") +
                           (listen ? "Listen" : "Attach");
    return findConnector(connectorName);
  }

  @NotNull
  public static Connector findConnector(String connectorName) throws ExecutionException {
    VirtualMachineManager virtualMachineManager;
    if (connectorName.startsWith("com.jetbrains")) {
      virtualMachineManager = VirtualMachineManagerImpl.virtualMachineManager();
    }
    else {
      try {
        virtualMachineManager = Bootstrap.virtualMachineManager();
      }
      catch (Error e) {
        throw new ExecutionException(JavaDebuggerBundle.message("debugger.jdi.bootstrap.error",
                                                                e.getClass().getName() + " : " + e.getLocalizedMessage()));
      }
    }
    return StreamEx.of(virtualMachineManager.allConnectors())
      .findFirst(c -> connectorName.equals(c.name()))
      .orElseThrow(() -> new CantRunException(JavaDebuggerBundle.message("error.debug.connector.not.found", connectorName)));
  }

  protected void checkVirtualMachineVersion(VirtualMachine vm) {
    final String versionString = vm.version();
    if ("1.4.0".equals(versionString)) {
      DebuggerInvocationUtil.swingInvokeLater(myProject, () -> Messages.showMessageDialog(
        myProject,
        JavaDebuggerBundle.message("warning.jdk140.unstable"), JavaDebuggerBundle.message("title.jdk140.unstable"), Messages.getWarningIcon()
      ));
    }
    if (getSession().getAlternativeJre() == null) {
      Sdk runjre = getSession().getRunJre();
      JavaVersion version = JavaVersion.tryParse(versionString);
      if (version != null && (runjre == null || runjre.getSdkType() instanceof JavaSdkType) && !versionMatch(runjre, version)) {
        Arrays.stream(ProjectJdkTable.getInstance().getAllJdks())
          .sorted(Comparator.comparing(sdk -> !(sdk.getSdkType() instanceof JavaSdk))) // prefer regular jdks
          .filter(sdk -> versionMatch(sdk, version))
          .findFirst().ifPresent(sdk -> {
            XDebuggerManagerImpl.getNotificationGroup().createNotification(
              JavaDebuggerBundle.message("message.remote.jre.version.mismatch",
                                         versionString,
                                         runjre != null ? runjre.getVersionString() : "unknown",
                                         sdk.getName())
              , MessageType.INFO).notify(myProject);
            getSession().setAlternativeJre(sdk);
          });
      }
    }
  }

  private static boolean versionMatch(@Nullable Sdk sdk, @NotNull JavaVersion version) {
    if (sdk != null && sdk.getSdkType() instanceof JavaSdkType) {
      JavaVersion v = JavaVersion.tryParse(sdk.getVersionString());
      if (v != null) {
        return version.feature == v.feature && version.minor == v.minor && version.update == v.update;
      }
    }
    return false;
  }

  /*Event dispatching*/
  public void addEvaluationListener(EvaluationListener evaluationListener) {
    myEvaluationDispatcher.addListener(evaluationListener);
  }

  public void removeEvaluationListener(EvaluationListener evaluationListener) {
    myEvaluationDispatcher.removeListener(evaluationListener);
  }


  @Override
  public void addDebugProcessListener(DebugProcessListener listener, Disposable parentDisposable) {
    myDebugProcessDispatcher.addListener(listener, parentDisposable);
  }

  @Override
  public void addDebugProcessListener(DebugProcessListener listener) {
    myDebugProcessDispatcher.addListener(listener);
  }

  @Override
  public void removeDebugProcessListener(DebugProcessListener listener) {
    myDebugProcessDispatcher.removeListener(listener);
  }

  public void addProcessListener(ProcessListener processListener) {
    synchronized (myProcessListeners) {
      if (getProcessHandler() != null) {
        getProcessHandler().addProcessListener(processListener);
      }
      else {
        myProcessListeners.add(processListener);
      }
    }
  }

  public void removeProcessListener(ProcessListener processListener) {
    synchronized (myProcessListeners) {
      if (getProcessHandler() != null) {
        getProcessHandler().removeProcessListener(processListener);
      }
      else {
        myProcessListeners.remove(processListener);
      }
    }
  }

  /* getters */
  public RemoteConnection getConnection() {
    return myConnection;
  }

  @Override
  public ExecutionResult getExecutionResult() {
    return myExecutionResult;
  }

  @NotNull
  @Override
  public Project getProject() {
    return myProject;
  }

  public boolean canRedefineClasses() {
    final VirtualMachineProxyImpl vm = myVirtualMachineProxy;
    return vm != null && vm.canRedefineClasses();
  }

  public boolean isInInitialState() {
    return myState.get() == State.INITIAL;
  }

  @Override
  public boolean isAttached() {
    return myState.get() == State.ATTACHED;
  }

  @Override
  public boolean isDetached() {
    return myState.get() == State.DETACHED;
  }

  @Override
  public boolean isDetaching() {
    return myState.get() == State.DETACHING;
  }

  @Override
  public RequestManagerImpl getRequestsManager() {
    return myRequestManager;
  }

  @NotNull
  @Override
  public VirtualMachineProxyImpl getVirtualMachineProxy() {
    DebuggerManagerThreadImpl.assertIsManagerThread();
    final VirtualMachineProxyImpl vm = myVirtualMachineProxy;
    if (vm == null) {
      if (isInInitialState()) {
        throw new IllegalStateException("Virtual machine is not initialized yet");
      }
      else {
        throw new VMDisconnectedException();
      }
    }
    return vm;
  }

  @Override
  public void appendPositionManager(final PositionManager positionManager) {
    DebuggerManagerThreadImpl.assertIsManagerThread();
    myPositionManager.appendPositionManager(positionManager);
  }

  private volatile SteppingBreakpoint mySteppingBreakpoint;

  public void setSteppingBreakpoint(@Nullable SteppingBreakpoint breakpoint) {
    mySteppingBreakpoint = breakpoint;
  }

  public void cancelRunToCursorBreakpoint() {
    DebuggerManagerThreadImpl.assertIsManagerThread();
    final SteppingBreakpoint runToCursorBreakpoint = mySteppingBreakpoint;
    if (runToCursorBreakpoint != null) {
      setSteppingBreakpoint(null);
      getRequestsManager().deleteRequest(runToCursorBreakpoint);
      if (runToCursorBreakpoint.isRestoreBreakpoints()) {
        DebuggerManagerEx.getInstanceEx(getProject()).getBreakpointManager().enableBreakpoints(this);
      }
    }
  }

  public static void prepareAndSetSteppingBreakpoint(SuspendContextImpl context,
                                                     @NotNull SteppingBreakpoint breakpoint,
                                                     RequestHint hint,
                                                     boolean resetThreadFilter) {
    DebugProcessImpl debugProcess = context.getDebugProcess();
    if (resetThreadFilter) {
      BreakpointManager breakpointManager = DebuggerManagerEx.getInstanceEx(debugProcess.getProject()).getBreakpointManager();
      breakpointManager.applyThreadFilter(debugProcess, null); // clear the filter on resume
    }
    breakpoint.setSuspendPolicy(
      context.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD ? DebuggerSettings.SUSPEND_THREAD : DebuggerSettings.SUSPEND_ALL);
    breakpoint.createRequest(debugProcess);
    breakpoint.setRequestHint(hint);
    debugProcess.setSteppingBreakpoint(breakpoint);
  }

  public void resetIgnoreSteppingFilters(@Nullable Location location, @Nullable RequestHint hint) {
    if (hint != null && hint.isResetIgnoreFilters() && location != null && !isPositionFiltered(location)) {
      getSession().resetIgnoreStepFiltersFlag();
    }
  }

  private void detachProcess(boolean closedByUser, boolean keepManagerThread, Consumer<@Nullable VirtualMachineData> callback) {
    DebuggerManagerThreadImpl.assertIsManagerThread();

    if (myState.compareAndSet(State.INITIAL, State.DETACHING) || myState.compareAndSet(State.ATTACHED, State.DETACHING)) {
      try {
        if (!keepManagerThread) {
          getManagerThread().close();
        }
      }
      finally {
        if (!(myConnection instanceof RemoteConnectionStub)) {
          VirtualMachineData vmData = new VirtualMachineData(myVirtualMachineProxy, myConnection, myDebuggerManagerThread);
          myVirtualMachineProxy = null;
          myPositionManager = CompoundPositionManager.EMPTY;
          myReturnValueWatcher = null;
          myNodeRenderersMap.clear();
          myRenderers.clear();
          DebuggerUtils.cleanupAfterProcessFinish(this);
          myState.compareAndSet(State.DETACHING, State.DETACHED);
          try {
            myDebugProcessDispatcher.getMulticaster().processDetached(this, closedByUser);
          }
          finally {
            callback.accept(vmData);
          }
        }
      }
    }
  }

  protected void closeProcess(boolean closedByUser) {
    detachProcess(closedByUser, false, vmData -> {
      //if (DebuggerSettings.getInstance().UNMUTE_ON_STOP) {
      //  XDebugSession session = mySession.getXDebugSession();
      //  if (session != null) {
      //    session.setBreakpointMuted(false);
      //  }
      //}
      if (vmData != null && vmData.vm != null) {
        try {
          vmData.vm.dispose(); // to be on the safe side ensure that VM mirror, if present, is disposed and invalidated
        }
        catch (Throwable ignored) {
        }
      }
      myWaitFor.up();

      unstashAndReattach();
    });
  }

  @Contract(pure = true)
  private static String formatMessage(String message) {
    final int lineLength = 90;
    StringBuilder buf = new StringBuilder(message.length());
    int index = 0;
    while (index < message.length()) {
      buf.append(message, index, Math.min(index + lineLength, message.length())).append('\n');
      index += lineLength;
    }
    return buf.toString();
  }

  public static @NlsContexts.DialogMessage String processError(Exception e) {
    String message;

    if (e instanceof VMStartException) {
      VMStartException e1 = (VMStartException)e;
      message = e1.getLocalizedMessage();
    }
    else if (e instanceof IllegalConnectorArgumentsException) {
      IllegalConnectorArgumentsException e1 = (IllegalConnectorArgumentsException)e;
      final List<String> invalidArgumentNames = e1.argumentNames();
      message = formatMessage(
        JavaDebuggerBundle.message("error.invalid.argument", invalidArgumentNames.size()) + ": " + e1.getLocalizedMessage()) + invalidArgumentNames;
      LOG.debug(e1);
    }
    else if (e instanceof CantRunException) {
      message = e.getLocalizedMessage();
    }
    else if (e instanceof VMDisconnectedException) {
      message = JavaDebuggerBundle.message("error.vm.disconnected");
    }
    else if (e instanceof IOException) {
      message = processIOException((IOException)e, null);
    }
    else if (e instanceof ExecutionException) {
      message = e.getLocalizedMessage();
    }
    else {
      message = JavaDebuggerBundle.message("error.exception.while.connecting", e.getClass().getName(), e.getLocalizedMessage());
      LOG.debug(e);
    }
    return message;
  }

  @NotNull
  public static @NlsContexts.DialogMessage String processIOException(@NotNull IOException e, @Nullable String address) {
    if (e instanceof UnknownHostException) {
      if (address != null) {
        return JavaDebuggerBundle.message("error.unknown.host.with.address", address) + ":\n" + e.getLocalizedMessage();
      }
      return JavaDebuggerBundle.message("error.unknown.host") + ":\n" + e.getLocalizedMessage();
    }

    // Failed SA attach
    Throwable cause = e.getCause();
    if (cause instanceof InvocationTargetException) {
      if (cause.getCause() != null) {
        return cause.getCause().getLocalizedMessage();
      }
    }

    @Nls StringBuilder buf = new StringBuilder();
    if (address != null) {
      buf.append(JavaDebuggerBundle.message("error.cannot.open.debugger.port"));
      buf.append(" (").append(address).append("): ");
    }
    buf.append(e.getClass().getName()).append(" ");
    if (!StringUtil.isEmpty(e.getLocalizedMessage())) {
      buf.append('"').append(e.getLocalizedMessage()).append('"');
    }
    if (cause != null && !StringUtil.isEmpty(cause.getLocalizedMessage())) {
      buf.append(" (").append(cause.getLocalizedMessage()).append(')');
    }
    LOG.debug(e);
    return buf.toString();
  }

  public void dispose() {
    Disposer.dispose(myDisposable);
    myRequestManager.setFilterThread(null);
  }

  @Override
  public @NotNull DebuggerManagerThreadImpl getManagerThread() {
    return myDebuggerManagerThread;
  }

  public boolean isCurrentVirtualMachine(@NotNull VirtualMachineProxyImpl vmProxy) {
    VirtualMachine vm = ObjectUtils.doIfNotNull(myVirtualMachineProxy, it -> it.getVirtualMachine());
    return vmProxy.getVirtualMachine() == vm;
  }

  private static int getInvokePolicy(SuspendContext suspendContext) {
    if (suspendContext.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD ||
        isResumeOnlyCurrentThread() ||
        ThreadBlockedMonitor.getSingleThreadedEvaluationThreshold() > 0) {
      return ObjectReference.INVOKE_SINGLE_THREADED;
    }
    return 0;
  }

  @Override
  public void waitFor() {
    LOG.assertTrue(!DebuggerManagerThreadImpl.isManagerThread());
    myWaitFor.waitFor();
  }

  @Override
  public void waitFor(long timeout) {
    LOG.assertTrue(!DebuggerManagerThreadImpl.isManagerThread());
    myWaitFor.waitFor(timeout);
  }

  private abstract class InvokeCommand<E extends Value> {
    private final Method myMethod;
    private final List<Value> myArgs;

    protected InvokeCommand(@NotNull Method method, @NotNull List<? extends Value> args) {
      myMethod = method;
      myArgs = new ArrayList<>(args);
    }

    public String toString() {
      return "INVOKE: " + super.toString();
    }

    protected abstract E invokeMethod(int invokePolicy, Method method, List<? extends Value> args) throws InvocationException,
                                                                                ClassNotLoadedException,
                                                                                IncompatibleThreadStateException,
                                                                                InvalidTypeException;


    E start(EvaluationContextImpl evaluationContext, boolean internalEvaluate) throws EvaluateException {
      while (true) {
        try {
          return startInternal(evaluationContext, internalEvaluate);
        }
        catch (ClassNotLoadedException e) {
          ReferenceType loadedClass = null;
          try {
            if (evaluationContext.isAutoLoadClasses()) {
              loadedClass = loadClass(evaluationContext, e.className(), evaluationContext.getClassLoader());
            }
          }
          catch (Exception ignored) {
            loadedClass = null;
          }
          if (loadedClass == null) {
            throw EvaluateExceptionUtil.createEvaluateException(e);
          }
        }
      }
    }

    E startInternal(EvaluationContextImpl evaluationContext, boolean internalEvaluate)
      throws EvaluateException, ClassNotLoadedException {
      DebuggerManagerThreadImpl.assertIsManagerThread();
      SuspendContextImpl suspendContext = evaluationContext.getSuspendContext();
      SuspendManagerUtil.assertSuspendContext(suspendContext);

      ThreadReferenceProxyImpl invokeThread = suspendContext.getThread();

      if (SuspendManagerUtil.isEvaluating(getSuspendManager(), invokeThread)) {
        throw EvaluateExceptionUtil.NESTED_EVALUATION_ERROR;
      }

      if (!suspendContext.suspends(invokeThread)) {
        throw EvaluateExceptionUtil.THREAD_WAS_RESUMED;
      }

      Set<SuspendContextImpl> suspendingContexts = SuspendManagerUtil.getSuspendingContexts(getSuspendManager(), invokeThread);
      final ThreadReference invokeThreadRef = invokeThread.getThreadReference();

      myEvaluationDispatcher.getMulticaster().evaluationStarted(suspendContext);
      beforeMethodInvocation(suspendContext, myMethod, internalEvaluate);

      Object resumeData = null;
      try {
        for (SuspendContextImpl suspendingContext : suspendingContexts) {
          final ThreadReferenceProxyImpl suspendContextThread = suspendingContext.getThread();
          if (suspendContextThread != invokeThread) {
            if (LOG.isDebugEnabled()) {
              LOG.debug("Resuming " + invokeThread + " that is paused by " + suspendContextThread);
            }
            LOG.assertTrue(suspendContextThread == null || !invokeThreadRef.equals(suspendContextThread.getThreadReference()));
            getSuspendManager().resumeThread(suspendingContext, invokeThread);
          }
        }

        resumeData = SuspendManagerUtil.prepareForResume(suspendContext);
        suspendContext.setIsEvaluating(evaluationContext);

        getVirtualMachineProxy().clearCaches();

        return invokeMethodAndFork(suspendContext);
      }
      catch (InvocationException | InternalException | UnsupportedOperationException | ObjectCollectedException |
             InvalidTypeException | IncompatibleThreadStateException e) {
        throw EvaluateExceptionUtil.createEvaluateException(e);
      }
      finally {
        suspendContext.setIsEvaluating(null);
        if (resumeData != null) {
          SuspendManagerUtil.restoreAfterResume(suspendContext, resumeData);
        }
        for (SuspendContextImpl suspendingContext : mySuspendManager.getEventContexts()) {
          if (suspendingContexts.contains(suspendingContext) &&
              !suspendingContext.isEvaluating() &&
              !suspendingContext.suspends(invokeThread)) {
            mySuspendManager.suspendThread(suspendingContext, invokeThread);
          }
        }

        LOG.debug("getVirtualMachine().clearCaches()");
        getVirtualMachineProxy().clearCaches();
        afterMethodInvocation(suspendContext, internalEvaluate);

        myEvaluationDispatcher.getMulticaster().evaluationFinished(suspendContext);
      }
    }

    private E invokeMethodAndFork(final SuspendContextImpl context) throws InvocationException,
                                                                           ClassNotLoadedException,
                                                                           IncompatibleThreadStateException,
                                                                           InvalidTypeException {
      Ref<Exception> exception = Ref.create();
      Ref<E> result = Ref.create();
      getManagerThread().startLongProcessAndFork(() -> {
        ThreadReferenceProxyImpl thread = context.getThread();
        ThreadBlockedMonitor.InvocationWatcher invocationWatcher = null;
        try {
          try {
            if (LOG.isDebugEnabled()) {
              final VirtualMachineProxyImpl virtualMachineProxy = getVirtualMachineProxy();
              virtualMachineProxy.logThreads();
              LOG.debug("Invoke in " + thread.name());
              assertThreadSuspended(thread, context);
            }

            if (myMethod.isVarArgs()) {
              // See IDEA-63581
              // if vararg parameter array is of interface type and Object[] is expected, JDI wrap it into another array,
              // in this case we have to unroll the array manually and pass its elements to the method instead of array object
              int lastIndex = myMethod.argumentTypeNames().size() - 1;
              if (lastIndex >= 0 && myArgs.size() > lastIndex) { // at least one varargs param
                Object firstVararg = myArgs.get(lastIndex);
                if (myArgs.size() == lastIndex + 1) { // only one vararg param
                  if (firstVararg instanceof ArrayReference arrayRef) {
                    if (((ArrayType)arrayRef.referenceType()).componentType() instanceof InterfaceType) {
                      List<String> argTypes = myMethod.argumentTypeNames();
                      if (argTypes.size() > lastIndex && argTypes.get(lastIndex).startsWith(CommonClassNames.JAVA_LANG_OBJECT)) {
                        // unwrap array of interfaces for vararg param
                        myArgs.remove(lastIndex);
                        myArgs.addAll(arrayRef.getValues());
                      }
                    }
                  }
                }
              }
            }

            if (!Patches.IBM_JDK_DISABLE_COLLECTION_BUG) {
              // ensure args are not collected
              StreamEx.of(myArgs).select(ObjectReference.class).forEach(DebuggerUtilsEx::disableCollection);
            }

            if (Patches.JDK_BUG_ID_21275177 && (ourTraceMask & VirtualMachine.TRACE_SENDS) != 0) {
              //noinspection ResultOfMethodCallIgnored
              myArgs.forEach(Object::toString);
            }

            // workaround for jdi hang in trace mode, see IDEA-183387
            if (Patches.JDK_BUG_WITH_TRACE_SEND && (getTraceMask() & VirtualMachine.TRACE_SENDS) != 0) {
              StreamEx.of(myArgs).findAny(ThreadReference.class::isInstance).ifPresent(t -> {
                //noinspection UseOfSystemOutOrSystemErr
                System.err.println("[JDI: workaround for invocation of " + myMethod + "]");
                myMethod.virtualMachine().setDebugTraceMode(getTraceMask() & ~VirtualMachine.TRACE_SENDS);
              });
            }

            int invokePolicy = getInvokePolicy(context);
            invocationWatcher = myThreadBlockedMonitor.startInvokeWatching(invokePolicy, thread, context);

            result.set(invokeMethod(invokePolicy, myMethod, myArgs));
          }
          finally {
            if (invocationWatcher != null) {
              invocationWatcher.invocationFinished();
            }
            if (Patches.JDK_BUG_WITH_TRACE_SEND && (getTraceMask() & VirtualMachine.TRACE_SENDS) != 0) {
              myMethod.virtualMachine().setDebugTraceMode(getTraceMask());
            }
            //  assertThreadSuspended(thread, context);
            if (!Patches.IBM_JDK_DISABLE_COLLECTION_BUG) {
              // ensure args are not collected
              StreamEx.of(myArgs).select(ObjectReference.class).forEach(DebuggerUtilsEx::enableCollection);
            }
          }
        }
        catch (Exception e) {
          exception.set(e);
        }
      });

      Exception ex = exception.get();
      if (ex != null) {
        if (ex instanceof InvocationException) {
          throw (InvocationException)ex;
        }
        else if (ex instanceof ClassNotLoadedException) {
          throw (ClassNotLoadedException)ex;
        }
        else if (ex instanceof IncompatibleThreadStateException) {
          throw (IncompatibleThreadStateException)ex;
        }
        else if (ex instanceof InvalidTypeException) {
          throw (InvalidTypeException)ex;
        }
        else if (ex instanceof RuntimeException) {
          throw (RuntimeException)ex;
        }
        else {
          LOG.error("Unexpected exception", new Throwable(ex));
        }
      }

      return result.get();
    }

    private void assertThreadSuspended(final ThreadReferenceProxyImpl thread, final SuspendContextImpl context) {
      LOG.assertTrue(context.isEvaluating());
      try {
        final boolean isSuspended = thread.isSuspended();
        LOG.assertTrue(isSuspended, thread);
      }
      catch (ObjectCollectedException ignored) {
      }
    }
  }

  @Override
  public Value invokeMethod(@NotNull EvaluationContext evaluationContext,
                            @NotNull ObjectReference objRef,
                            @NotNull Method method,
                            @NotNull List<? extends Value> args) throws EvaluateException {
    return invokeInstanceMethod(evaluationContext, objRef, method, args, 0);
  }

  @Override
  public Value invokeInstanceMethod(@NotNull EvaluationContext evaluationContext,
                                    @NotNull final ObjectReference objRef,
                                    @NotNull Method method,
                                    @NotNull List<? extends Value> args,
                                    final int invocationOptions) throws EvaluateException {
    return invokeInstanceMethod(evaluationContext, objRef, method, args, invocationOptions, false);
  }

  public Value invokeInstanceMethod(@NotNull EvaluationContext evaluationContext,
                                    @NotNull final ObjectReference objRef,
                                    @NotNull Method method,
                                    @NotNull List<? extends Value> args,
                                    final int invocationOptions,
                                    boolean internalEvaluate) throws EvaluateException {
    final ThreadReference thread = getEvaluationThread(evaluationContext);
    return new InvokeCommand<>(method, args) {
      @Override
      protected Value invokeMethod(int invokePolicy, Method method, List<? extends Value> args)
        throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Invoking " + objRef.type().name() + "." + method.name());
        }
        return objRef.invokeMethod(thread, method, args, invokePolicy | invocationOptions);
      }
    }.start((EvaluationContextImpl)evaluationContext, internalEvaluate);
  }

  private static ThreadReference getEvaluationThread(final EvaluationContext evaluationContext) throws EvaluateException {
    ThreadReferenceProxy evaluationThread = evaluationContext.getSuspendContext().getThread();
    if (evaluationThread == null) {
      throw EvaluateExceptionUtil.NULL_STACK_FRAME;
    }
    return evaluationThread.getThreadReference();
  }

  @Override
  public Value invokeMethod(EvaluationContext evaluationContext,
                            ClassType classType,
                            Method method,
                            List<? extends Value> args) throws EvaluateException {
    return invokeMethod(evaluationContext, classType, method, args, 0, false);
  }

  public Value invokeMethod(@NotNull EvaluationContext evaluationContext,
                            @NotNull final ClassType classType,
                            @NotNull Method method,
                            @NotNull List<? extends Value> args,
                            boolean internalEvaluate) throws EvaluateException {
    return invokeMethod(evaluationContext, classType, method, args, 0, internalEvaluate);
  }

  public Value invokeMethod(@NotNull EvaluationContext evaluationContext,
                            @NotNull final ClassType classType,
                            @NotNull Method method,
                            @NotNull List<? extends Value> args,
                            int extraInvocationOptions,
                            boolean internalEvaluate) throws EvaluateException {
    final ThreadReference thread = getEvaluationThread(evaluationContext);
    return new InvokeCommand<>(method, args) {
      @Override
      protected Value invokeMethod(int invokePolicy, Method method, List<? extends Value> args) throws InvocationException,
                                                                                                       ClassNotLoadedException,
                                                                                                       IncompatibleThreadStateException,
                                                                                                       InvalidTypeException {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Invoking " + classType.name() + "." + method.name());
        }
        return classType.invokeMethod(thread, method, args, invokePolicy | extraInvocationOptions);
      }
    }.start((EvaluationContextImpl)evaluationContext, internalEvaluate);
  }

  public Value invokeMethod(EvaluationContext evaluationContext,
                            InterfaceType interfaceType,
                            Method method,
                            List<? extends Value> args) throws EvaluateException {
    final ThreadReference thread = getEvaluationThread(evaluationContext);
    return new InvokeCommand<>(method, args) {
      @Override
      protected Value invokeMethod(int invokePolicy, Method method, List<? extends Value> args) throws InvocationException,
                                                                                                       ClassNotLoadedException,
                                                                                                       IncompatibleThreadStateException,
                                                                                                       InvalidTypeException {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Invoking " + interfaceType.name() + "." + method.name());
        }

        try {
          return interfaceType.invokeMethod(thread, method, args, invokePolicy);
        }
        catch (LinkageError e) {
          throw new IllegalStateException("Interface method invocation is not supported in JVM " +
                                          SystemInfo.JAVA_VERSION +
                                          ". Use JVM 1.8.0_45 or higher to run " +
                                          ApplicationNamesInfo.getInstance().getFullProductName());
        }
      }
    }.start((EvaluationContextImpl)evaluationContext, false);
  }


  @Override
  public ArrayReference newInstance(final ArrayType arrayType,
                                    final int dimension)
    throws EvaluateException {
    try {
      return arrayType.newInstance(dimension);
    }
    catch (Exception e) {
      throw EvaluateExceptionUtil.createEvaluateException(e);
    }
  }

  @Override
  public ObjectReference newInstance(@NotNull final EvaluationContext evaluationContext,
                                     @NotNull final ClassType classType,
                                     @NotNull Method method,
                                     @NotNull List<? extends Value> args) throws EvaluateException {
    return newInstance(evaluationContext, classType, method, args, 0, false);
  }

  public ObjectReference newInstance(@NotNull final EvaluationContext evaluationContext,
                                     @NotNull final ClassType classType,
                                     @NotNull Method method,
                                     @NotNull List<? extends Value> args,
                                     final int invocationOptions,
                                     boolean internalEvaluate) throws EvaluateException {
    final ThreadReference thread = getEvaluationThread(evaluationContext);
    InvokeCommand<ObjectReference> invokeCommand = new InvokeCommand<>(method, args) {
      @Override
      protected ObjectReference invokeMethod(int invokePolicy, Method method, List<? extends Value> args)
        throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException {
        if (LOG.isDebugEnabled()) {
          LOG.debug("New instance " + classType.name() + "." + method.name());
        }
        return classType.newInstance(thread, method, args, invokePolicy | invocationOptions);
      }
    };
    return invokeCommand.start((EvaluationContextImpl)evaluationContext, internalEvaluate);
  }

  public void clearCashes(@MagicConstant(flagsFromClass = EventRequest.class) int suspendPolicy) {
    if (!isAttached()) return;
    switch (suspendPolicy) {
      case EventRequest.SUSPEND_ALL, EventRequest.SUSPEND_EVENT_THREAD -> getVirtualMachineProxy().clearCaches();
    }
  }

  protected void beforeSuspend(SuspendContextImpl suspendContext) {
    clearCashes(suspendContext.getSuspendPolicy());
  }

  private void beforeMethodInvocation(SuspendContextImpl suspendContext, Method method, boolean internalEvaluate) {
    if (LOG.isDebugEnabled()) {
      LOG.debug(
        "before invocation in  thread " + suspendContext.getThread().name() + " method " + (method == null ? "null" : method.name()));
    }

    if (!internalEvaluate) {
      if (method != null) {
        showStatusText(JavaDebuggerBundle.message("progress.evaluating", DebuggerUtilsEx.methodName(method)));
      }
      else {
        showStatusText(JavaDebuggerBundle.message("title.evaluating"));
      }
    }
  }

  private void afterMethodInvocation(SuspendContextImpl suspendContext, boolean internalEvaluate) {
    if (LOG.isDebugEnabled()) {
      LOG.debug("after invocation in  thread " + suspendContext.getThread().name());
    }
    if (!internalEvaluate) {
      showStatusText("");
    }
  }

  @Override
  public ReferenceType findClass(@Nullable EvaluationContext evaluationContext, String className,
                                 ClassLoaderReference classLoader) throws EvaluateException {
    try {
      DebuggerManagerThreadImpl.assertIsManagerThread();
      ReferenceType result;
      List<ReferenceType> types = ContainerUtil.filter(getVirtualMachineProxy().classesByName(className), ReferenceType::isPrepared);
      // first try to quickly find the equal classloader only
      result = ContainerUtil.find(types, refType -> Objects.equals(classLoader, refType.classLoader()));
      // now do the full visibility check
      if (result == null && classLoader != null) {
        result = ContainerUtil.find(types, refType -> isVisibleFromClassLoader(classLoader, refType));
      }
      if (result == null && evaluationContext != null) {
        EvaluationContextImpl evalContext = (EvaluationContextImpl)evaluationContext;
        if (evalContext.isAutoLoadClasses()) {
          return loadClass(evalContext, className, classLoader);
        }
      }
      return result;
    }
    catch (InvocationException | InvalidTypeException | IncompatibleThreadStateException | ClassNotLoadedException e) {
      throw EvaluateExceptionUtil.createEvaluateException(e);
    }
  }

  private static boolean isVisibleFromClassLoader(@NotNull ClassLoaderReference fromLoader, final ReferenceType refType) {
    // IMPORTANT! Even if the refType is already loaded by some parent or bootstrap loader, it may not be visible from the given loader.
    // For example because there were no accesses yet from this loader to this class. So the loader is not in the list of "initialing" loaders
    // for this refType and the refType is not visible to the loader.
    // Attempt to evaluate method with this refType will yield ClassNotLoadedException.
    // The only way to say for sure whether the class is _visible_ to the given loader, is to use the following API call
    if (fromLoader instanceof ClassLoaderReferenceImpl) {
      return ((ClassLoaderReferenceImpl)fromLoader).isVisible(refType);
    }
    return fromLoader.visibleClasses().contains(refType);
  }

  private static String reformatArrayName(String className) {
    if (className.indexOf('[') == -1) return className;

    int dims = 0;
    while (className.endsWith("[]")) {
      className = className.substring(0, className.length() - 2);
      dims++;
    }

    StringBuilder buffer = new StringBuilder();
    StringUtil.repeatSymbol(buffer, '[', dims);
    String primitiveSignature = JVMNameUtil.getPrimitiveSignature(className);
    if (primitiveSignature != null) {
      buffer.append(primitiveSignature);
    }
    else {
      buffer.append('L');
      buffer.append(className);
      buffer.append(';');
    }
    return buffer.toString();
  }

  @SuppressWarnings({"SpellCheckingInspection"})
  public ReferenceType loadClass(EvaluationContextImpl evaluationContext, String qName, ClassLoaderReference classLoader)
    throws InvocationException, ClassNotLoadedException, IncompatibleThreadStateException, InvalidTypeException, EvaluateException {

    DebuggerManagerThreadImpl.assertIsManagerThread();
    qName = reformatArrayName(qName);
    VirtualMachineProxyImpl virtualMachine = getVirtualMachineProxy();
    ClassType classClassType = (ClassType)ContainerUtil.getFirstItem(virtualMachine.classesByName(CommonClassNames.JAVA_LANG_CLASS));
    if (classClassType == null) {
      LOG.error("Unable to find loaded class " + CommonClassNames.JAVA_LANG_CLASS);
      return null;
    }
    final Method forNameMethod;
    List<Value> args = new ArrayList<>(); // do not use unmodifiable lists because the list is modified by JPDA
    args.add(virtualMachine.mirrorOf(qName));
    if (classLoader != null) {
      //forNameMethod = classClassType.concreteMethodByName("forName", "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;");
      forNameMethod = DebuggerUtils.findMethod(classClassType, "forName", "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;");
      args.add(virtualMachine.mirrorOf(true));
      args.add(classLoader);
    }
    else {
      //forNameMethod = classClassType.concreteMethodByName("forName", "(Ljava/lang/String;)Ljava/lang/Class;");
      forNameMethod = DebuggerUtils.findMethod(classClassType, "forName", "(Ljava/lang/String;)Ljava/lang/Class;");
    }
    if (forNameMethod == null) {
      LOG.error("Unable to find forName method in " + classClassType);
      return null;
    }
    Value classReference = invokeMethod(evaluationContext, classClassType, forNameMethod, args, MethodImpl.SKIP_ASSIGNABLE_CHECK, true);
    if (classReference instanceof ClassObjectReference) {
      ReferenceType refType = ((ClassObjectReference)classReference).reflectedType();
      if (classLoader instanceof ClassLoaderReferenceImpl) {
        ((ClassLoaderReferenceImpl)classLoader).addVisible(refType);
      }
      return refType;
    }
    return null;
  }

  public void logThreads() {
    if (LOG.isDebugEnabled()) {
      try {
        Collection<ThreadReferenceProxyImpl> allThreads = getVirtualMachineProxy().allThreads();
        for (ThreadReferenceProxyImpl threadReferenceProxy : allThreads) {
          LOG.debug("Thread name=" + threadReferenceProxy.name() + " suspendCount()=" + threadReferenceProxy.getSuspendCount());
        }
      }
      catch (Exception e) {
        LOG.debug(e);
      }
    }
  }

  public void onHotSwapFinished() {
    getPositionManager().clearCache();
    StackCapturingLineBreakpoint.clearCaches(this);
  }

  @NotNull
  public SuspendManager getSuspendManager() {
    return mySuspendManager;
  }

  @NotNull
  @Override
  public CompoundPositionManager getPositionManager() {
    return myPositionManager;
  }
  //ManagerCommands

  @Override
  public void stop(boolean forceTerminate) {
    myIsStopped.set(true);
    stopConnecting(); // does this first place in case debugger manager hanged accepting debugger connection (forever)
    getManagerThread().terminateAndInvoke(createStopCommand(forceTerminate), ApplicationManager.getApplication().isUnitTestMode() ? 0 : DebuggerManagerThreadImpl.COMMAND_TIMEOUT);
  }

  @NotNull
  public StopCommand createStopCommand(boolean forceTerminate) {
    return new StopCommand(forceTerminate);
  }

  protected class StopCommand extends DebuggerCommandImpl {
    private final boolean myIsTerminateTargetVM;

    public StopCommand(boolean isTerminateTargetVM) {
      myIsTerminateTargetVM = isTerminateTargetVM;
    }

    @Override
    public Priority getPriority() {
      return Priority.HIGH;
    }

    @Override
    protected void action() {
      if (isAttached()) {
        VirtualMachineProxyImpl virtualMachineProxy = getVirtualMachineProxy();

        if (!virtualMachineProxy.canBeModified()) {
          closeProcess(false);
          return;
        }

        if (myIsTerminateTargetVM) {
          virtualMachineProxy.exit(-1);
        }
        else {
          // some VMs (like IBM VM 1.4.2 bundled with WebSphere) does not resume threads on dispose() like it should
          try {
            virtualMachineProxy.resume();
          }
          finally {
            virtualMachineProxy.dispose();
          }
        }
      }
      else {
        try {
          stopConnecting();
        }
        finally {
          closeProcess(true);
        }
      }
    }
  }

  public class StepOutCommand extends StepCommand {
    private final int myStepSize;

    public StepOutCommand(SuspendContextImpl suspendContext, int stepSize) {
      super(suspendContext, null);
      myStepSize = stepSize;
    }

    @Override
    public void contextAction(@NotNull SuspendContextImpl suspendContext) {
      showStatusText(JavaDebuggerBundle.message("status.step.out"));
      final ThreadReferenceProxyImpl thread = getContextThread();
      RequestHint hint = getHint(suspendContext, thread, null);
      applyThreadFilter(thread);
      startWatchingMethodReturn(thread);
      step(suspendContext, thread, hint);
      super.contextAction(suspendContext);
    }

    @Override
    @NotNull
    public RequestHint getHint(SuspendContextImpl suspendContext, ThreadReferenceProxyImpl thread, @Nullable RequestHint parentHint) {
      RequestHint hint = new RequestHint(thread, suspendContext, StepRequest.STEP_LINE, StepRequest.STEP_OUT, null, parentHint);
      hint.setIgnoreFilters(mySession.shouldIgnoreSteppingFilters());
      return hint;
    }

    @Override
    public void step(SuspendContextImpl suspendContext, ThreadReferenceProxyImpl stepThread, RequestHint hint) {
      doStep(suspendContext, stepThread, myStepSize, StepRequest.STEP_OUT, hint);
    }
  }

  public class StepIntoCommand extends StepCommand {
    private final boolean myForcedIgnoreFilters;
    @Nullable
    private final StepIntoBreakpoint myBreakpoint;
    private final int myStepSize;

    public StepIntoCommand(SuspendContextImpl suspendContext, boolean ignoreFilters, @Nullable final MethodFilter methodFilter,
                           int stepSize) {
      super(suspendContext, methodFilter);
      myForcedIgnoreFilters = ignoreFilters || methodFilter != null;
      myBreakpoint = methodFilter instanceof BreakpointStepMethodFilter ?
        DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().addStepIntoBreakpoint(((BreakpointStepMethodFilter)methodFilter)) :
        null;
      myStepSize = stepSize;
    }

    @Override
    public void contextAction(@NotNull SuspendContextImpl suspendContext) {
      showStatusText(JavaDebuggerBundle.message("status.step.into"));
      final ThreadReferenceProxyImpl stepThread = getContextThread();
      final RequestHint hint = getHint(suspendContext, stepThread, null);
      if (myForcedIgnoreFilters) {
        mySession.setIgnoreStepFiltersFlag(getFrameCount(stepThread, suspendContext));
      }
      hint.setIgnoreFilters(myForcedIgnoreFilters || mySession.shouldIgnoreSteppingFilters());
      applyThreadFilter(stepThread);
      if (myBreakpoint != null) {
        prepareAndSetSteppingBreakpoint(suspendContext, myBreakpoint, hint, false);
      }
      step(suspendContext, stepThread, hint);
      super.contextAction(suspendContext);
    }

    @Override
    @NotNull
    public RequestHint getHint(SuspendContextImpl suspendContext, ThreadReferenceProxyImpl stepThread, @Nullable RequestHint parentHint) {
      final RequestHint hint = new RequestHint(stepThread, suspendContext, StepRequest.STEP_LINE, StepRequest.STEP_INTO, myMethodFilter, parentHint);
      hint.setResetIgnoreFilters(myMethodFilter != null && !mySession.shouldIgnoreSteppingFilters());
      return hint;
    }

    @Override
    public void step(SuspendContextImpl suspendContext, ThreadReferenceProxyImpl stepThread, RequestHint hint) {
      doStep(suspendContext, stepThread, myStepSize, StepRequest.STEP_INTO, hint);
    }
  }

  public class StepOverCommand extends StepCommand {
    private final boolean myIsIgnoreBreakpoints;
    private final int myStepSize;

    public StepOverCommand(SuspendContextImpl suspendContext,
                           boolean ignoreBreakpoints,
                           @Nullable MethodFilter methodFilter,
                           int stepSize) {
      super(suspendContext, methodFilter);
      myIsIgnoreBreakpoints = ignoreBreakpoints;
      myStepSize = stepSize;
    }

    public StepOverCommand(SuspendContextImpl suspendContext, boolean ignoreBreakpoints, int stepSize) {
      this(suspendContext, ignoreBreakpoints, null, stepSize);
    }

    protected int getStepDepth() {
      return StepRequest.STEP_OVER;
    }

    @NotNull
    protected @Nls String getStatusText() {
      return JavaDebuggerBundle.message("status.step.over");
    }

    @Override
    @NotNull
    public RequestHint getHint(SuspendContextImpl suspendContext, ThreadReferenceProxyImpl stepThread, @Nullable RequestHint parentHint) {
      // need this hint while stepping over for JSR45 support:
      // several lines of generated java code may correspond to a single line in the source file,
      // from which the java code was generated
      RequestHint hint = new RequestHint(stepThread, suspendContext, StepRequest.STEP_LINE, StepRequest.STEP_OVER, myMethodFilter, parentHint);
      hint.setRestoreBreakpoints(myIsIgnoreBreakpoints);
      hint.setIgnoreFilters(myIsIgnoreBreakpoints || mySession.shouldIgnoreSteppingFilters());
      return hint;
    }

    @Override
    public void contextAction(@NotNull SuspendContextImpl suspendContext) {
      showStatusText(getStatusText());

      ThreadReferenceProxyImpl stepThread = getContextThread();

      RequestHint hint = getHint(suspendContext, stepThread, null);

      applyThreadFilter(stepThread);

      startWatchingMethodReturn(stepThread);

      step(suspendContext, stepThread, hint);

      if (myIsIgnoreBreakpoints) {
        DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().disableBreakpoints(DebugProcessImpl.this);
      }
      super.contextAction(suspendContext);
    }

    @Override
    public void step(SuspendContextImpl suspendContext, ThreadReferenceProxyImpl stepThread, RequestHint hint) {
      doStep(suspendContext, stepThread, myStepSize, getStepDepth(), hint);
    }
  }

  private final class RunToCursorCommand extends StepCommand {
    private final RunToCursorBreakpoint myRunToCursorBreakpoint;
    private final boolean myIgnoreBreakpoints;

    private RunToCursorCommand(SuspendContextImpl suspendContext, @NotNull XSourcePosition position, final boolean ignoreBreakpoints) {
      super(suspendContext, null);
      myIgnoreBreakpoints = ignoreBreakpoints;
      myRunToCursorBreakpoint =
        DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().addRunToCursorBreakpoint(position, ignoreBreakpoints);
    }

    @Override
    public void contextAction(@NotNull SuspendContextImpl context) {
      showStatusText(JavaDebuggerBundle.message("status.run.to.cursor"));
      cancelRunToCursorBreakpoint();
      if (myRunToCursorBreakpoint == null) {
        return;
      }
      if (myIgnoreBreakpoints) {
        DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().disableBreakpoints(DebugProcessImpl.this);
      }
      applyThreadFilter(getContextThread());
      prepareAndSetSteppingBreakpoint(context, myRunToCursorBreakpoint, null, false);
      final DebugProcessImpl debugProcess = context.getDebugProcess();

      if (debugProcess.getRequestsManager().getWarning(myRunToCursorBreakpoint) == null) {
        super.contextAction(context);
      }
      else {
        DebuggerInvocationUtil.swingInvokeLater(myProject, () -> {
          Messages.showErrorDialog(
            JavaDebuggerBundle.message("error.running.to.cursor.no.executable.code",
                                       myRunToCursorBreakpoint.getSourcePosition().getFile().getName(),
                                       myRunToCursorBreakpoint.getLineIndex() + 1),
            UIUtil.removeMnemonic(ActionsBundle.actionText(XDebuggerActions.RUN_TO_CURSOR)));
          DebuggerSession session = debugProcess.getSession();
          session.getContextManager().setState(DebuggerContextUtil.createDebuggerContext(session, context),
                                               DebuggerSession.State.PAUSED, DebuggerSession.Event.CONTEXT, null);
        });
      }
    }
  }

  public abstract class StepCommand extends ResumeCommand {
    @Nullable
    protected final MethodFilter myMethodFilter;

    StepCommand(SuspendContextImpl suspendContext, @Nullable MethodFilter filter) {
      super(suspendContext);
      myMethodFilter = filter;
    }

    @Override
    protected void resumeAction() {
      SuspendContextImpl context = getSuspendContext();
      if (context != null &&
          (context.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD || isResumeOnlyCurrentThread())) {
        myThreadBlockedMonitor.startWatching(myContextThread);
      }
      if (context != null
          && isResumeOnlyCurrentThread()
          && context.getSuspendPolicy() == EventRequest.SUSPEND_ALL
          && myContextThread != null) {
        getSuspendManager().resumeThread(context, myContextThread);
      }
      else {
        super.resumeAction();
      }
    }

    public void step(SuspendContextImpl suspendContext, ThreadReferenceProxyImpl stepThread, RequestHint hint) {
    }

    @Nullable
    public RequestHint getHint(SuspendContextImpl suspendContext, ThreadReferenceProxyImpl stepThread, @Nullable RequestHint parentHint) {
      return null;
    }
  }

  public abstract class ResumeCommand extends SuspendContextCommandImpl {
    @Nullable protected final ThreadReferenceProxyImpl myContextThread;

    public ResumeCommand(SuspendContextImpl suspendContext) {
      super(suspendContext);
      final ThreadReferenceProxyImpl thread = suspendContext != null ? suspendContext.getThread() : null;
      myContextThread = thread != null ? thread : getDebuggerContext().getThreadProxy();
    }

    @Override
    public Priority getPriority() {
      return Priority.HIGH;
    }

    @Override
    public void contextAction(@NotNull SuspendContextImpl suspendContext) {
      showStatusText(JavaDebuggerBundle.message("status.process.resumed"));
      resumeAction();
      myDebugProcessDispatcher.getMulticaster().resumed(suspendContext);
    }

    protected void resumeAction() {
      getSuspendManager().resume(getSuspendContext());
    }

    @Nullable
    public ThreadReferenceProxyImpl getContextThread() {
      return myContextThread;
    }

    protected void applyThreadFilter(ThreadReferenceProxy thread) {
      if (getSuspendContext().getSuspendPolicy() == EventRequest.SUSPEND_ALL) {
        // there could be explicit resume as a result of call to voteSuspend()
        // e.g. when breakpoint was considered invalid, in that case the filter will be applied _after_
        // resuming and all breakpoints in other threads will be ignored.
        // As resume() implicitly cleares the filter, the filter must be always applied _before_ any resume() action happens
        final BreakpointManager breakpointManager = DebuggerManagerEx.getInstanceEx(getProject()).getBreakpointManager();
        breakpointManager.applyThreadFilter(DebugProcessImpl.this, thread.getThreadReference());
      }
    }
  }

  private class PauseCommand extends DebuggerCommandImpl {
    PauseCommand() {
    }

    @Override
    public void action() {
      if (!isAttached() || getVirtualMachineProxy().isPausePressed()) {
        return;
      }
      logThreads();
      getVirtualMachineProxy().suspend();
      logThreads();
      SuspendContextImpl suspendContext = mySuspendManager.pushSuspendContext(EventRequest.SUSPEND_ALL, 0);
      myDebugProcessDispatcher.getMulticaster().paused(suspendContext);
    }
  }

  private class ResumeThreadCommand extends SuspendContextCommandImpl {
    private final ThreadReferenceProxyImpl myThread;

    ResumeThreadCommand(SuspendContextImpl suspendContext, @NotNull ThreadReferenceProxyImpl thread) {
      super(suspendContext);
      myThread = thread;
    }

    @Override
    public void contextAction(@NotNull SuspendContextImpl context) {
      // handle unfreeze through the regular context resume
      if (false && getSuspendManager().isFrozen(myThread)) {
        getSuspendManager().unfreezeThread(myThread);
        return;
      }

      final Set<SuspendContextImpl> suspendingContexts = SuspendManagerUtil.getSuspendingContexts(getSuspendManager(), myThread);
      for (SuspendContextImpl suspendContext : suspendingContexts) {
        if (suspendContext.getSuspendPolicy() == EventRequest.SUSPEND_EVENT_THREAD && suspendContext.getThread() == myThread) {
          getSession().getXDebugSession().sessionResumed();
          getManagerThread().invoke(createResumeCommand(suspendContext));
        }
        else {
          getSuspendManager().resumeThread(suspendContext, myThread);
        }
      }
    }
  }

  private class FreezeThreadCommand extends DebuggerCommandImpl {
    private final ThreadReferenceProxyImpl myThread;

    FreezeThreadCommand(ThreadReferenceProxyImpl thread) {
      myThread = thread;
    }

    @Override
    protected void action() {
      SuspendManager suspendManager = getSuspendManager();
      if (!suspendManager.isFrozen(myThread)) {
        suspendManager.freezeThread(myThread);
        SuspendContextImpl suspendContext = mySuspendManager.pushSuspendContext(EventRequest.SUSPEND_EVENT_THREAD, 0);
        suspendContext.setThread(myThread.getThreadReference());
        myDebugProcessDispatcher.getMulticaster().paused(suspendContext);
      }
    }
  }

  private class PopFrameCommand extends DebuggerContextCommandImpl {
    private final StackFrameProxyImpl myStackFrame;

    PopFrameCommand(DebuggerContextImpl context, StackFrameProxyImpl frameProxy) {
      super(context, frameProxy.threadProxy());
      myStackFrame = frameProxy;
    }

    @Override
    public void threadAction(@NotNull SuspendContextImpl suspendContext) {
      final ThreadReferenceProxyImpl thread = myStackFrame.threadProxy();
      try {
        if (!getSuspendManager().isSuspended(thread)) {
          notifyCancelled();
          return;
        }
      }
      catch (ObjectCollectedException ignored) {
        notifyCancelled();
        return;
      }

      if (!suspendContext.suspends(thread)) {
        suspendContext.postponeCommand(this);
        return;
      }

      if (myStackFrame.isBottom()) {
        DebuggerInvocationUtil.swingInvokeLater(myProject, () -> Messages.showMessageDialog(myProject, JavaDebuggerBundle
          .message("error.pop.bottom.stackframe"), XDebuggerBundle.message("xdebugger.reset.frame.title"), Messages.getErrorIcon()));
        return;
      }

      try {
        thread.popFrames(myStackFrame);
        getSuspendManager().popFrame(suspendContext);
      }
      catch (NativeMethodException e) {
        showError(JavaDebuggerBundle.message("error.native.method.exception"));
        LOG.info(e);
      }
      catch (EvaluateException e) {
        showError(JavaDebuggerBundle.message("error.pop.stackframe", e.getLocalizedMessage()));
        LOG.info(e);
      }
    }

    private void showError(@NlsContexts.DialogMessage String message) {
      DebuggerInvocationUtil.swingInvokeLater(myProject, () ->
        Messages.showMessageDialog(myProject, message,
                                   XDebuggerBundle.message("xdebugger.reset.frame.title"), Messages.getErrorIcon()));
    }
  }

  @Override
  @NotNull
  public GlobalSearchScope getSearchScope() {
    LOG.assertTrue(mySession != null, "Accessing debug session before its initialization");
    return mySession.getSearchScope();
  }

  public void reattach(final DebugEnvironment environment) {
    reattach(environment, false, () -> {});
  }

  public void reattach(final DebugEnvironment environment, boolean keepCurrentVM, Runnable vmReadyCallback) {
    reattach(environment, () -> {
      if (keepCurrentVM) {
        detachProcess(false, true, vmData -> {
          myStashedVirtualMachines.addFirst(vmData);
          myDebuggerManagerThread = new DebuggerManagerThreadImpl(myDisposable, myProject);
        });
      }
      else {
        closeProcess(false);
      }
    }, vmReadyCallback);
  }

  private void unstashAndReattach() {
    VirtualMachineData vmData = myStashedVirtualMachines.pollFirst();
    if (vmData != null && vmData.vm != null) {
      myDebuggerManagerThread = vmData.debuggerManagerThread;
      reattach(vmData.connection, () -> {}, () -> {
        afterProcessStarted(() -> getManagerThread().schedule(new DebuggerCommandImpl() {
          @Override
          protected void action() {
            try {
              commitVM(vmData.vm.getVirtualMachine());
            }
            catch (VMDisconnectedException e) {
              fail();
            }
          }
        }));
      });
    }
  }

  private void reattach(DebugEnvironment environment, Runnable detachVm, Runnable vmReadyCallback) {
    reattach(environment.getRemoteConnection(), detachVm, () -> {
      createVirtualMachine(environment);
      vmReadyCallback.run();
    });
  }

  private void reattach(RemoteConnection connection, Runnable detachVm, Runnable attachVm) {
    if (!myIsStopped.get()) {
      getManagerThread().schedule(new DebuggerCommandImpl() {
        @Override
        protected void action() {
          detachVm.run();
          getManagerThread().processRemaining();
          doReattach();
        }

        @Override
        protected void commandCancelled() {
          doReattach(); // if the original process is already finished
        }

        private void doReattach() {
          DebuggerInvocationUtil.swingInvokeLater(myProject, () -> {
            ((XDebugSessionImpl)getXdebugProcess().getSession()).reset();
            myState.set(State.INITIAL);
            myConnection = connection;
            getManagerThread().restartIfNeeded();
            attachVm.run();
          });
        }
      });
    }
  }

  @Nullable
  public ExecutionResult attachVirtualMachine(@NotNull DebugEnvironment environment,
                                              @NotNull DebuggerSession session) throws ExecutionException {
    mySession = session;
    myWaitFor.down();

    LOG.assertTrue(isInInitialState());

    myConnection = environment.getRemoteConnection();

    // in client mode start target process before the debugger to reduce polling
    if (!(myConnection instanceof RemoteConnectionStub) &&
        !(myConnection instanceof DelayedRemoteConnection) &&
        myConnection.isServerMode()) {
      createVirtualMachine(environment);
      if (myIsFailed.get()) {
        myExecutionResult = null;
        return null;
      }
    }

    ExecutionResult executionResult;
    try {
      synchronized (myProcessListeners) {
        executionResult = environment.createExecutionResult();
        myExecutionResult = executionResult;
        if (executionResult == null) {
          fail();
          return null;
        }
        for (ProcessListener processListener : myProcessListeners) {
          executionResult.getProcessHandler().addProcessListener(processListener);
        }
        myProcessListeners.clear();
        if (!myTextBeforeStart.isEmpty()) {
          printToConsoleImpl(myTextBeforeStart.toString());
          myTextBeforeStart.setLength(0);
        }
      }
    }
    catch (ExecutionException e) {
      fail();
      throw e;
    }

    if (myConnection instanceof DelayedRemoteConnection) {
      ((DelayedRemoteConnection)myConnection).setAttachRunnable(() -> createVirtualMachine(environment));
    }
    else if (!(myConnection instanceof RemoteConnectionStub) && !myConnection.isServerMode()) {
      createVirtualMachine(environment);
      if (myIsFailed.get()) {
        myExecutionResult = null;
        return null;
      }
    }

    return executionResult;
  }

  private void fail() {
    // need this in order to prevent calling stop() twice
    if (myIsFailed.compareAndSet(false, true)) {
      stop(false);
    }
  }

  private void createVirtualMachine(final DebugEnvironment environment) {
    final String sessionName = environment.getSessionName();
    final long pollTimeout = environment.getPollTimeout();
    final Semaphore semaphore = new Semaphore();
    semaphore.down();

    final AtomicBoolean connectorIsReady = new AtomicBoolean(false);
    myDebugProcessDispatcher.addListener(new DebugProcessListener() {
      @Override
      public void connectorIsReady() {
        connectorIsReady.set(true);
        semaphore.up();
        myDebugProcessDispatcher.removeListener(this);
      }
    });

    getManagerThread().schedule(new DebuggerCommandImpl() {
      @Override
      protected void action() {
        VirtualMachine vm = null;

        try {
          final long time = System.currentTimeMillis();
          do {
            try {
              vm = createVirtualMachineInt();
              break;
            }
            catch (final ExecutionException e) {
              if (pollTimeout > 0 && !myConnection.isServerMode() && e.getCause() instanceof IOException) {
                synchronized (this) {
                  try {
                    wait(500);
                  }
                  catch (InterruptedException ignored) {
                    break;
                  }
                }
              }
              else {
                ProcessHandler processHandler = getProcessHandler();
                boolean terminated =
                  processHandler != null && (processHandler.isProcessTerminating() || processHandler.isProcessTerminated());

                fail();
                DebuggerInvocationUtil.swingInvokeLater(myProject, () -> {
                  // propagate exception only in case we succeeded to obtain execution result,
                  // otherwise if the error is induced by the fact that there is nothing to debug, and there is no need to show
                  // this problem to the user
                  if (((myExecutionResult != null && !terminated) || !connectorIsReady.get()) &&
                      !ApplicationManager.getApplication().isHeadlessEnvironment()) {
                    ExecutionUtil.handleExecutionError(myProject, ToolWindowId.DEBUG, sessionName, e);
                  }
                });
                break;
              }
            }
          }
          while (System.currentTimeMillis() - time < pollTimeout);
        }
        finally {
          semaphore.up();
        }

        if (vm != null) {
          final VirtualMachine vm1 = vm;
          afterProcessStarted(() -> getManagerThread().schedule(new DebuggerCommandImpl() {
            @Override
            protected void action() {
              try {
                commitVM(vm1);
              }
              catch (VMDisconnectedException e) {
                fail();
              }
            }
          }));
        }
        else {
          fail();
        }
      }

      @Override
      protected void commandCancelled() {
        try {
          super.commandCancelled();
        }
        finally {
          semaphore.up();
        }
      }
    });

    semaphore.waitFor();
  }

  private void afterProcessStarted(final Runnable run) {
    class MyProcessAdapter extends ProcessAdapter {
      private boolean alreadyRun = false;

      public synchronized void run() {
        if (!alreadyRun) {
          alreadyRun = true;
          run.run();
        }
        removeProcessListener(this);
      }

      @Override
      public void startNotified(@NotNull ProcessEvent event) {
        run();
      }
    }
    MyProcessAdapter processListener = new MyProcessAdapter();
    addProcessListener(processListener);
    if (myExecutionResult != null) {
      if (myExecutionResult.getProcessHandler().isStartNotified()) {
        processListener.run();
      }
    }
  }

  @NotNull
  public DebuggerCommandImpl createPauseCommand() {
    return new PauseCommand();
  }

  @NotNull
  public ResumeCommand createResumeCommand(SuspendContextImpl suspendContext) {
    return createResumeCommand(suspendContext, PrioritizedTask.Priority.HIGH);
  }

  @NotNull
  public ResumeCommand createResumeCommand(SuspendContextImpl suspendContext, final PrioritizedTask.Priority priority) {
    final BreakpointManager breakpointManager = DebuggerManagerEx.getInstanceEx(getProject()).getBreakpointManager();
    return new ResumeCommand(suspendContext) {
      @Override
      public void contextAction(@NotNull SuspendContextImpl suspendContext) {
        breakpointManager.applyThreadFilter(DebugProcessImpl.this, null); // clear the filter on resume
        if (myReturnValueWatcher != null) {
          myReturnValueWatcher.clear();
        }
        super.contextAction(suspendContext);
      }

      @Override
      public Priority getPriority() {
        return priority;
      }
    };
  }

  @NotNull
  public ResumeCommand createStepOverCommand(SuspendContextImpl suspendContext, boolean ignoreBreakpoints) {
    return createStepOverCommand(suspendContext, ignoreBreakpoints, StepRequest.STEP_LINE);
  }

  @NotNull
  public ResumeCommand createStepOverCommand(SuspendContextImpl suspendContext, boolean ignoreBreakpoints, int stepSize) {
    return createStepOverCommand(suspendContext, ignoreBreakpoints, null, StepRequest.STEP_LINE);
  }

  @NotNull
  public ResumeCommand createStepOverCommand(SuspendContextImpl suspendContext,
                                             boolean ignoreBreakpoints,
                                             @Nullable MethodFilter methodFilter,
                                             int stepSize) {
    return new StepOverCommand(suspendContext, ignoreBreakpoints, methodFilter, stepSize);
  }

  @NotNull
  public ResumeCommand createStepOutCommand(SuspendContextImpl suspendContext) {
    return createStepOutCommand(suspendContext, StepRequest.STEP_LINE);
  }

  @NotNull
  public ResumeCommand createStepOutCommand(SuspendContextImpl suspendContext, int stepSize) {
    return new StepOutCommand(suspendContext, stepSize);
  }

  @NotNull
  public ResumeCommand createStepIntoCommand(SuspendContextImpl suspendContext, boolean ignoreFilters, final MethodFilter smartStepFilter) {
    return createStepIntoCommand(suspendContext, ignoreFilters, smartStepFilter, StepRequest.STEP_LINE);
  }

  @NotNull
  public ResumeCommand createStepIntoCommand(SuspendContextImpl suspendContext, boolean ignoreFilters, final MethodFilter smartStepFilter,
                                             int stepSize) {
    return new StepIntoCommand(suspendContext, ignoreFilters, smartStepFilter, stepSize);
  }

  @NotNull
  public ResumeCommand createRunToCursorCommand(SuspendContextImpl suspendContext,
                                                @NotNull XSourcePosition position,
                                                boolean ignoreBreakpoints)
    throws EvaluateException {
    RunToCursorCommand runToCursorCommand = new RunToCursorCommand(suspendContext, position, ignoreBreakpoints);
    if (runToCursorCommand.myRunToCursorBreakpoint == null) {
      PsiFile psiFile = PsiManager.getInstance(myProject).findFile(position.getFile());
      throw new EvaluateException(
        JavaDebuggerBundle.message("error.running.to.cursor.no.executable.code", psiFile != null ? psiFile.getName() : "<No File>",
                                   position.getLine()), null);
    }
    return runToCursorCommand;
  }

  @NotNull
  public DebuggerCommandImpl createFreezeThreadCommand(ThreadReferenceProxyImpl thread) {
    return new FreezeThreadCommand(thread);
  }

  @NotNull
  public SuspendContextCommandImpl createResumeThreadCommand(SuspendContextImpl suspendContext, @NotNull ThreadReferenceProxyImpl thread) {
    return new ResumeThreadCommand(suspendContext, thread);
  }

  @NotNull
  public SuspendContextCommandImpl createPopFrameCommand(DebuggerContextImpl context, StackFrameProxyImpl stackFrame) {
    return new PopFrameCommand(context, stackFrame);
  }

  //public void setBreakpointsMuted(final boolean muted) {
  //  XDebugSession session = mySession.getXDebugSession();
  //  if (isAttached()) {
  //    getManagerThread().schedule(new DebuggerCommandImpl() {
  //      @Override
  //      protected void action() {
  //        // set the flag before enabling/disabling cause it affects if breakpoints will create requests
  //        if (myBreakpointsMuted.getAndSet(muted) != muted) {
  //          final BreakpointManager breakpointManager = DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager();
  //          if (muted) {
  //            breakpointManager.disableBreakpoints(DebugProcessImpl.this);
  //          }
  //          else {
  //            breakpointManager.enableBreakpoints(DebugProcessImpl.this);
  //          }
  //        }
  //      }
  //    });
  //  }
  //  else {
  //    session.setBreakpointMuted(muted);
  //  }
  //}

  @NotNull
  public DebuggerContextImpl getDebuggerContext() {
    return mySession.getContextManager().getContext();
  }

  public void setXDebugProcess(JavaDebugProcess XDebugProcess) {
    myXDebugProcess = XDebugProcess;
  }

  @Nullable
  public JavaDebugProcess getXdebugProcess() {
    return myXDebugProcess;
  }

  public boolean areBreakpointsMuted() {
    XDebugSession session = mySession.getXDebugSession();
    return session != null && session.areBreakpointsMuted();
  }

  public DebuggerSession getSession() {
    return mySession;
  }

  static boolean isResumeOnlyCurrentThread() {
    return DebuggerSettings.getInstance().RESUME_ONLY_CURRENT_THREAD;
  }

  public boolean isEvaluationPossible() {
    return getSuspendManager().getPausedContext() != null;
  }

  public boolean isEvaluationPossible(SuspendContextImpl suspendContext) {
    return mySuspendManager.hasPausedContext(suspendContext);
  }

  public void startWatchingMethodReturn(ThreadReferenceProxyImpl thread) {
    if (myReturnValueWatcher != null) {
      myReturnValueWatcher.enable(thread.getThreadReference());
    }
  }

  void stopWatchingMethodReturn() {
    if (myReturnValueWatcher != null) {
      myReturnValueWatcher.disable();
    }
  }

  private record VirtualMachineData(VirtualMachineProxyImpl vm, RemoteConnection connection,
                                    DebuggerManagerThreadImpl debuggerManagerThread) {
  }
}
