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