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 }