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