1 /*
2  * Copyright (c) 2016, 2020, 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.jfr.internal;
27 
28 import java.security.AccessControlContext;
29 import java.security.AccessController;
30 import java.security.PrivilegedAction;
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.Iterator;
34 import java.util.List;
35 import java.util.Objects;
36 import java.util.concurrent.CopyOnWriteArrayList;
37 import java.util.function.Predicate;
38 import jdk.jfr.Event;
39 import jdk.jfr.EventType;
40 
41 public final class RequestEngine {
42 
43     private final static JVM jvm = JVM.getJVM();
44 
45     final static class RequestHook {
46         private final Runnable hook;
47         private final PlatformEventType type;
48         private final AccessControlContext accessControllerContext;
49         private long delta;
50 
51         // Java events
RequestHook(AccessControlContext acc, PlatformEventType eventType, Runnable hook)52         private RequestHook(AccessControlContext acc, PlatformEventType eventType, Runnable hook) {
53             this.hook = hook;
54             this.type = eventType;
55             this.accessControllerContext = acc;
56         }
57 
58         // native events
RequestHook(PlatformEventType eventType)59         RequestHook(PlatformEventType eventType) {
60             this(null, eventType, null);
61         }
62 
execute()63         private void execute() {
64             try {
65                 if (accessControllerContext == null) { // native
66                     if (type.isJDK()) {
67                         hook.run();
68                     } else {
69                         jvm.emitEvent(type.getId(), JVM.counterTime(), 0);
70                     }
71                     if (Logger.shouldLog(LogTag.JFR_SYSTEM_EVENT, LogLevel.DEBUG)) {
72                         Logger.log(LogTag.JFR_SYSTEM_EVENT, LogLevel.DEBUG, "Executed periodic hook for " + type.getLogName());
73                     }
74                 } else {
75                     executeSecure();
76                 }
77             } catch (Throwable e) {
78                 // Prevent malicious user to propagate exception callback in the wrong context
79                 Logger.log(LogTag.JFR_SYSTEM_EVENT, LogLevel.WARN, "Exception occurred during execution of period hook for " + type.getLogName());
80             }
81         }
82 
executeSecure()83         private void executeSecure() {
84             AccessController.doPrivileged(new PrivilegedAction<Void>() {
85                 @Override
86                 public Void run() {
87                     try {
88                         hook.run();
89                         if (Logger.shouldLog(LogTag.JFR_EVENT, LogLevel.DEBUG)) {
90                             Logger.log(LogTag.JFR_EVENT, LogLevel.DEBUG, "Executed periodic hook for " + type.getLogName());
91                         }
92                     } catch (Throwable t) {
93                         // Prevent malicious user to propagate exception callback in the wrong context
94                         Logger.log(LogTag.JFR_EVENT, LogLevel.WARN, "Exception occurred during execution of period hook for " + type.getLogName());
95                     }
96                     return null;
97                 }
98             }, accessControllerContext);
99         }
100     }
101 
102     private final static List<RequestHook> entries = new CopyOnWriteArrayList<>();
103     private static long lastTimeMillis;
104     private static long flushInterval = Long.MAX_VALUE;
105     private static long streamDelta;
106 
addHook(AccessControlContext acc, PlatformEventType type, Runnable hook)107     public static void addHook(AccessControlContext acc, PlatformEventType type, Runnable hook) {
108         Objects.requireNonNull(acc);
109         addHookInternal(acc, type, hook);
110     }
111 
addHookInternal(AccessControlContext acc, PlatformEventType type, Runnable hook)112     private static void addHookInternal(AccessControlContext acc, PlatformEventType type, Runnable hook) {
113         RequestHook he = new RequestHook(acc, type, hook);
114         for (RequestHook e : entries) {
115             if (e.hook == hook) {
116                 throw new IllegalArgumentException("Hook has already been added");
117             }
118         }
119         he.type.setEventHook(true);
120         // Insertion takes O(2*n), could be O(1) with HashMap, but
121         // thinking is that CopyOnWriteArrayList is faster
122         // to iterate over, which will happen more over time.
123         entries.add(he);
124         logHook("Added", type);
125     }
126 
addTrustedJDKHook(Class<? extends Event> eventClass, Runnable runnable)127     public static void addTrustedJDKHook(Class<? extends Event> eventClass, Runnable runnable) {
128         if (eventClass.getClassLoader() != null) {
129             throw new SecurityException("Hook can only be registered for event classes that are loaded by the bootstrap class loader");
130         }
131         if (runnable.getClass().getClassLoader() != null) {
132             throw new SecurityException("Runnable hook class must be loaded by the bootstrap class loader");
133         }
134         EventType eType = MetadataRepository.getInstance().getEventType(eventClass);
135         PlatformEventType pType = PrivateAccess.getInstance().getPlatformEventType(eType);
136         addHookInternal(null, pType, runnable);
137     }
138 
logHook(String action, PlatformEventType type)139     private static void logHook(String action, PlatformEventType type) {
140         if (type.isJDK() || type.isJVM()) {
141             Logger.log(LogTag.JFR_SYSTEM_EVENT, LogLevel.INFO, action + " periodic hook for " + type.getLogName());
142         } else {
143             Logger.log(LogTag.JFR_EVENT, LogLevel.INFO, action + " periodic hook for " + type.getLogName());
144         }
145     }
146 
147     // Takes O(2*n), see addHook.
removeHook(Runnable hook)148     public static boolean removeHook(Runnable hook) {
149         for (RequestHook rh : entries) {
150             if (rh.hook == hook) {
151                 entries.remove(rh);
152                 rh.type.setEventHook(false);
153                 logHook("Removed", rh.type);
154                 return true;
155             }
156         }
157         return false;
158     }
159 
160     // Only to be used for JVM events. No access control contest
161     // or check if hook already exists
addHooks(List<RequestHook> newEntries)162     static void addHooks(List<RequestHook> newEntries) {
163         List<RequestHook> addEntries = new ArrayList<>();
164         for (RequestHook rh : newEntries) {
165             rh.type.setEventHook(true);
166             addEntries.add(rh);
167             logHook("Added", rh.type);
168         }
169         entries.addAll(newEntries);
170     }
171 
doChunkEnd()172     static void doChunkEnd() {
173         doChunk(x -> x.isEndChunk());
174     }
175 
doChunkBegin()176     static void doChunkBegin() {
177         doChunk(x -> x.isBeginChunk());
178     }
179 
doChunk(Predicate<PlatformEventType> predicate)180     private static void doChunk(Predicate<PlatformEventType> predicate) {
181         for (RequestHook requestHook : entries) {
182             PlatformEventType s = requestHook.type;
183             if (s.isEnabled() && predicate.test(s)) {
184                 requestHook.execute();
185             }
186         }
187     }
188 
doPeriodic()189     static long doPeriodic() {
190         return run_requests(entries);
191     }
192 
193     // code copied from native impl.
run_requests(Collection<RequestHook> entries)194     private static long run_requests(Collection<RequestHook> entries) {
195         long last = lastTimeMillis;
196         // Bug 9000556 - current time millis has rather lame resolution
197         // The use of os::elapsed_counter() is deliberate here, we don't
198         // want it exchanged for os::ft_elapsed_counter().
199         // Keeping direct call os::elapsed_counter() here for reliable
200         // real time values in order to decide when registered requestable
201         // events are due.
202         long now = System.currentTimeMillis();
203         long min = 0;
204         long delta = 0;
205 
206         if (last == 0) {
207             last = now;
208         }
209 
210         // time from then to now
211         delta = now - last;
212 
213         if (delta < 0) {
214             // to handle time adjustments
215             // for example Daylight Savings
216             lastTimeMillis = now;
217             return 0;
218         }
219         Iterator<RequestHook> hookIterator = entries.iterator();
220         while(hookIterator.hasNext()) {
221             RequestHook he = hookIterator.next();
222             long left = 0;
223             PlatformEventType es = he.type;
224             // Not enabled, skip.
225             if (!es.isEnabled() || es.isEveryChunk()) {
226                 continue;
227             }
228             long r_period = es.getPeriod();
229             long r_delta = he.delta;
230 
231             // add time elapsed.
232             r_delta += delta;
233 
234             // above threshold?
235             if (r_delta >= r_period) {
236                 // Bug 9000556 - don't try to compensate
237                 // for wait > period
238                 r_delta = 0;
239                 he.execute();
240             }
241 
242             // calculate time left
243             left = (r_period - r_delta);
244 
245             /*
246              * nothing outside checks that a period is >= 0, so left can end up
247              * negative here. ex. (r_period =(-1)) - (r_delta = 0) if it is,
248              * handle it.
249              */
250             if (left < 0) {
251                 left = 0;
252             }
253 
254             // assign delta back
255             he.delta = r_delta;
256 
257             if (min == 0 || left < min) {
258                 min = left;
259             }
260         }
261         // Flush should happen after all periodic events has been emitted
262         // Repeat of the above algorithm, but using the stream interval.
263         if (flushInterval != Long.MAX_VALUE) {
264             long r_period = flushInterval;
265             long r_delta = streamDelta;
266             r_delta += delta;
267             if (r_delta >= r_period) {
268                 r_delta = 0;
269                 MetadataRepository.getInstance().flush();
270                 Utils.notifyFlush();
271             }
272             long left = (r_period - r_delta);
273             if (left < 0) {
274                 left = 0;
275             }
276             streamDelta = r_delta;
277             if (min == 0 || left < min) {
278                 min = left;
279             }
280         }
281 
282         lastTimeMillis = now;
283         return min;
284     }
285 
setFlushInterval(long millis)286     static void setFlushInterval(long millis) {
287         // Don't accept shorter interval than 1 s.
288         long interval = millis < 1000 ? 1000 : millis;
289         boolean needNotify = interval < flushInterval;
290         flushInterval = interval;
291         if (needNotify) {
292             synchronized (JVM.FILE_DELTA_CHANGE) {
293                 JVM.FILE_DELTA_CHANGE.notifyAll();
294             }
295         }
296     }
297 }
298