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