/*******************************************************************************
* Copyright (c) 2015, 2017 Pivotal Software, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.dash.util;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.inject.Provider;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Platform;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.model.IProcess;
import org.springframework.ide.eclipse.boot.dash.model.RunState;
import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate;
import org.springframework.ide.eclipse.boot.launch.cli.CloudCliServiceLaunchConfigurationDelegate;
import org.springframework.ide.eclipse.boot.launch.util.BootLaunchUtils;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springframework.ide.eclipse.boot.util.ProcessListenerAdapter;
import org.springframework.ide.eclipse.boot.util.ProcessTracker;
import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression;
import org.springsource.ide.eclipse.commons.livexp.core.ValueListener;
import org.springsource.ide.eclipse.commons.livexp.ui.Disposable;
/**
* Generalization of OwnerRunStateTracker. An instance of this class tracks active processes
* in the eclipse DebugUI and maitains a 'RunState' that is associated indirectly with some
* object these processes belong to.
* <p>
* This is an abstract class as clients need to implement the 'getOwner' method to define
* what enitity a given launch configuration belongs to.
*
* @author Kris De Volder
*/
public abstract class RunStateTracker<T> extends ProcessListenerAdapter implements Disposable {
//// public API ///////////////////////////////////////////////////////////////////
public interface RunStateListener<T> {
void stateChanged(T owner);
}
public RunStateTracker() {
activeStates = new HashMap<>();
processTracker = new ProcessTracker(this);
updateOwnerStatesAndFireEvents();
}
public synchronized RunState getState(final T owner) {
return getState(activeStates, owner);
}
public void addListener(RunStateListener<T> listener) {
this.listeners.add(listener);
}
public void removeListener(RunStateListener<T> listener) {
this.listeners.remove(listener);
}
///////////////////////// stuff below is implementation cruft ////////////////////
private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder");
private static void debug(String string) {
if (DEBUG) {
System.out.println(string);
}
}
private Map<T, RunState> activeStates = null;
private Map<ILaunch, ReadyStateMonitor> readyStateTrackers = null;
private ProcessTracker processTracker = null;
private ListenerList listeners = new ListenerList(); // listeners that are interested in us (i.e. clients)
private static <T> RunState getState(Map<T, RunState> states, T p) {
if (states!=null) {
RunState state = states.get(p);
if (state!=null) {
return state;
}
}
return RunState.INACTIVE;
}
private Map<T, RunState> getCurrentActiveStates() {
Map<T, RunState> states = new HashMap<>();
for (ILaunch l : launchManager().getLaunches()) {
if (!l.isTerminated() && isInteresting(l)) {
T p = getOwner(l);
RunState s1 = getState(states, p);
RunState s2 = getActiveState(l);
states.put(p, s1.merge(s2));
}
}
return states;
}
protected abstract T getOwner(ILaunch l);
protected boolean isInteresting(ILaunch l) {
return BootLaunchUtils.isBootLaunch(l)
|| CloudCliServiceLaunchConfigurationDelegate.isLocalCloudServiceLaunch(l.getLaunchConfiguration());
}
/**
* Assuming that l is an active launch, determine its RunState.
*/
private RunState getActiveState(ILaunch l) {
boolean isReady = getReadyState(l).getValue();
if (isReady) {
return BootLaunchUtils.isDebugging(l)
? RunState.DEBUGGING
: RunState.RUNNING;
}
return RunState.STARTING;
}
private synchronized LiveExpression<Boolean> getReadyState(ILaunch l) {
if (readyStateTrackers==null) {
readyStateTrackers = new HashMap<>();
}
ReadyStateMonitor tracker = readyStateTrackers.get(l);
if (tracker==null) {
readyStateTrackers.put(l, tracker = createReadyStateTracker(l));
tracker.getReady().addListener(readyStateListener);
}
return tracker.getReady();
}
private ValueListener<Boolean> readyStateListener = new ValueListener<Boolean>() {
public void gotValue(LiveExpression<Boolean> exp, Boolean value) {
if (value) {
//ready state tracker detected a launch just entered the 'ready' state
updateOwnerStatesAndFireEvents();
}
}
};
private boolean updateInProgress;
protected ReadyStateMonitor createReadyStateTracker(ILaunch l) {
try {
if (BootLaunchConfigurationDelegate.canUseLifeCycle(l) || CloudCliServiceLaunchConfigurationDelegate.canUseLifeCycle(l)) {
Provider<Integer> jmxPort = () -> BootLaunchConfigurationDelegate.getJMXPortAsInt(l);
return new SpringApplicationReadyStateMonitor(jmxPort);
}
} catch (Exception e) {
Log.log(e);
}
return DummyReadyStateMonitor.create();
}
private synchronized void cleanupReadyStateTrackers() {
if (readyStateTrackers!=null) {
Iterator<Entry<ILaunch, ReadyStateMonitor>> iter = readyStateTrackers.entrySet().iterator();
while(iter.hasNext()) {
Entry<ILaunch, ReadyStateMonitor> entry = iter.next();
ILaunch l = entry.getKey();
if (l.isTerminated()) {
ReadyStateMonitor tracker = entry.getValue();
iter.remove();
tracker.dispose();
}
}
}
}
protected ILaunchManager launchManager() {
return DebugPlugin.getDefault().getLaunchManager();
}
public void dispose() {
if (processTracker!=null) {
processTracker.dispose();
processTracker = null;
}
}
private void updateOwnerStatesAndFireEvents() {
//Note that updateOwnerStates is synchronized, but this method is not.
// Important not to keep locks while firing events.
Set<T> affected = updateOwnerStates();
for (Object _l : listeners.getListeners()) {
@SuppressWarnings("unchecked")
RunStateListener<T> listener = (RunStateListener<T>) _l;
for (T p : affected) {
listener.stateChanged(p);
}
}
}
private synchronized Set<T> updateOwnerStates() {
if (updateInProgress) {
//Avoid bug caused by reentrance from same thread.
//This bug causes double update events for INAVTIVE -> RUNNING for owners that
// don't have ready state tracking and so immediately enter the ready state upon
// creation.
return Collections.emptySet();
} else {
updateInProgress = true;
try {
Map<T, RunState> oldStates = activeStates;
activeStates = getCurrentActiveStates();
// Compute set of owners who's state has changed
Set<T> affectedOwners = new HashSet<>(keySet(oldStates));
affectedOwners.addAll(keySet(activeStates));
Iterator<T> iter = affectedOwners.iterator();
while (iter.hasNext()) {
T p = iter.next();
RunState oldState = getState(oldStates, p);
RunState newState = getState(activeStates, p);
if (oldState.equals(newState)) {
iter.remove();
} else {
debug(p+": "+ oldState +" => " + newState);
}
}
return affectedOwners;
} finally {
updateInProgress = false;
}
}
}
/**
* Null-safe 'keySet' fetcher for map.
*/
private <K,V> Set<K> keySet(Map<K, V> map) {
if (map==null) {
return Collections.emptySet();
}
return map.keySet();
}
@Override
public void processTerminated(ProcessTracker tracker, IProcess process) {
updateOwnerStatesAndFireEvents();
cleanupReadyStateTrackers();
}
@Override
public void processCreated(ProcessTracker tracker, IProcess process) {
updateOwnerStatesAndFireEvents();
}
}