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 }