1 /* 2 * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. 3 * 4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 * 6 * The contents of this file are subject to the terms of either the Universal Permissive License 7 * v 1.0 as shown at http://oss.oracle.com/licenses/upl 8 * 9 * or the following license: 10 * 11 * Redistribution and use in source and binary forms, with or without modification, are permitted 12 * provided that the following conditions are met: 13 * 14 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions 15 * and the following disclaimer. 16 * 17 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of 18 * conditions and the following disclaimer in the documentation and/or other materials provided with 19 * the distribution. 20 * 21 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to 22 * endorse or promote products derived from this software without specific prior written permission. 23 * 24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 25 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 26 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 27 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 30 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY 31 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 */ 33 package org.openjdk.jmc.rjmx.services.jfr.internal; 34 35 import static org.openjdk.jmc.common.unit.UnitLookup.EPOCH_MS; 36 import static org.openjdk.jmc.common.unit.UnitLookup.toDate; 37 import static org.openjdk.jmc.rjmx.services.jfr.internal.RecordingOptionsToolkitV2.toTabularData; 38 39 import java.io.IOException; 40 import java.io.InputStream; 41 import java.util.ArrayList; 42 import java.util.Collection; 43 import java.util.Collections; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Map.Entry; 48 import java.util.logging.Level; 49 import java.util.logging.Logger; 50 51 import javax.management.MBeanServerConnection; 52 import javax.management.openmbean.CompositeData; 53 import javax.management.openmbean.OpenDataException; 54 import javax.management.openmbean.TabularData; 55 56 import org.eclipse.osgi.util.NLS; 57 import org.openjdk.jmc.common.unit.IConstrainedMap; 58 import org.openjdk.jmc.common.unit.IConstraint; 59 import org.openjdk.jmc.common.unit.IDescribedMap; 60 import org.openjdk.jmc.common.unit.IMutableConstrainedMap; 61 import org.openjdk.jmc.common.unit.IOptionDescriptor; 62 import org.openjdk.jmc.common.unit.IQuantity; 63 import org.openjdk.jmc.common.unit.QuantityConversionException; 64 import org.openjdk.jmc.common.version.JavaVersionSupport; 65 import org.openjdk.jmc.flightrecorder.configuration.ConfigurationToolkit; 66 import org.openjdk.jmc.flightrecorder.configuration.OptionInfo; 67 import org.openjdk.jmc.flightrecorder.configuration.events.EventOptionID; 68 import org.openjdk.jmc.flightrecorder.configuration.events.SchemaVersion; 69 import org.openjdk.jmc.flightrecorder.configuration.internal.DefaultValueMap; 70 import org.openjdk.jmc.flightrecorder.configuration.internal.EventTypeIDV2; 71 import org.openjdk.jmc.flightrecorder.configuration.internal.KnownEventOptions; 72 import org.openjdk.jmc.flightrecorder.configuration.internal.KnownRecordingOptions; 73 import org.openjdk.jmc.flightrecorder.configuration.internal.ValidationToolkit; 74 import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder; 75 import org.openjdk.jmc.rjmx.ConnectionException; 76 import org.openjdk.jmc.rjmx.ConnectionToolkit; 77 import org.openjdk.jmc.rjmx.IConnectionHandle; 78 import org.openjdk.jmc.rjmx.JVMSupportToolkit; 79 import org.openjdk.jmc.rjmx.ServiceNotAvailableException; 80 import org.openjdk.jmc.rjmx.services.ICommercialFeaturesService; 81 import org.openjdk.jmc.rjmx.services.jfr.FlightRecorderException; 82 import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService; 83 import org.openjdk.jmc.rjmx.services.jfr.IRecordingDescriptor; 84 import org.openjdk.jmc.rjmx.subscription.IMBeanHelperService; 85 86 public class FlightRecorderServiceV2 implements IFlightRecorderService { 87 final static Logger LOGGER = Logger.getLogger("org.openjdk.jmc.rjmx.services.jfr"); //$NON-NLS-1$ 88 final private FlightRecorderCommunicationHelperV2 helper; 89 private long eventTypeMetaNextUpdate; 90 private List<EventTypeMetadataV2> eventTypeMetas; 91 private Map<EventTypeIDV2, EventTypeMetadataV2> eventTypeInfoById; 92 private Map<org.openjdk.jmc.flightrecorder.configuration.events.EventOptionID, OptionInfo<?>> optionInfoById; 93 private final ICommercialFeaturesService cfs; 94 private final IMBeanHelperService mbhs; 95 private final String serverId; 96 private final IConnectionHandle connection; 97 98 @Override 99 public String getVersion() { 100 return "2.0"; //$NON-NLS-1$ 101 } 102 103 private boolean isDynamicFlightRecorderSupported(IConnectionHandle handle) { 104 return ConnectionToolkit.isHotSpot(handle) 105 && ConnectionToolkit.isJavaVersionAboveOrEqual(handle, JavaVersionSupport.DYNAMIC_JFR_SUPPORTED); 106 } 107 108 private boolean isFlightRecorderCommercial() { 109 return ConnectionToolkit.isHotSpot(connection) 110 && !ConnectionToolkit.isJavaVersionAboveOrEqual(connection, JavaVersionSupport.JFR_NOT_COMMERCIAL); 111 } 112 113 private boolean isFlightRecorderDisabled(IConnectionHandle handle) { 114 if (cfs != null && isFlightRecorderCommercial()) { 115 return !cfs.isCommercialFeaturesEnabled() || JVMSupportToolkit.isFlightRecorderDisabled(handle, false); 116 } else { 117 return JVMSupportToolkit.isFlightRecorderDisabled(handle, false); 118 } 119 } 120 121 public static boolean isAvailable(IConnectionHandle handle) { 122 return FlightRecorderCommunicationHelperV2.isAvailable(handle); 123 } 124 125 public FlightRecorderServiceV2(IConnectionHandle handle) throws ConnectionException, ServiceNotAvailableException { 126 cfs = handle.getServiceOrThrow(ICommercialFeaturesService.class); 127 if (!isDynamicFlightRecorderSupported(handle) && isFlightRecorderDisabled(handle)) { 128 throw new ServiceNotAvailableException(""); //$NON-NLS-1$ 129 } 130 if (JVMSupportToolkit.isFlightRecorderDisabled(handle, true)) { 131 throw new ServiceNotAvailableException(""); //$NON-NLS-1$ 132 } 133 connection = handle; 134 helper = new FlightRecorderCommunicationHelperV2(handle.getServiceOrThrow(MBeanServerConnection.class)); 135 mbhs = handle.getServiceOrThrow(IMBeanHelperService.class); 136 serverId = handle.getServerDescriptor().getGUID(); 137 } 138 139 @Override 140 public void stop(IRecordingDescriptor descriptor) throws FlightRecorderException { 141 stop(descriptor.getId()); 142 } 143 144 private void stop(Long id) throws FlightRecorderException { 145 try { 146 helper.invokeOperation("stopRecording", id); //$NON-NLS-1$ 147 } catch (Exception e) { 148 throw new FlightRecorderException("Could not stop the recording!", e); //$NON-NLS-1$ 149 } 150 } 151 152 @Override 153 public void close(IRecordingDescriptor descriptor) throws FlightRecorderException { 154 helper.closeRecording(descriptor); 155 } 156 157 @Override 158 public IRecordingDescriptor start( 159 IConstrainedMap<String> recordingOptions, IConstrainedMap<EventOptionID> eventOptions) 160 throws FlightRecorderException { 161 Long id; 162 try { 163 validateOptions(recordingOptions); 164 id = (Long) helper.invokeOperation("newRecording"); //$NON-NLS-1$ 165 } catch (Exception e) { 166 throw new FlightRecorderException("Could not create a recording!", e); //$NON-NLS-1$ 167 } 168 try { 169 updateRecordingOptions(id, recordingOptions); 170 if (eventOptions != null) { 171 updateEventOptions(id, eventOptions); 172 } 173 helper.invokeOperation("startRecording", id); //$NON-NLS-1$ 174 return getUpdatedRecordingDescriptor(id); 175 } catch (Exception e) { 176 try { 177 helper.invokeOperation("closeRecording", id); //$NON-NLS-1$ 178 } catch (IOException ioe) { 179 e.addSuppressed(ioe); 180 throw new FlightRecorderException( 181 "Could not start the recording! Could not remove the unstarted recording.", e); //$NON-NLS-1$ 182 } 183 throw new FlightRecorderException("Could not start the recording! Removed the unstarted recording.", e); //$NON-NLS-1$ 184 } 185 } 186 187 private IMutableConstrainedMap<String> getEmptyRecordingOptions() { 188 return ConfigurationToolkit.getRecordingOptions(JavaVersionSupport.JDK_9).emptyWithSameConstraints(); 189 } 190 191 @Override 192 public IDescribedMap<String> getDefaultRecordingOptions() { 193 return KnownRecordingOptions.OPTION_DEFAULTS_V2; 194 } 195 196 @Override 197 public IConstrainedMap<String> getRecordingOptions(IRecordingDescriptor recording) throws FlightRecorderException { 198 try { 199 return getRecordingOptions(recording.getId()); 200 } catch (Exception e) { 201 throw new FlightRecorderException("Could not retrieve recording options.", e); //$NON-NLS-1$ 202 } 203 } 204 205 private IConstrainedMap<String> getRecordingOptions(Long id) throws FlightRecorderException, IOException { 206 IMutableConstrainedMap<String> options = getEmptyRecordingOptions(); 207 for (Object o : ((TabularData) helper.invokeOperation("getRecordingOptions", id)).values()) { //$NON-NLS-1$ 208 CompositeData row = (CompositeData) o; 209 String key = (String) row.get("key"); //$NON-NLS-1$ 210 String value = (String) row.get("value"); //$NON-NLS-1$ 211 IConstraint<?> constraint = RecordingOptionsToolkitV2.getRecordingOptionConstraint(key); 212 // FIXME: Use generic string constraint if nothing better was found. 213 if (constraint != null) { 214 try { 215 options.putPersistedString(key, constraint, value); 216 } catch (QuantityConversionException e) { 217 // Shouldn't happen, but I want to know if it does. 218 LOGGER.log(Level.FINE, "Recording option conversion problem", e); //$NON-NLS-1$ 219 } 220 } 221 } 222 return options; 223 } 224 225 @Override 226 public IConstrainedMap<EventOptionID> getEventSettings(IRecordingDescriptor recording) 227 throws FlightRecorderException { 228 try { 229 TabularData tabularData = (TabularData) helper.invokeOperation("getRecordingSettings", //$NON-NLS-1$ 230 recording.getId()); 231 IMutableConstrainedMap<EventOptionID> settings = getDefaultEventOptions().emptyWithSameConstraints(); 232 for (Object row : tabularData.values()) { 233 CompositeData data = (CompositeData) row; 234 String key = (String) data.get("key"); //$NON-NLS-1$ 235 String value = (String) data.get("value"); //$NON-NLS-1$ 236 int hashPos = key.lastIndexOf('#'); 237 if (hashPos > 0) { 238 // FIXME: Deal with numerically specified event type (instance). 239 EventTypeIDV2 type = new EventTypeIDV2(key.substring(0, hashPos)); 240 EventOptionID option = new EventOptionID(type, key.substring(hashPos + 1)); 241 // FIXME: Try/catch and ignore? 242 settings.putPersistedString(option, value); 243 } 244 } 245 return settings; 246 } catch (Exception e) { 247 FlightRecorderException flr = new FlightRecorderException( 248 "Could not retrieve recording options for recording " + recording.getName() + '.'); //$NON-NLS-1$ 249 flr.initCause(e); 250 throw flr; 251 } 252 } 253 254 // FIXME: This should _really_ be retrieved from the server, but the server API does not allow that at the moment. 255 @Override 256 public Map<String, IOptionDescriptor<?>> getAvailableRecordingOptions() throws FlightRecorderException { 257 return RecordingOptionsToolkitV2.getAvailableRecordingOptions(); 258 } 259 260 @Override 261 public String toString() { 262 return helper.toString(); 263 } 264 265 @Override 266 public InputStream openStream(IRecordingDescriptor descriptor, boolean removeOnClose) 267 throws FlightRecorderException { 268 IRecordingDescriptor streamDescriptor = descriptor; 269 boolean clone = isStillRunning(descriptor); 270 if (clone) { 271 streamDescriptor = clone(descriptor); 272 } 273 return new JfrRecordingInputStreamV2(helper, streamDescriptor, clone | removeOnClose); 274 } 275 276 @Override 277 public InputStream openStream( 278 IRecordingDescriptor descriptor, IQuantity startTime, IQuantity endTime, boolean removeOnClose) 279 throws FlightRecorderException { 280 IRecordingDescriptor streamDescriptor = descriptor; 281 boolean clone = isStillRunning(descriptor); 282 if (clone) { 283 streamDescriptor = clone(descriptor); 284 } 285 return new JfrRecordingInputStreamV2(helper, streamDescriptor, toDate(startTime), toDate(endTime), 286 clone | removeOnClose); 287 } 288 289 @Override 290 public Collection<EventTypeMetadataV2> getAvailableEventTypes() throws FlightRecorderException { 291 return updateEventTypeMetadataMaps(true); 292 } 293 294 @Override 295 public List<IRecordingDescriptor> getAvailableRecordings() throws FlightRecorderException { 296 CompositeData[] attribute = (CompositeData[]) helper.getAttribute("Recordings"); //$NON-NLS-1$ 297 List<IRecordingDescriptor> recordings = new ArrayList<>(); 298 for (CompositeData data : attribute) { 299 recordings.add(new RecordingDescriptorV2(serverId, data)); 300 } 301 return Collections.unmodifiableList(recordings); 302 } 303 304 @Override 305 public IRecordingDescriptor getSnapshotRecording() throws FlightRecorderException { 306 try { 307 Long id = (Long) helper.invokeOperation("takeSnapshot", new Object[0]); //$NON-NLS-1$ 308 return getUpdatedRecordingDescriptor(id); 309 } catch (Exception e) { 310 throw new FlightRecorderException("Could not take a snapshot of the flight recorder", e); //$NON-NLS-1$ 311 } 312 } 313 314 @Override 315 public IDescribedMap<EventOptionID> getCurrentEventTypeSettings() throws FlightRecorderException { 316 updateEventTypeMetadataMaps(true); 317 return new DefaultValueMap<>(optionInfoById, new ExcludingEventOptionMapper(eventTypeInfoById.keySet(), 318 EventTypeIDV2.class, KnownEventOptions.EVENT_OPTIONS_BY_KEY_V2)); 319 } 320 321 @Override 322 public IDescribedMap<EventOptionID> getDefaultEventOptions() { 323 try { 324 return getCurrentEventTypeSettings(); 325 } catch (FlightRecorderException e) { 326 LOGGER.log(Level.WARNING, "Couldn't get event settings", e); //$NON-NLS-1$ 327 return ConfigurationToolkit.getEventOptions(SchemaVersion.V2); 328 } 329 } 330 331 @Override 332 public IRecordingDescriptor getUpdatedRecordingDescription(IRecordingDescriptor descriptor) 333 throws FlightRecorderException { 334 return getUpdatedRecordingDescriptor(descriptor.getId()); 335 } 336 337 @Override 338 public List<String> getServerTemplates() throws FlightRecorderException { 339 CompositeData[] compositeData = (CompositeData[]) helper.getAttribute("Configurations"); //$NON-NLS-1$ 340 return RecordingTemplateToolkit.getServerTemplatesV2(compositeData); 341 } 342 343 @Override 344 public void updateEventOptions(IRecordingDescriptor descriptor, IConstrainedMap<EventOptionID> options) 345 throws FlightRecorderException { 346 try { 347 updateEventOptions(descriptor.getId(), options); 348 } catch (Exception e) { 349 throw new FlightRecorderException("Failed updating the event options for " + descriptor.getName(), e); //$NON-NLS-1$ 350 } 351 } 352 353 private IRecordingDescriptor getUpdatedRecordingDescriptor(Long id) throws FlightRecorderException { 354 // getRecordingOptions doesn't quite contain all we need, so retrieve 355 // everything and filter out what we need... 356 for (IRecordingDescriptor recording : getAvailableRecordings()) { 357 if (id.equals(recording.getId())) { 358 return recording; 359 } 360 } 361 return null; 362 } 363 364 private void validateOptions(IConstrainedMap<String> recordingOptions) throws FlightRecorderException { 365 try { 366 ValidationToolkit.validate(recordingOptions); 367 } catch (Exception e) { 368 throw new FlightRecorderException("Could not validate options!\n" + e.getMessage()); //$NON-NLS-1$ 369 } 370 } 371 372 @Override 373 public Map<EventTypeIDV2, EventTypeMetadataV2> getEventTypeInfoMapByID() throws FlightRecorderException { 374 updateEventTypeMetadataMaps(false); 375 return eventTypeInfoById; 376 } 377 378 private Collection<EventTypeMetadataV2> updateEventTypeMetadataMaps(boolean force) throws FlightRecorderException { 379 long timestamp = System.currentTimeMillis(); 380 if (force || (timestamp > eventTypeMetaNextUpdate)) { 381 382 CompositeData[] compositeList = (CompositeData[]) helper.getAttribute("EventTypes"); //$NON-NLS-1$ 383 384 List<EventTypeMetadataV2> metadataList = new ArrayList<>(compositeList.length); 385 Map<EventTypeIDV2, EventTypeMetadataV2> byId = new HashMap<>(); 386 Map<EventOptionID, OptionInfo<?>> optionById = new HashMap<>(); 387 for (CompositeData data : compositeList) { 388 EventTypeMetadataV2 typeInfo = EventTypeMetadataV2.from(data); 389 metadataList.add(typeInfo); 390 EventTypeIDV2 typeID = typeInfo.getEventTypeID(); 391 byId.put(typeID, typeInfo); 392 for (Entry<String, OptionInfo<?>> entry : typeInfo.getOptionDescriptors().entrySet()) { 393 optionById.put(new EventOptionID(typeID, entry.getKey()), entry.getValue()); 394 } 395 } 396 397 // Do not update more often than every minute. 398 // FIXME: Use JMX notifications instead? 399 eventTypeMetaNextUpdate = timestamp + 60 * 1000; 400 eventTypeMetas = Collections.unmodifiableList(metadataList); 401 eventTypeInfoById = Collections.unmodifiableMap(byId); 402 optionInfoById = Collections.unmodifiableMap(optionById); 403 } 404 return eventTypeMetas; 405 } 406 407 private boolean isStillRunning(IRecordingDescriptor descriptor) throws FlightRecorderException { 408 IRecordingDescriptor updatedDescriptor = getUpdatedRecordingDescription(descriptor); 409 return updatedDescriptor != null 410 && IRecordingDescriptor.RecordingState.RUNNING.equals(updatedDescriptor.getState()); 411 } 412 413 // creates a stopped clone 414 private IRecordingDescriptor clone(IRecordingDescriptor descriptor) throws FlightRecorderException { 415 try { 416 Long id = (Long) helper.invokeOperation("cloneRecording", //$NON-NLS-1$ 417 descriptor.getId(), Boolean.TRUE); 418 IMutableConstrainedMap<String> options = getEmptyRecordingOptions(); 419 options.put(RecordingOptionsBuilder.KEY_NAME, 420 NLS.bind(Messages.FlightRecorderServiceV2_CLONE_OF_RECORDING_NAME, descriptor.getName())); 421 helper.invokeOperation("setRecordingOptions", id, toTabularData(options)); //$NON-NLS-1$ 422 return getUpdatedRecordingDescriptor(id); 423 } catch (Exception e) { 424 throw new FlightRecorderException("Could not clone the " + descriptor.getName() + " recording ", e); //$NON-NLS-1$ //$NON-NLS-2$ 425 } 426 } 427 428 private void updateEventOptions(Long id, IConstrainedMap<EventOptionID> options) 429 throws OpenDataException, IOException, FlightRecorderException { 430 helper.invokeOperation("setRecordingSettings", id, //$NON-NLS-1$ 431 toTabularData(options)); 432 } 433 434 @Override 435 public void updateRecordingOptions(IRecordingDescriptor descriptor, IConstrainedMap<String> options) 436 throws FlightRecorderException { 437 validateOptions(options); 438 // Currently (2016-06-01), in some states, JFR complains about the presence of certain 439 // options even if unchanged. So, just send the delta. 440 IConstrainedMap<String> current = getRecordingOptions(descriptor); 441 IConstrainedMap<String> deltaOptions = ConfigurationToolkit.extractDelta(options, current); 442 try { 443 updateRecordingOptions(descriptor.getId(), deltaOptions); 444 } catch (Exception e) { 445 throw new FlightRecorderException("Failed updating the recording options for " + descriptor.getName(), e); //$NON-NLS-1$ 446 } 447 } 448 449 private void updateRecordingOptions(Long id, IConstrainedMap<String> options) 450 throws OpenDataException, IOException, FlightRecorderException { 451 helper.invokeOperation("setRecordingOptions", id, //$NON-NLS-1$ 452 toTabularData(options)); 453 } 454 455 @Override 456 public InputStream openStream(IRecordingDescriptor descriptor, IQuantity lastPartDuration, boolean removeOnClose) 457 throws FlightRecorderException { 458 /* 459 * FIXME: JMC-4270 - Server time approximation is not reliable. Can perhaps get a better 460 * time by cloning the recording and getting the end time from there like in the commented 461 * out code below. 462 */ 463 // IRecordingDescriptor streamDescriptor = descriptor; 464 // boolean clone = isStillRunning(descriptor); 465 // if (clone) { 466 // streamDescriptor = clone(descriptor); 467 // } 468 // IQuantity endTime = streamDescriptor.getDataEndTime(); 469 // IQuantity startTime = endTime.subtract(lastPartDuration); 470 // return new JfrRecordingInputStreamV2(helper, streamDescriptor, toDate(startTime), toDate(endTime), clone | removeOnClose); 471 472 long serverTime = mbhs.getApproximateServerTime(System.currentTimeMillis()); 473 IQuantity endDate = EPOCH_MS.quantity(serverTime); 474 IQuantity startDate = endDate.subtract(lastPartDuration); 475 return openStream(descriptor, startDate, endDate, removeOnClose); 476 } 477 478 @Override 479 public boolean isEnabled() { 480 return isFlightRecorderCommercial() 481 ? cfs.isCommercialFeaturesEnabled() 482 : isAvailable(connection); 483 } 484 485 @Override 486 public void enable() throws FlightRecorderException { 487 try { 488 cfs.enableCommercialFeatures(); 489 } catch (Exception e) { 490 throw new FlightRecorderException("Failed to enable commercial features", e); //$NON-NLS-1$ 491 } 492 } 493 }