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