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 }