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 
MXBeanListener(NotificationListener listener, NotificationFilter filter, Object handback)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 
recordingStateChanged(Recording recording)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 
FlightRecorderMXBeanImpl()115     FlightRecorderMXBeanImpl() {
116         super(FlightRecorderMXBean.class, true, new NotificationBroadcasterSupport(createNotificationInfo()));
117     }
118 
119     @Override
startRecording(long id)120     public void startRecording(long id) {
121         MBeanUtils.checkControl();
122         getExistingRecording(id).start();
123     }
124 
125     @Override
stopRecording(long id)126     public boolean stopRecording(long id) {
127         MBeanUtils.checkControl();
128         return getExistingRecording(id).stop();
129     }
130 
131     @Override
closeRecording(long id)132     public void closeRecording(long id) {
133         MBeanUtils.checkControl();
134         getExistingRecording(id).close();
135     }
136 
137     @Override
openStream(long id, Map<String, String> options)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
closeStream(long streamIdentifier)156     public void closeStream(long streamIdentifier) throws IOException {
157         MBeanUtils.checkControl();
158         streamHandler.getStream(streamIdentifier).close();
159     }
160 
161     @Override
readStream(long streamIdentifier)162     public byte[] readStream(long streamIdentifier) throws IOException {
163         MBeanUtils.checkMonitor();
164         return streamHandler.getStream(streamIdentifier).read();
165     }
166 
167     @Override
getRecordings()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
getConfigurations()177     public List<ConfigurationInfo> getConfigurations() {
178         MBeanUtils.checkMonitor();
179         return MBeanUtils.transformList(Configuration.getConfigurations(), ConfigurationInfo::new);
180     }
181 
182     @Override
getEventTypes()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
getRecordingSettings(long recording)196     public Map<String, String> getRecordingSettings(long recording) throws IllegalArgumentException {
197         MBeanUtils.checkMonitor();
198         return getExistingRecording(recording).getSettings();
199     }
200 
201     @Override
setRecordingSettings(long recording, Map<String, String> values)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
newRecording()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 }
441