1 /* 2 * Copyright (c) 2016, 2019, 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 jdk.management.jfr; 27 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.io.StringReader; 31 import java.nio.file.Paths; 32 import java.security.AccessControlContext; 33 import java.security.AccessController; 34 import java.security.PrivilegedAction; 35 import java.text.ParseException; 36 import java.time.Instant; 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.Collections; 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.Objects; 44 import java.util.concurrent.ConcurrentHashMap; 45 import java.util.concurrent.CopyOnWriteArrayList; 46 import java.util.concurrent.atomic.AtomicLong; 47 import java.util.function.Consumer; 48 import java.util.function.Function; 49 import java.util.function.Predicate; 50 51 import javax.management.AttributeChangeNotification; 52 import javax.management.AttributeNotFoundException; 53 import javax.management.ListenerNotFoundException; 54 import javax.management.MBeanException; 55 import javax.management.MBeanNotificationInfo; 56 import javax.management.Notification; 57 import javax.management.NotificationBroadcasterSupport; 58 import javax.management.NotificationEmitter; 59 import javax.management.NotificationFilter; 60 import javax.management.NotificationListener; 61 import javax.management.ObjectName; 62 import javax.management.ReflectionException; 63 import javax.management.StandardEmitterMBean; 64 65 import jdk.jfr.Configuration; 66 import jdk.jfr.EventType; 67 import jdk.jfr.FlightRecorder; 68 import jdk.jfr.FlightRecorderListener; 69 import jdk.jfr.FlightRecorderPermission; 70 import jdk.jfr.Recording; 71 import jdk.jfr.RecordingState; 72 import jdk.jfr.internal.management.ManagementSupport; 73 74 public final class FlightRecorderMXBeanImpl extends StandardEmitterMBean implements FlightRecorderMXBean, NotificationEmitter { 75 76 final class MXBeanListener implements FlightRecorderListener { 77 private final NotificationListener listener; 78 private final NotificationFilter filter; 79 private final Object handback; 80 private final AccessControlContext context; 81 82 public MXBeanListener(NotificationListener listener, NotificationFilter filter, Object handback) { 83 this.context = AccessController.getContext(); 84 this.listener = listener; 85 this.filter = filter; 86 this.handback = handback; 87 } 88 89 public void recordingStateChanged(Recording recording) { 90 AccessController.doPrivileged(new PrivilegedAction<Void>() { 91 @Override 92 public Void run() { 93 sendNotification(createNotication(recording)); 94 return null; 95 } 96 }, context); 97 } 98 } 99 100 private static final String ATTRIBUTE_RECORDINGS = "Recordings"; 101 private static final String OPTION_MAX_SIZE = "maxSize"; 102 private static final String OPTION_MAX_AGE = "maxAge"; 103 private static final String OPTION_NAME = "name"; 104 private static final String OPTION_DISK = "disk"; 105 private static final String OPTION_DUMP_ON_EXIT = "dumpOnExit"; 106 private static final String OPTION_DURATION = "duration"; 107 private static final List<String> OPTIONS = Arrays.asList(new String[] { OPTION_DUMP_ON_EXIT, OPTION_DURATION, OPTION_NAME, OPTION_MAX_AGE, OPTION_MAX_SIZE, OPTION_DISK, }); 108 private final StreamManager streamHandler = new StreamManager(); 109 private final Map<Long, Object> changes = new ConcurrentHashMap<>(); 110 private final AtomicLong sequenceNumber = new AtomicLong(); 111 private final List<MXBeanListener> listeners = new CopyOnWriteArrayList<>(); 112 private FlightRecorder recorder; 113 114 public FlightRecorderMXBeanImpl() { 115 super(FlightRecorderMXBean.class, true, new NotificationBroadcasterSupport(createNotificationInfo())); 116 } 117 118 @Override 119 public void startRecording(long id) { 120 MBeanUtils.checkControl(); 121 getExistingRecording(id).start(); 122 } 123 124 @Override 125 public boolean stopRecording(long id) { 126 MBeanUtils.checkControl(); 127 return getExistingRecording(id).stop(); 128 } 129 130 @Override 131 public void closeRecording(long id) { 132 MBeanUtils.checkControl(); 133 getExistingRecording(id).close(); 134 } 135 136 @Override 137 public long openStream(long id, Map<String, String> options) throws IOException { 138 MBeanUtils.checkControl(); 139 if (!FlightRecorder.isInitialized()) { 140 throw new IllegalArgumentException("No recording available with id " + id); 141 } 142 // Make local copy to prevent concurrent modification 143 Map<String, String> s = options == null ? new HashMap<>() : new HashMap<>(options); 144 Instant starttime = MBeanUtils.parseTimestamp(s.get("startTime"), Instant.MIN); 145 Instant endtime = MBeanUtils.parseTimestamp(s.get("endTime"), Instant.MAX); 146 int blockSize = MBeanUtils.parseBlockSize(s.get("blockSize"), StreamManager.DEFAULT_BLOCK_SIZE); 147 InputStream is = getExistingRecording(id).getStream(starttime, endtime); 148 if (is == null) { 149 throw new IOException("No recording data available"); 150 } 151 return streamHandler.create(is, blockSize).getId(); 152 } 153 154 @Override 155 public void closeStream(long streamIdentifier) throws IOException { 156 MBeanUtils.checkControl(); 157 streamHandler.getStream(streamIdentifier).close(); 158 } 159 160 @Override 161 public byte[] readStream(long streamIdentifier) throws IOException { 162 MBeanUtils.checkMonitor(); 163 return streamHandler.getStream(streamIdentifier).read(); 164 } 165 166 @Override 167 public List<RecordingInfo> getRecordings() { 168 MBeanUtils.checkMonitor(); 169 if (!FlightRecorder.isInitialized()) { 170 return Collections.emptyList(); 171 } 172 return MBeanUtils.transformList(getRecorder().getRecordings(), RecordingInfo::new); 173 } 174 175 @Override 176 public List<ConfigurationInfo> getConfigurations() { 177 MBeanUtils.checkMonitor(); 178 return MBeanUtils.transformList(Configuration.getConfigurations(), ConfigurationInfo::new); 179 } 180 181 @Override 182 public List<EventTypeInfo> getEventTypes() { 183 MBeanUtils.checkMonitor(); 184 List<EventType> eventTypes = AccessController.doPrivileged(new PrivilegedAction<List<EventType>>() { 185 @Override 186 public List<EventType> run() { 187 return ManagementSupport.getEventTypes(); 188 } 189 }, null, new FlightRecorderPermission("accessFlightRecorder")); 190 191 return MBeanUtils.transformList(eventTypes, EventTypeInfo::new); 192 } 193 194 @Override 195 public Map<String, String> getRecordingSettings(long recording) throws IllegalArgumentException { 196 MBeanUtils.checkMonitor(); 197 return getExistingRecording(recording).getSettings(); 198 } 199 200 @Override 201 public void setRecordingSettings(long recording, Map<String, String> values) throws IllegalArgumentException { 202 Objects.requireNonNull(values); 203 MBeanUtils.checkControl(); 204 getExistingRecording(recording).setSettings(values); 205 } 206 207 @Override 208 public long newRecording() { 209 MBeanUtils.checkControl(); 210 getRecorder(); // ensure notification listener is setup 211 return AccessController.doPrivileged(new PrivilegedAction<Recording>() { 212 @Override 213 public Recording run() { 214 return new Recording(); 215 } 216 }, null, new FlightRecorderPermission("accessFlightRecorder")).getId(); 217 } 218 219 @Override 220 public long takeSnapshot() { 221 MBeanUtils.checkControl(); 222 return getRecorder().takeSnapshot().getId(); 223 } 224 225 @Override 226 public void setConfiguration(long recording, String configuration) throws IllegalArgumentException { 227 Objects.requireNonNull(configuration); 228 MBeanUtils.checkControl(); 229 try { 230 Configuration c = Configuration.create(new StringReader(configuration)); 231 getExistingRecording(recording).setSettings(c.getSettings()); 232 } catch (IOException | ParseException e) { 233 throw new IllegalArgumentException("Could not parse configuration", e); 234 } 235 } 236 237 @Override 238 public void setPredefinedConfiguration(long recording, String configurationName) throws IllegalArgumentException { 239 Objects.requireNonNull(configurationName); 240 MBeanUtils.checkControl(); 241 Recording r = getExistingRecording(recording); 242 for (Configuration c : Configuration.getConfigurations()) { 243 if (c.getName().equals(configurationName)) { 244 r.setSettings(c.getSettings()); 245 return; 246 } 247 } 248 throw new IllegalArgumentException("Could not find configuration with name " + configurationName); 249 } 250 251 @Override 252 public void copyTo(long recording, String path) throws IOException { 253 Objects.requireNonNull(path); 254 MBeanUtils.checkControl(); 255 getExistingRecording(recording).dump(Paths.get(path)); 256 } 257 258 @Override 259 public void setRecordingOptions(long recording, Map<String, String> options) throws IllegalArgumentException { 260 Objects.requireNonNull(options); 261 MBeanUtils.checkControl(); 262 // Make local copy to prevent concurrent modification 263 Map<String, String> ops = new HashMap<String, String>(options); 264 for (Map.Entry<String, String> entry : ops.entrySet()) { 265 Object key = entry.getKey(); 266 Object value = entry.getValue(); 267 if (!(key instanceof String)) { 268 throw new IllegalArgumentException("Option key must not be null, or other type than " + String.class); 269 } 270 if (!OPTIONS.contains(key)) { 271 throw new IllegalArgumentException("Unknown recording option: " + key + ". Valid options are " + OPTIONS + "."); 272 } 273 if (value != null && !(value instanceof String)) { 274 throw new IllegalArgumentException("Incorrect value for option " + key + ". Values must be of type " + String.class + " ."); 275 } 276 } 277 278 Recording r = getExistingRecording(recording); 279 validateOption(ops, OPTION_DUMP_ON_EXIT, MBeanUtils::booleanValue); 280 validateOption(ops, OPTION_DISK, MBeanUtils::booleanValue); 281 validateOption(ops, OPTION_NAME, Function.identity()); 282 validateOption(ops, OPTION_MAX_AGE, MBeanUtils::duration); 283 validateOption(ops, OPTION_MAX_SIZE, MBeanUtils::size); 284 validateOption(ops, OPTION_DURATION, MBeanUtils::duration); 285 286 // All OK, now set them.atomically 287 setOption(ops, OPTION_DUMP_ON_EXIT, "false", MBeanUtils::booleanValue, x -> r.setDumpOnExit(x)); 288 setOption(ops, OPTION_DISK, "true", MBeanUtils::booleanValue, x -> r.setToDisk(x)); 289 setOption(ops, OPTION_NAME, String.valueOf(r.getId()), Function.identity(), x -> r.setName(x)); 290 setOption(ops, OPTION_MAX_AGE, null, MBeanUtils::duration, x -> r.setMaxAge(x)); 291 setOption(ops, OPTION_MAX_SIZE, "0", MBeanUtils::size, x -> r.setMaxSize(x)); 292 setOption(ops, OPTION_DURATION, null, MBeanUtils::duration, x -> r.setDuration(x)); 293 } 294 295 @Override 296 public Map<String, String> getRecordingOptions(long recording) throws IllegalArgumentException { 297 MBeanUtils.checkMonitor(); 298 Recording r = getExistingRecording(recording); 299 Map<String, String> options = new HashMap<>(10); 300 options.put(OPTION_DUMP_ON_EXIT, String.valueOf(r.getDumpOnExit())); 301 options.put(OPTION_DISK, String.valueOf(r.isToDisk())); 302 options.put(OPTION_NAME, String.valueOf(r.getName())); 303 options.put(OPTION_MAX_AGE, ManagementSupport.formatTimespan(r.getMaxAge(), " ")); 304 Long maxSize = r.getMaxSize(); 305 options.put(OPTION_MAX_SIZE, String.valueOf(maxSize == null ? "0" : maxSize.toString())); 306 options.put(OPTION_DURATION, ManagementSupport.formatTimespan(r.getDuration(), " ")); 307 return options; 308 } 309 310 @Override 311 public long cloneRecording(long id, boolean stop) throws IllegalStateException, SecurityException { 312 MBeanUtils.checkControl(); 313 return getRecording(id).copy(stop).getId(); 314 } 315 316 @Override 317 public ObjectName getObjectName() { 318 return MBeanUtils.createObjectName(); 319 } 320 321 private Recording getExistingRecording(long id) { 322 if (FlightRecorder.isInitialized()) { 323 Recording recording = getRecording(id); 324 if (recording != null) { 325 return recording; 326 } 327 } 328 throw new IllegalArgumentException("No recording available with id " + id); 329 } 330 331 private Recording getRecording(long id) { 332 List<Recording> recs = getRecorder().getRecordings(); 333 return recs.stream().filter(r -> r.getId() == id).findFirst().orElse(null); 334 } 335 336 private static <T, U> void setOption(Map<String, String> options, String name, String defaultValue, Function<String, U> converter, Consumer<U> setter) { 337 if (!options.containsKey(name)) { 338 return; 339 } 340 String v = options.get(name); 341 if (v == null) { 342 v = defaultValue; 343 } 344 try { 345 setter.accept(converter.apply(v)); 346 } catch (IllegalArgumentException iae) { 347 throw new IllegalArgumentException("Not a valid value for option '" + name + "'. " + iae.getMessage()); 348 } 349 } 350 351 private static <T, U> void validateOption(Map<String, String> options, String name, Function<String, U> validator) { 352 try { 353 String v = options.get(name); 354 if (v == null) { 355 return; // OK, will set default 356 } 357 validator.apply(v); 358 } catch (IllegalArgumentException iae) { 359 throw new IllegalArgumentException("Not a valid value for option '" + name + "'. " + iae.getMessage()); 360 } 361 } 362 363 private FlightRecorder getRecorder() throws SecurityException { 364 // Synchronize on some private object that is always available 365 synchronized (streamHandler) { 366 if (recorder == null) { 367 recorder = AccessController.doPrivileged(new PrivilegedAction<FlightRecorder>() { 368 @Override 369 public FlightRecorder run() { 370 return FlightRecorder.getFlightRecorder(); 371 } 372 }, null, new FlightRecorderPermission("accessFlightRecorder")); 373 } 374 return recorder; 375 } 376 } 377 378 private static MBeanNotificationInfo[] createNotificationInfo() { 379 String[] types = new String[] { AttributeChangeNotification.ATTRIBUTE_CHANGE }; 380 String name = AttributeChangeNotification.class.getName(); 381 String description = "Notifies if the RecordingState has changed for one of the recordings, for example if a recording starts or stops"; 382 MBeanNotificationInfo info = new MBeanNotificationInfo(types, name, description); 383 return new MBeanNotificationInfo[] { info }; 384 } 385 386 @Override 387 public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) { 388 MXBeanListener mxbeanListener = new MXBeanListener(listener, filter, handback); 389 listeners.add(mxbeanListener); 390 AccessController.doPrivileged(new PrivilegedAction<Void>() { 391 @Override 392 public Void run(){ 393 FlightRecorder.addListener(mxbeanListener); 394 return null; 395 } 396 }, null, new FlightRecorderPermission("accessFlightRecorder")); 397 super.addNotificationListener(listener, filter, handback); 398 } 399 400 @Override 401 public void removeNotificationListener(NotificationListener listener) throws ListenerNotFoundException { 402 removeListeners( x -> listener == x.listener); 403 super.removeNotificationListener(listener); 404 } 405 406 @Override 407 public void removeNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws ListenerNotFoundException { 408 removeListeners( x -> listener == x.listener && filter == x.filter && handback == x.handback); 409 super.removeNotificationListener(listener, filter, handback); 410 } 411 412 private void removeListeners(Predicate<MXBeanListener> p) { 413 List<MXBeanListener> toBeRemoved = new ArrayList<>(listeners.size()); 414 for (MXBeanListener l : listeners) { 415 if (p.test(l)) { 416 toBeRemoved.add(l); 417 FlightRecorder.removeListener(l); 418 } 419 } 420 listeners.removeAll(toBeRemoved); 421 } 422 423 private Notification createNotication(Recording recording) { 424 try { 425 Long id = recording.getId(); 426 Object oldValue = changes.get(recording.getId()); 427 Object newValue = getAttribute(ATTRIBUTE_RECORDINGS); 428 if (recording.getState() != RecordingState.CLOSED) { 429 changes.put(id, newValue); 430 } else { 431 changes.remove(id); 432 } 433 return new AttributeChangeNotification(getObjectName(), sequenceNumber.incrementAndGet(), System.currentTimeMillis(), "Recording " + recording.getName() + " is " 434 + recording.getState(), ATTRIBUTE_RECORDINGS, newValue.getClass().getName(), oldValue, newValue); 435 } catch (AttributeNotFoundException | MBeanException | ReflectionException e) { 436 throw new RuntimeException("Could not create notifcation for FlightRecorderMXBean. " + e.getMessage(), e); 437 } 438 } 439 }