src/jdk.management.jfr/share/classes/jdk/management/jfr/FlightRecorderMXBeanImpl.java
changeset 50113 caf115bb98ad
child 58480 8ca46e186a63
equal deleted inserted replaced
50112:7a2a740815b7 50113:caf115bb98ad
       
     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 }