IDEA-79921 Suspend one thread while debugging
[idea/community.git] / java / debugger / impl / src / com / intellij / debugger / engine / SuspendManagerImpl.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.debugger.engine;
17
18 import com.intellij.debugger.engine.events.DebuggerCommandImpl;
19 import com.intellij.debugger.jdi.ThreadReferenceProxyImpl;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.sun.jdi.InternalException;
22 import com.sun.jdi.ObjectCollectedException;
23 import com.sun.jdi.event.EventSet;
24 import com.sun.jdi.request.EventRequest;
25 import org.jetbrains.annotations.NotNull;
26
27 import java.util.*;
28
29 /**
30  * @author lex
31  */
32 public class SuspendManagerImpl implements SuspendManager {
33   private static final Logger LOG = Logger.getInstance("#com.intellij.debugger.engine.SuspendManager");
34
35   private final LinkedList<SuspendContextImpl> myEventContexts  = new LinkedList<SuspendContextImpl>();
36   /**
37    * contexts, paused at breakpoint or another debugger event requests. Note that thread, explicitly paused by user is not considered as
38    * "paused at breakpoint" and JDI prohibits data queries on its stack frames
39    */
40   private final LinkedList<SuspendContextImpl> myPausedContexts = new LinkedList<SuspendContextImpl>();
41   private final Set<ThreadReferenceProxyImpl>  myFrozenThreads  = Collections.synchronizedSet(new HashSet<ThreadReferenceProxyImpl>());
42
43   private final DebugProcessImpl myDebugProcess;
44
45   public int suspends = 0;
46
47   public SuspendManagerImpl(@NotNull DebugProcessImpl debugProcess) {
48     myDebugProcess = debugProcess;
49     myDebugProcess.addDebugProcessListener(new DebugProcessAdapterImpl() {
50       @Override
51       public void processDetached(DebugProcessImpl process, boolean closedByUser) {
52         myEventContexts.clear();
53         myPausedContexts.clear();
54         myFrozenThreads.clear();
55       }
56     });
57   }
58
59   @Override
60   public SuspendContextImpl pushSuspendContext(final int suspendPolicy, int nVotes) {
61     SuspendContextImpl suspendContext = new SuspendContextImpl(myDebugProcess, suspendPolicy, nVotes, null) {
62       @Override
63       protected void resumeImpl() {
64         if (LOG.isDebugEnabled()) {
65           LOG.debug("Start resuming...");
66         }
67         myDebugProcess.logThreads();
68         switch(getSuspendPolicy()) {
69           case EventRequest.SUSPEND_ALL:
70             int resumeAttempts = 5;
71             while (--resumeAttempts > 0) {
72               try {
73                 myDebugProcess.getVirtualMachineProxy().resume();
74                 break;
75               }
76               catch (InternalException e) {
77                 //InternalException 13 means that there are running threads that we are trying to resume
78                 //On MacOS it happened that native thread didn't stop while some java thread reached breakpoint
79                 //noinspection StatementWithEmptyBody
80                 if (/*Patches.MAC_RESUME_VM_HACK && */e.errorCode() == 13) {
81                   //Its funny, but second resume solves the problem
82                 }
83                 else {
84                   LOG.error(e);
85                   break;
86                 }
87               }
88             }
89             
90             if (LOG.isDebugEnabled()) {
91               LOG.debug("VM resumed ");
92             }
93             break;
94           case EventRequest.SUSPEND_EVENT_THREAD:
95             myFrozenThreads.remove(getThread());
96             getThread().resume();
97             if(LOG.isDebugEnabled()) {
98               LOG.debug("Thread resumed : " + getThread().toString());
99             }
100             break;
101           case EventRequest.SUSPEND_NONE:
102             if (LOG.isDebugEnabled()) {
103               LOG.debug("None resumed");
104             }
105             break;
106         }
107         if (LOG.isDebugEnabled()) {
108           LOG.debug("Suspends = " + suspends);
109         }
110         myDebugProcess.logThreads();
111       }
112     };
113     pushContext(suspendContext);
114     return suspendContext;
115   }
116
117   @Override
118   public SuspendContextImpl pushSuspendContext(final EventSet set) {
119     SuspendContextImpl suspendContext = new SuspendContextImpl(myDebugProcess, set.suspendPolicy(), set.size(), set) {
120       @Override
121       protected void resumeImpl() {
122         if (LOG.isDebugEnabled()) {
123           LOG.debug("Start resuming eventSet " + set.toString() + " suspendPolicy = " + set.suspendPolicy() + ",size = " + set.size());
124         }
125         myDebugProcess.logThreads();
126         //final ThreadReferenceProxyImpl thread = getThread();
127         //
128         //if (thread != null) { // check that thread is suspended at the moment
129         //  try {
130         //    if (!thread.isSuspended()) {
131         //      final int status = thread.status();
132         //      if ((status != ThreadReference.THREAD_STATUS_ZOMBIE) && (status != ThreadReference.THREAD_STATUS_NOT_STARTED) && (status != ThreadReference.THREAD_STATUS_UNKNOWN)) {
133         //        LOG.error("Context thread must be suspended");
134         //      }
135         //    }
136         //  }
137         //  catch (ObjectCollectedException ignored) {}
138         //}
139
140         int attempts = 5;
141         while (--attempts > 0) {
142           try {
143             set.resume();
144             break;
145           }
146           catch (ObjectCollectedException e) {
147             // according to error reports set.resume() may throw this if one of the threads has been collected
148             LOG.info(e);
149           }
150           catch (InternalException e) {
151             //InternalException 13 means that there are running threads that we are trying to resume
152             //On MacOS it happened that native thread didn't stop while some java thread reached breakpoint
153             //noinspection StatementWithEmptyBody
154             if (/*Patches.MAC_RESUME_VM_HACK && */e.errorCode() == 13 && set.suspendPolicy() == EventRequest.SUSPEND_ALL) {
155               //Its funny, but second resume solves the problem
156             }
157             else {
158               LOG.error(e);
159               break;
160             }
161           }
162         }
163         if (LOG.isDebugEnabled()) {
164           LOG.debug("Set resumed ");
165         }
166         myDebugProcess.logThreads();
167       }
168     };
169     pushContext(suspendContext);
170     return suspendContext;
171   }
172
173   private void pushContext(SuspendContextImpl suspendContext) {
174     DebuggerManagerThreadImpl.assertIsManagerThread();
175     myEventContexts.addFirst(suspendContext);
176     suspends++;
177     if (LOG.isDebugEnabled()) {
178       LOG.debug("Push context : Suspends = " + suspends);
179     }
180   }
181
182   @Override
183   public void resume(SuspendContextImpl context) {
184     SuspendManagerUtil.prepareForResume(context);
185
186     myDebugProcess.logThreads();
187     final int suspendPolicy = context.getSuspendPolicy();
188     popContext(context);
189     context.resume();
190     myDebugProcess.clearCashes(suspendPolicy);
191   }
192
193   @Override
194   public void popFrame(SuspendContextImpl suspendContext) {
195     popContext(suspendContext);
196     SuspendContextImpl newSuspendContext = pushSuspendContext(suspendContext.getSuspendPolicy(), 0);
197     newSuspendContext.setThread(suspendContext.getThread().getThreadReference());
198     notifyPaused(newSuspendContext);
199   }
200
201   @Override
202   public SuspendContextImpl getPausedContext() {
203     return !myPausedContexts.isEmpty() ? myPausedContexts.getFirst() : null;
204   }
205
206   public void popContext(SuspendContextImpl suspendContext) {
207     DebuggerManagerThreadImpl.assertIsManagerThread();
208     suspends--;
209     if (LOG.isDebugEnabled()) {
210       LOG.debug("popContext, suspends = " + suspends);
211     }
212     myEventContexts.remove(suspendContext);
213     myPausedContexts.remove(suspendContext);
214   }
215
216   void pushPausedContext(SuspendContextImpl suspendContext) {
217     if(LOG.isDebugEnabled()) {
218       LOG.assertTrue(myEventContexts.contains(suspendContext));
219     }
220
221     myPausedContexts.addFirst(suspendContext);
222   }
223
224   public boolean hasEventContext(SuspendContextImpl suspendContext) {
225     DebuggerManagerThreadImpl.assertIsManagerThread();
226     return myEventContexts.contains(suspendContext);
227   }
228
229   @Override
230   public List<SuspendContextImpl> getEventContexts() {
231     DebuggerManagerThreadImpl.assertIsManagerThread();
232     return Collections.unmodifiableList(myEventContexts);
233   }
234
235   @Override
236   public boolean isFrozen(ThreadReferenceProxyImpl thread) {
237     return myFrozenThreads.contains(thread);
238   }
239
240   @Override
241   public boolean isSuspended(ThreadReferenceProxyImpl thread) throws ObjectCollectedException{
242     DebuggerManagerThreadImpl.assertIsManagerThread();
243
244     boolean suspended = false;
245
246     if (isFrozen(thread)) {
247       suspended = true;
248     }
249     else {
250       for (SuspendContextImpl suspendContext : myEventContexts) {
251         if (suspendContext.suspends(thread)) {
252           suspended = true;
253           break;
254         }
255       }
256     }
257
258     //bug in JDI : newly created thread may be resumed even when suspendPolicy == SUSPEND_ALL
259     //if(LOG.isDebugEnabled() && suspended) {
260     //  LOG.assertTrue(thread.suspends(), thread.name());
261     //}
262     return suspended && (thread == null || thread.isSuspended());
263   }
264
265   @Override
266   public void suspendThread(SuspendContextImpl context, ThreadReferenceProxyImpl thread) {
267     LOG.assertTrue(thread != context.getThread(), "Thread is already suspended at the breakpoint");
268
269     if(context.isExplicitlyResumed(thread)) {
270       context.myResumedThreads.remove(thread);
271       thread.suspend();
272     }
273   }
274
275   @Override
276   public void resumeThread(SuspendContextImpl context, ThreadReferenceProxyImpl thread) {
277     LOG.assertTrue(thread != context.getThread(), "Use resume() instead of resuming breakpoint thread");
278     LOG.assertTrue(!context.isExplicitlyResumed(thread));
279
280     if(context.myResumedThreads == null) {
281       context.myResumedThreads = new HashSet<ThreadReferenceProxyImpl>();
282     }
283     context.myResumedThreads.add(thread);
284     thread.resume();
285   }
286
287   @Override
288   public void freezeThread(ThreadReferenceProxyImpl thread) {
289     if (myFrozenThreads.add(thread)) {
290       thread.suspend();
291     }
292   }
293
294   @Override
295   public void unfreezeThread(ThreadReferenceProxyImpl thread) {
296     if (myFrozenThreads.remove(thread)) {
297       thread.resume();
298     }
299   }
300
301   private void processVote(final SuspendContextImpl suspendContext) {
302     LOG.assertTrue(suspendContext.myVotesToVote > 0);
303     suspendContext.myVotesToVote--;
304
305     if (LOG.isDebugEnabled()) {
306       LOG.debug("myVotesToVote = " +  suspendContext.myVotesToVote);
307     }
308     if(suspendContext.myVotesToVote == 0) {
309       if(suspendContext.myIsVotedForResume) {
310         // resume in a separate request to allow other requests be processed (e.g. dependent bpts enable)
311         myDebugProcess.getManagerThread().schedule(new DebuggerCommandImpl() {
312           @Override
313           protected void action() throws Exception {
314             resume(suspendContext);
315           }
316
317           @Override
318           public Priority getPriority() {
319             return Priority.HIGH;
320           }
321         });
322       }
323       else {
324         if (LOG.isDebugEnabled()) {
325           LOG.debug("vote paused");
326         }
327         myDebugProcess.logThreads();
328         myDebugProcess.cancelRunToCursorBreakpoint();
329         final ThreadReferenceProxyImpl thread = suspendContext.getThread();
330         myDebugProcess.deleteStepRequests(thread != null? thread.getThreadReference() : null);
331         notifyPaused(suspendContext);
332       }
333     }
334   }
335
336   public void notifyPaused(SuspendContextImpl suspendContext) {
337     pushPausedContext(suspendContext);
338     myDebugProcess.myDebugProcessDispatcher.getMulticaster().paused(suspendContext);
339   }
340
341   @Override
342   public void voteResume(SuspendContextImpl suspendContext) {
343     if (LOG.isDebugEnabled()) {
344       LOG.debug("Resume voted");
345     }
346     processVote(suspendContext);
347   }
348
349   @Override
350   public void voteSuspend(SuspendContextImpl suspendContext) {
351     suspendContext.myIsVotedForResume = false;
352     processVote(suspendContext);
353   }
354
355   LinkedList<SuspendContextImpl> getPausedContexts() {
356     return myPausedContexts;
357   }
358 }