1 /*
   2  * Copyright (c) 2010, 2016, 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.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /*
  25  * @test
  26  * @bug 6957378
  27  * @summary Test that a listener can be removed remotely from an MBean that no longer exists.
  28  * @modules java.management/javax.management.remote.rmi:open
  29  *          java.management/com.sun.jmx.remote.internal:+open
  30  * @author Eamonn McManus
  31  * @run main/othervm -XX:+UsePerfData DeadListenerTest
  32  */
  33 
  34 import com.sun.jmx.remote.internal.ServerNotifForwarder;
  35 import java.io.IOException;
  36 import java.lang.management.ManagementFactory;
  37 import java.lang.ref.WeakReference;
  38 import java.lang.reflect.Field;
  39 import java.lang.reflect.Method;
  40 import java.util.ArrayList;
  41 import java.util.HashMap;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.Set;
  45 import java.util.concurrent.atomic.AtomicInteger;
  46 import javax.management.ListenerNotFoundException;
  47 import javax.management.MBeanServer;
  48 import javax.management.MBeanServerConnection;
  49 import javax.management.MBeanServerDelegate;
  50 import javax.management.Notification;
  51 import javax.management.NotificationBroadcasterSupport;
  52 import javax.management.NotificationFilterSupport;
  53 import javax.management.NotificationListener;
  54 import javax.management.ObjectName;
  55 import javax.management.remote.JMXConnector;
  56 import javax.management.remote.JMXConnectorFactory;
  57 import javax.management.remote.JMXServiceURL;
  58 import javax.management.remote.rmi.RMIConnection;
  59 import javax.management.remote.rmi.RMIConnectionImpl;
  60 import javax.management.remote.rmi.RMIConnectorServer;
  61 import javax.management.remote.rmi.RMIJRMPServerImpl;
  62 import javax.security.auth.Subject;
  63 
  64 public class DeadListenerTest {
  65     public static void main(String[] args) throws Exception {
  66         final ObjectName delegateName = MBeanServerDelegate.DELEGATE_NAME;
  67 
  68         MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
  69         Noddy mbean = new Noddy();
  70         ObjectName name = new ObjectName("d:k=v");
  71         mbs.registerMBean(mbean, name);
  72 
  73         JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///");
  74         SnoopRMIServerImpl rmiServer = new SnoopRMIServerImpl();
  75         RMIConnectorServer cs = new RMIConnectorServer(url, null, rmiServer, mbs);
  76         cs.start();
  77         JMXServiceURL addr = cs.getAddress();
  78         assertTrue("No connections in new connector server", rmiServer.connections.isEmpty());
  79 
  80         JMXConnector cc = JMXConnectorFactory.connect(addr);
  81         MBeanServerConnection mbsc = cc.getMBeanServerConnection();
  82         assertTrue("One connection on server after client connect", rmiServer.connections.size() == 1);
  83         RMIConnectionImpl connection = rmiServer.connections.get(0);
  84         Method getServerNotifFwdM = RMIConnectionImpl.class.getDeclaredMethod("getServerNotifFwd");
  85         getServerNotifFwdM.setAccessible(true);
  86         ServerNotifForwarder serverNotifForwarder = (ServerNotifForwarder) getServerNotifFwdM.invoke(connection);
  87         Field listenerMapF = ServerNotifForwarder.class.getDeclaredField("listenerMap");
  88         listenerMapF.setAccessible(true);
  89         @SuppressWarnings("unchecked")
  90         Map<ObjectName, Set<?>> listenerMap = (Map<ObjectName, Set<?>>) listenerMapF.get(serverNotifForwarder);
  91         assertTrue("Server listenerMap initially empty", mapWithoutKey(listenerMap, delegateName).isEmpty());
  92 
  93         final AtomicInteger count1Val = new AtomicInteger();
  94         CountListener count1 = new CountListener(count1Val);
  95         mbsc.addNotificationListener(name, count1, null, null);
  96         WeakReference<CountListener> count1Ref = new WeakReference<>(count1);
  97         count1 = null;
  98 
  99         final AtomicInteger count2Val = new AtomicInteger();
 100         CountListener count2 = new CountListener(count2Val);
 101         NotificationFilterSupport dummyFilter = new NotificationFilterSupport();
 102         dummyFilter.enableType("");
 103         mbsc.addNotificationListener(name, count2, dummyFilter, "noddy");
 104         WeakReference<CountListener> count2Ref = new WeakReference<>(count2);
 105         count2 = null;
 106 
 107         assertTrue("One entry in listenerMap for two listeners on same MBean", mapWithoutKey(listenerMap, delegateName).size() == 1);
 108         Set<?> set = listenerMap.get(name);
 109         assertTrue("Set in listenerMap for MBean has two elements", set != null && set.size() == 2);
 110 
 111         assertTrue("Initial value of count1 == 0", count1Val.get() == 0);
 112         assertTrue("Initial value of count2 == 0", count2Val.get() == 0);
 113 
 114         Notification notif = new Notification("type", name, 0);
 115 
 116         mbean.sendNotification(notif);
 117 
 118         // Make sure notifs are working normally.
 119         while ((count1Val.get() != 1 || count2Val.get() != 1) ) {
 120             Thread.sleep(20);
 121         }
 122         assertTrue("New value of count1 == 1", count1Val.get() == 1);
 123         assertTrue("Initial value of count2 == 1", count2Val.get() == 1);
 124 
 125         // Make sure that removing a nonexistent listener from an existent MBean produces ListenerNotFoundException
 126         CountListener count3 = new CountListener();
 127         try {
 128             mbsc.removeNotificationListener(name, count3);
 129             assertTrue("Remove of nonexistent listener succeeded but should not have", false);
 130         } catch (ListenerNotFoundException e) {
 131             // OK: expected
 132         }
 133 
 134         // Make sure that removing a nonexistent listener from a nonexistent MBean produces ListenerNotFoundException
 135         ObjectName nonexistent = new ObjectName("foo:bar=baz");
 136         assertTrue("Nonexistent is nonexistent", !mbs.isRegistered(nonexistent));
 137         try {
 138             mbsc.removeNotificationListener(nonexistent, count3);
 139             assertTrue("Remove of listener from nonexistent MBean succeeded but should not have", false);
 140         } catch (ListenerNotFoundException e) {
 141             // OK: expected
 142         }
 143 
 144         // Now unregister our MBean, and check that notifs it sends no longer go anywhere.
 145         mbs.unregisterMBean(name);
 146         mbean.sendNotification(notif);
 147         Thread.sleep(200);
 148 
 149         assertTrue("New value of count1 == 1", count1Val.get() == 1);
 150         assertTrue("Initial value of count2 == 1", count2Val.get() == 1);
 151 
 152         // wait for the listener cleanup to take place upon processing notifications
 153         int countdown = 50; // waiting max. 5 secs
 154         while (countdown-- > 0 &&
 155                 (count1Ref.get() != null ||
 156                  count2Ref.get() != null)) {
 157             System.gc();
 158             Thread.sleep(100);
 159             System.gc();
 160         }
 161         // listener has been removed or the wait has timed out
 162 
 163         assertTrue("count1 notification listener has not been cleaned up", count1Ref.get() == null);
 164         assertTrue("count2 notification listener has not been cleaned up", count2Ref.get() == null);
 165 
 166         // Check that there is no trace of the listeners any more in ServerNotifForwarder.listenerMap.
 167         // THIS DEPENDS ON JMX IMPLEMENTATION DETAILS.
 168         // If the JMX implementation changes, the code here may have to change too.
 169         Set<?> setForUnreg = listenerMap.get(name);
 170         assertTrue("No trace of unregistered MBean: " + setForUnreg, setForUnreg == null);
 171     }
 172 
 173     private static <K, V> Map<K, V> mapWithoutKey(Map<K, V> map, K key) {
 174         Map<K, V> copy = new HashMap<K, V>(map);
 175         copy.remove(key);
 176         return copy;
 177     }
 178 
 179     public static interface NoddyMBean {}
 180 
 181     public static class Noddy extends NotificationBroadcasterSupport implements NoddyMBean {}
 182 
 183     public static class CountListener implements NotificationListener {
 184         final AtomicInteger count;
 185 
 186         public CountListener(AtomicInteger i) {
 187             count = i;
 188         }
 189 
 190         public CountListener() {
 191             this.count = new AtomicInteger();
 192         }
 193 
 194         int count() {
 195             return count.get();
 196         }
 197 
 198         public void handleNotification(Notification notification, Object handback) {
 199             count.incrementAndGet();
 200         }
 201     }
 202 
 203     private static void assertTrue(String what, boolean cond) {
 204         if (!cond) {
 205             throw new AssertionError("Assertion failed: " + what);
 206         }
 207     }
 208 
 209     private static class SnoopRMIServerImpl extends RMIJRMPServerImpl {
 210         final List<RMIConnectionImpl> connections = new ArrayList<RMIConnectionImpl>();
 211         SnoopRMIServerImpl() throws IOException {
 212             super(0, null, null, null);
 213         }
 214 
 215         @Override
 216         protected RMIConnection makeClient(String id, Subject subject) throws IOException {
 217             RMIConnectionImpl conn = (RMIConnectionImpl) super.makeClient(id, subject);
 218             connections.add(conn);
 219             return conn;
 220         }
 221     }
 222 }