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