1 /*
  2  * Copyright (c) 1998, 2021, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 
 26 package com.sun.tools.jdi;
 27 
 28 import java.lang.ref.WeakReference;
 29 import java.util.ArrayList;
 30 import java.util.Arrays;
 31 import java.util.Collections;
 32 import java.util.Iterator;
 33 import java.util.List;
 34 
 35 import com.sun.jdi.ClassNotLoadedException;
 36 import com.sun.jdi.IncompatibleThreadStateException;
 37 import com.sun.jdi.InternalException;
 38 import com.sun.jdi.InvalidStackFrameException;
 39 import com.sun.jdi.InvalidTypeException;
 40 import com.sun.jdi.Location;
 41 import com.sun.jdi.MonitorInfo;
 42 import com.sun.jdi.NativeMethodException;
 43 import com.sun.jdi.ObjectReference;
 44 import com.sun.jdi.ReferenceType;
 45 import com.sun.jdi.StackFrame;
 46 import com.sun.jdi.ThreadGroupReference;
 47 import com.sun.jdi.ThreadReference;
 48 import com.sun.jdi.Value;
 49 import com.sun.jdi.VirtualMachine;
 50 import com.sun.jdi.request.BreakpointRequest;
 51 
 52 public class ThreadReferenceImpl extends ObjectReferenceImpl
 53                                  implements ThreadReference {
 54     static final int SUSPEND_STATUS_SUSPENDED = 0x1;
 55     static final int SUSPEND_STATUS_BREAK = 0x2;
 56 
 57     private int suspendedZombieCount = 0;
 58 
 59     /*
 60      * Some objects can only be created while a thread is suspended and are valid
 61      * only while the thread remains suspended.  Examples are StackFrameImpl
 62      * and MonitorInfoImpl.  When the thread resumes, these objects have to be
 63      * marked as invalid so that their methods can throw
 64      * InvalidStackFrameException if they are called.  To do this, such objects
 65      * register themselves as listeners of the associated thread.  When the
 66      * thread is resumed, its listeners are notified and mark themselves
 67      * invalid.
 68      * Also, note that ThreadReferenceImpl itself caches some info that
 69      * is valid only as long as the thread is suspended.  When the thread
 70      * is resumed, that cache must be purged.
 71      * Lastly, note that ThreadReferenceImpl and its super, ObjectReferenceImpl
 72      * cache some info that is only valid as long as the entire VM is suspended.
 73      * If _any_ thread is resumed, this cache must be purged.  To handle this,
 74      * both ThreadReferenceImpl and ObjectReferenceImpl register themselves as
 75      * VMListeners so that they get notified when all threads are suspended and
 76      * when any thread is resumed.
 77      */
 78 
 79     // The ThreadGroup is cached for the life of the thread
 80     private ThreadGroupReference threadGroup;
 81 
 82     // Whether a thread is a virtual thread or not is cached
 83     private volatile boolean isVirtual;
 84     private volatile boolean isVirtualCached;
 85 
 86     // This is cached only while this one thread is suspended.  Each time
 87     // the thread is resumed, we abandon the current cache object and
 88     // create a new initialized one.
 89     private static class LocalCache {
 90         JDWP.ThreadReference.Status status = null;
 91         List<StackFrame> frames = null;
 92         int framesStart = -1;
 93         int framesLength = 0;
 94         int frameCount = -1;
 95         List<ObjectReference> ownedMonitors = null;
 96         List<MonitorInfo> ownedMonitorsInfo = null;
 97         ObjectReference contendedMonitor = null;
 98         boolean triedCurrentContended = false;
 99     }
100 
101     /*
102      * The localCache instance var is set by resetLocalCache to an initialized
103      * object as shown above.  This occurs when the ThreadReference
104      * object is created, and when the mirrored thread is resumed.
105      * The fields are then filled in by the relevant methods as they
106      * are called.  A problem can occur if resetLocalCache is called
107      * (ie, a resume() is executed) at certain points in the execution
108      * of some of these methods - see 6751643.  To avoid this, each
109      * method that wants to use this cache must make a local copy of
110      * this variable and use that.  This means that each invocation of
111      * these methods will use a copy of the cache object that was in
112      * effect at the point that the copy was made; if a racy resume
113      * occurs, it won't affect the method's local copy.  This means that
114      * the values returned by these calls may not match the state of
115      * the debuggee at the time the caller gets the values.  EG,
116      * frameCount() is called and comes up with 5 frames.  But before
117      * it returns this, a resume of the debuggee thread is executed in a
118      * different debugger thread.  The thread is resumed and running at
119      * the time that the value 5 is returned.  Or even worse, the thread
120      * could be suspended again and have a different number of frames, eg, 24,
121      * but this call will still return 5.
122      */
123     private LocalCache localCache;
124 
125     private void resetLocalCache() {
126         localCache = new LocalCache();
127     }
128 
129     // This is cached only while all threads in the VM are suspended
130     // Yes, someone could change the name of a thread while it is suspended.
131     private static class Cache extends ObjectReferenceImpl.Cache {
132         String name = null;
133     }
134     protected ObjectReferenceImpl.Cache newCache() {
135         return new Cache();
136     }
137 
138     // Listeners - synchronized on vm.state()
139     private List<WeakReference<ThreadListener>> listeners = new ArrayList<>();
140 
141     ThreadReferenceImpl(VirtualMachine aVm, long aRef) {
142         super(aVm, aRef);
143         resetLocalCache();
144         vm.state().addListener(this);
145     }
146 
147     protected String description() {
148         return "ThreadReference " + uniqueID();
149     }
150 
151     /*
152      * VMListener implementation
153      */
154     public boolean vmNotSuspended(VMAction action) {
155         if (action.resumingThread() == null) {
156             // all threads are being resumed
157             synchronized (vm.state()) {
158                 processThreadAction(new ThreadAction(this,
159                                             ThreadAction.THREAD_RESUMABLE));
160             }
161 
162         }
163 
164         /*
165          * Othewise, only one thread is being resumed:
166          *   if it is us,
167          *      we have already done our processThreadAction to notify our
168          *      listeners when we processed the resume.
169          *   if it is not us,
170          *      we don't want to notify our listeners
171          *       because we are not being resumed.
172          */
173         return super.vmNotSuspended(action);
174     }
175 
176     /**
177      * Note that we only cache the name string while the entire VM is suspended
178      * because the name can change via Thread.setName arbitrarily while this
179      * thread is running.
180      */
181     public String name() {
182         String name = null;
183         try {
184             Cache local = (Cache)getCache();
185 
186             if (local != null) {
187                 name = local.name;
188             }
189             if (name == null) {
190                 name = JDWP.ThreadReference.Name.process(vm, this).threadName;
191                 if (local != null) {
192                     local.name = name;
193                 }
194             }
195         } catch (JDWPException exc) {
196             throw exc.toJDIException();
197         }
198         return name;
199     }
200 
201     /*
202      * Sends a command to the back end which is defined to do an
203      * implicit vm-wide resume.
204      */
205     PacketStream sendResumingCommand(CommandSender sender) {
206         synchronized (vm.state()) {
207             processThreadAction(new ThreadAction(this,
208                                         ThreadAction.THREAD_RESUMABLE));
209             return sender.send();
210         }
211     }
212 
213     public void suspend() {
214         try {
215             JDWP.ThreadReference.Suspend.process(vm, this);
216         } catch (JDWPException exc) {
217             throw exc.toJDIException();
218         }
219         // Don't consider the thread suspended yet. On reply, notifySuspend()
220         // will be called.
221     }
222 
223     public void resume() {
224         /*
225          * If it's a zombie, we can just update internal state without
226          * going to back end.
227          */
228         if (suspendedZombieCount > 0) {
229             suspendedZombieCount--;
230             return;
231         }
232 
233         PacketStream stream;
234         synchronized (vm.state()) {
235             processThreadAction(new ThreadAction(this,
236                                       ThreadAction.THREAD_RESUMABLE));
237             stream = JDWP.ThreadReference.Resume.enqueueCommand(vm, this);
238         }
239         try {
240             JDWP.ThreadReference.Resume.waitForReply(vm, stream);
241         } catch (JDWPException exc) {
242             throw exc.toJDIException();
243         }
244     }
245 
246     public int suspendCount() {
247         /*
248          * If it's a zombie, we maintain the count in the front end.
249          */
250         if (suspendedZombieCount > 0) {
251             return suspendedZombieCount;
252         }
253 
254         try {
255             return JDWP.ThreadReference.SuspendCount.process(vm, this).suspendCount;
256         } catch (JDWPException exc) {
257             throw exc.toJDIException();
258         }
259     }
260 
261     public void stop(ObjectReference throwable) throws InvalidTypeException {
262         validateMirrorOrNull(throwable);
263         // Verify that the given object is a Throwable instance
264         List<ReferenceType> list = vm.classesByName("java.lang.Throwable");
265         ClassTypeImpl throwableClass = (ClassTypeImpl)list.get(0);
266         if ((throwable == null) ||
267             !throwableClass.isAssignableFrom(throwable)) {
268              throw new InvalidTypeException("Not an instance of Throwable");
269         }
270 
271         try {
272             JDWP.ThreadReference.Stop.process(vm, this,
273                                          (ObjectReferenceImpl)throwable);
274         } catch (JDWPException exc) {
275             throw exc.toJDIException();
276         }
277     }
278 
279     public void interrupt() {
280         try {
281             JDWP.ThreadReference.Interrupt.process(vm, this);
282         } catch (JDWPException exc) {
283             throw exc.toJDIException();
284         }
285     }
286 
287     private JDWP.ThreadReference.Status jdwpStatus() {
288         LocalCache snapshot = localCache;
289         JDWP.ThreadReference.Status myStatus = snapshot.status;
290         try {
291              if (myStatus == null) {
292                  myStatus = JDWP.ThreadReference.Status.process(vm, this);
293                 if ((myStatus.suspendStatus & SUSPEND_STATUS_SUSPENDED) != 0) {
294                     // thread is suspended, we can cache the status.
295                     snapshot.status = myStatus;
296                 }
297             }
298          } catch (JDWPException exc) {
299             throw exc.toJDIException();
300         }
301         return myStatus;
302     }
303 
304     public int status() {
305         return jdwpStatus().threadStatus;
306     }
307 
308     public boolean isSuspended() {
309         return ((suspendedZombieCount > 0) ||
310                 ((jdwpStatus().suspendStatus & SUSPEND_STATUS_SUSPENDED) != 0));
311     }
312 
313     public boolean isAtBreakpoint() {
314         /*
315          * TO DO: This fails to take filters into account.
316          */
317         try {
318             StackFrame frame = frame(0);
319             Location location = frame.location();
320             List<BreakpointRequest> requests = vm.eventRequestManager().breakpointRequests();
321             for (BreakpointRequest request : requests) {
322                 if (location.equals(request.location())) {
323                     return true;
324                 }
325             }
326             return false;
327         } catch (IndexOutOfBoundsException iobe) {
328             return false;  // no frames on stack => not at breakpoint
329         } catch (IncompatibleThreadStateException itse) {
330             // Per the javadoc, not suspended => return false
331             return false;
332         }
333     }
334 
335     public ThreadGroupReference threadGroup() {
336         /*
337          * Thread group can't change, so it's cached once and for all.
338          */
339         if (threadGroup == null) {
340             try {
341                 threadGroup = JDWP.ThreadReference.ThreadGroup.
342                     process(vm, this).group;
343             } catch (JDWPException exc) {
344                 throw exc.toJDIException();
345             }
346         }
347         return threadGroup;
348     }
349 
350     public int frameCount() throws IncompatibleThreadStateException  {
351         LocalCache snapshot = localCache;
352         try {
353             if (snapshot.frameCount == -1) {
354                 snapshot.frameCount = JDWP.ThreadReference.FrameCount
355                                           .process(vm, this).frameCount;
356             }
357         } catch (JDWPException exc) {
358             switch (exc.errorCode()) {
359             case JDWP.Error.THREAD_NOT_SUSPENDED:
360             case JDWP.Error.INVALID_THREAD:   /* zombie */
361                 throw new IncompatibleThreadStateException();
362             default:
363                 throw exc.toJDIException();
364             }
365         }
366         return snapshot.frameCount;
367     }
368 
369     public List<StackFrame> frames() throws IncompatibleThreadStateException  {
370         return privateFrames(0, -1);
371     }
372 
373     public StackFrame frame(int index) throws IncompatibleThreadStateException  {
374         List<StackFrame> list = privateFrames(index, 1);
375         return list.get(0);
376     }
377 
378     /**
379      * Is the requested subrange within what has been retrieved?
380      * local is known to be non-null.  Should only be called from
381      * a sync method.
382      */
383     private boolean isSubrange(LocalCache snapshot,
384                                int start, int length) {
385         if (start < snapshot.framesStart) {
386             return false;
387         }
388         if (length == -1) {
389             return (snapshot.framesLength == -1);
390         }
391         if (snapshot.framesLength == -1) {
392             if ((start + length) > (snapshot.framesStart +
393                                     snapshot.frames.size())) {
394                 throw new IndexOutOfBoundsException();
395             }
396             return true;
397         }
398         return ((start + length) <= (snapshot.framesStart + snapshot.framesLength));
399     }
400 
401     public List<StackFrame> frames(int start, int length)
402                               throws IncompatibleThreadStateException  {
403         if (length < 0) {
404             throw new IndexOutOfBoundsException(
405                 "length must be greater than or equal to zero");
406         }
407         return privateFrames(start, length);
408     }
409 
410     /**
411      * Private version of frames() allows "-1" to specify all
412      * remaining frames.
413      */
414     synchronized private List<StackFrame> privateFrames(int start, int length)
415                               throws IncompatibleThreadStateException  {
416 
417         // Lock must be held while creating stack frames so if that two threads
418         // do this at the same time, one won't clobber the subset created by the other.
419         LocalCache snapshot = localCache;
420         try {
421             if (snapshot.frames == null || !isSubrange(snapshot, start, length)) {
422                 JDWP.ThreadReference.Frames.Frame[] jdwpFrames
423                     = JDWP.ThreadReference.Frames.
424                     process(vm, this, start, length).frames;
425                 int count = jdwpFrames.length;
426                 snapshot.frames = new ArrayList<>(count);
427 
428                 for (int i = 0; i<count; i++) {
429                     if (jdwpFrames[i].location == null) {
430                         throw new InternalException("Invalid frame location");
431                     }
432                     StackFrame frame = new StackFrameImpl(vm, this,
433                                                           jdwpFrames[i].frameID,
434                                                           jdwpFrames[i].location);
435                     // Add to the frame list
436                     snapshot.frames.add(frame);
437                 }
438                 snapshot.framesStart = start;
439                 snapshot.framesLength = length;
440                 return Collections.unmodifiableList(snapshot.frames);
441             } else {
442                 int fromIndex = start - snapshot.framesStart;
443                 int toIndex;
444                 if (length == -1) {
445                     toIndex = snapshot.frames.size() - fromIndex;
446                 } else {
447                     toIndex = fromIndex + length;
448                 }
449                 return Collections.unmodifiableList(snapshot.frames.subList(fromIndex, toIndex));
450             }
451         } catch (JDWPException exc) {
452             switch (exc.errorCode()) {
453             case JDWP.Error.THREAD_NOT_SUSPENDED:
454             case JDWP.Error.INVALID_THREAD:   /* zombie */
455                 throw new IncompatibleThreadStateException();
456             default:
457                 throw exc.toJDIException();
458             }
459         }
460     }
461 
462     public List<ObjectReference> ownedMonitors()  throws IncompatibleThreadStateException  {
463         LocalCache snapshot = localCache;
464         try {
465             if (snapshot.ownedMonitors == null) {
466                 snapshot.ownedMonitors = Arrays.asList(
467                                  (ObjectReference[])JDWP.ThreadReference.OwnedMonitors.
468                                          process(vm, this).owned);
469                 if ((vm.traceFlags & VirtualMachine.TRACE_OBJREFS) != 0) {
470                     vm.printTrace(description() +
471                                   " temporarily caching owned monitors"+
472                                   " (count = " + snapshot.ownedMonitors.size() + ")");
473                 }
474             }
475         } catch (JDWPException exc) {
476             switch (exc.errorCode()) {
477             case JDWP.Error.THREAD_NOT_SUSPENDED:
478             case JDWP.Error.INVALID_THREAD:   /* zombie */
479                 throw new IncompatibleThreadStateException();
480             default:
481                 throw exc.toJDIException();
482             }
483         }
484         return snapshot.ownedMonitors;
485     }
486 
487     public ObjectReference currentContendedMonitor()
488                               throws IncompatibleThreadStateException  {
489         LocalCache snapshot = localCache;
490         try {
491             if (snapshot.contendedMonitor == null &&
492                 !snapshot.triedCurrentContended) {
493                 snapshot.contendedMonitor = JDWP.ThreadReference.CurrentContendedMonitor.
494                     process(vm, this).monitor;
495                 snapshot.triedCurrentContended = true;
496                 if ((snapshot.contendedMonitor != null) &&
497                     ((vm.traceFlags & VirtualMachine.TRACE_OBJREFS) != 0)) {
498                     vm.printTrace(description() +
499                                   " temporarily caching contended monitor"+
500                                   " (id = " + snapshot.contendedMonitor.uniqueID() + ")");
501                 }
502             }
503         } catch (JDWPException exc) {
504             switch (exc.errorCode()) {
505             case JDWP.Error.THREAD_NOT_SUSPENDED:
506             case JDWP.Error.INVALID_THREAD:   /* zombie */
507                 throw new IncompatibleThreadStateException();
508             default:
509                 throw exc.toJDIException();
510             }
511         }
512         return snapshot.contendedMonitor;
513     }
514 
515     public List<MonitorInfo> ownedMonitorsAndFrames()  throws IncompatibleThreadStateException  {
516         LocalCache snapshot = localCache;
517         try {
518             if (snapshot.ownedMonitorsInfo == null) {
519                 JDWP.ThreadReference.OwnedMonitorsStackDepthInfo.monitor[] minfo;
520                 minfo = JDWP.ThreadReference.OwnedMonitorsStackDepthInfo.process(vm, this).owned;
521 
522                 snapshot.ownedMonitorsInfo = new ArrayList<>(minfo.length);
523 
524                 for (int i=0; i < minfo.length; i++) {
525                     MonitorInfo mon = new MonitorInfoImpl(vm, minfo[i].monitor, this, minfo[i].stack_depth);
526                     snapshot.ownedMonitorsInfo.add(mon);
527                 }
528 
529                 if ((vm.traceFlags & VirtualMachine.TRACE_OBJREFS) != 0) {
530                     vm.printTrace(description() +
531                                   " temporarily caching owned monitors"+
532                                   " (count = " + snapshot.ownedMonitorsInfo.size() + ")");
533                     }
534                 }
535 
536         } catch (JDWPException exc) {
537             switch (exc.errorCode()) {
538             case JDWP.Error.THREAD_NOT_SUSPENDED:
539             case JDWP.Error.INVALID_THREAD:   /* zombie */
540                 throw new IncompatibleThreadStateException();
541             default:
542                 throw exc.toJDIException();
543             }
544         }
545         return snapshot.ownedMonitorsInfo;
546     }
547 
548     public void popFrames(StackFrame frame) throws IncompatibleThreadStateException {
549         // Note that interface-wise this functionality belongs
550         // here in ThreadReference, but implementation-wise it
551         // belongs in StackFrame, so we just forward it.
552         if (!frame.thread().equals(this)) {
553             throw new IllegalArgumentException("frame does not belong to this thread");
554         }
555         if (!vm.canPopFrames()) {
556             throw new UnsupportedOperationException(
557                 "target does not support popping frames");
558         }
559         ((StackFrameImpl)frame).pop();
560     }
561 
562     public void forceEarlyReturn(Value returnValue) throws InvalidTypeException,
563                                                            ClassNotLoadedException,
564                                              IncompatibleThreadStateException {
565         if (!vm.canForceEarlyReturn()) {
566             throw new UnsupportedOperationException(
567                 "target does not support the forcing of a method to return early");
568         }
569 
570         validateMirrorOrNull(returnValue);
571 
572         StackFrameImpl sf;
573         try {
574            sf = (StackFrameImpl)frame(0);
575         } catch (IndexOutOfBoundsException exc) {
576            throw new InvalidStackFrameException("No more frames on the stack");
577         }
578         sf.validateStackFrame();
579         MethodImpl meth = (MethodImpl)sf.location().method();
580         ValueImpl convertedValue  = ValueImpl.prepareForAssignment(returnValue,
581                                                                    meth.getReturnValueContainer());
582 
583         try {
584             JDWP.ThreadReference.ForceEarlyReturn.process(vm, this, convertedValue);
585         } catch (JDWPException exc) {
586             switch (exc.errorCode()) {
587             case JDWP.Error.OPAQUE_FRAME:
588                 throw new NativeMethodException();
589             case JDWP.Error.THREAD_NOT_SUSPENDED:
590                 throw new IncompatibleThreadStateException(
591                          "Thread not suspended");
592             case JDWP.Error.THREAD_NOT_ALIVE:
593                 throw new IncompatibleThreadStateException(
594                                      "Thread has not started or has finished");
595             case JDWP.Error.NO_MORE_FRAMES:
596                 throw new InvalidStackFrameException(
597                          "No more frames on the stack");
598             default:
599                 throw exc.toJDIException();
600             }
601         }
602     }
603 
604     @Override
605     public boolean isVirtual() {
606         if (isVirtualCached) {
607             return isVirtual;
608         } else {
609             boolean result;
610             try {
611                 result = JDWP.ThreadReference.IsVirtual.process(vm, this).isVirtual;
612             } catch (JDWPException exc) {
613                 throw exc.toJDIException();
614             }
615             isVirtual = result;
616             isVirtualCached = true;
617             return result;
618         }
619     }
620 
621     public String toString() {
622         return "instance of " + referenceType().name() +
623                "(name='" + name() + "', " + "id=" + uniqueID() + ")";
624     }
625 
626     byte typeValueKey() {
627         return JDWP.Tag.THREAD;
628     }
629 
630     void addListener(ThreadListener listener) {
631         synchronized (vm.state()) {
632             listeners.add(new WeakReference<>(listener));
633         }
634     }
635 
636     void removeListener(ThreadListener listener) {
637         synchronized (vm.state()) {
638             Iterator<WeakReference<ThreadListener>> iter = listeners.iterator();
639             while (iter.hasNext()) {
640                 WeakReference<ThreadListener> ref = iter.next();
641                 if (listener.equals(ref.get())) {
642                     iter.remove();
643                     break;
644                 }
645             }
646         }
647     }
648 
649     /**
650      * Propagate the thread state change information
651      * to registered listeners.
652      * Must be entered while synchronized on vm.state()
653      */
654     private void processThreadAction(ThreadAction action) {
655         synchronized (vm.state()) {
656             Iterator<WeakReference<ThreadListener>> iter = listeners.iterator();
657             while (iter.hasNext()) {
658                 WeakReference<ThreadListener> ref = iter.next();
659                 ThreadListener listener = ref.get();
660                 if (listener != null) {
661                     switch (action.id()) {
662                         case ThreadAction.THREAD_RESUMABLE:
663                             if (!listener.threadResumable(action)) {
664                                 iter.remove();
665                             }
666                             break;
667                     }
668                 } else {
669                     // Listener is unreachable; clean up
670                     iter.remove();
671                 }
672             }
673 
674             // Discard our local cache
675             resetLocalCache();
676         }
677     }
678 }