package org.yamcs.management;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yamcs.ConfigurationException;
import org.yamcs.ProcessorFactory;
import org.yamcs.Processor;
import org.yamcs.ProcessorClient;
import org.yamcs.ProcessorException;
import org.yamcs.ProcessorListener;
import org.yamcs.YamcsException;
import org.yamcs.commanding.CommandQueue;
import org.yamcs.commanding.CommandQueueListener;
import org.yamcs.commanding.CommandQueueManager;
import org.yamcs.protobuf.YamcsManagement.ClientInfo;
import org.yamcs.protobuf.YamcsManagement.LinkInfo;
import org.yamcs.protobuf.YamcsManagement.ProcessorInfo;
import org.yamcs.protobuf.YamcsManagement.ProcessorManagementRequest;
import org.yamcs.protobuf.YamcsManagement.Statistics;
import org.yamcs.security.AuthenticationToken;
import org.yamcs.security.Privilege;
import org.yamcs.tctm.Link;
import org.yamcs.xtceproc.ProcessingStatistics;
import com.google.common.util.concurrent.Service;
/**
* Responsible for integrating with core yamcs classes, encoding to protobuf,
* and forwarding aggregated info downstream.
* <p>
* Notable examples of downstream listeners are the MBeanServer, the ActiveMQ-business,
* and subscribed websocket clients.
*/
public class ManagementService implements ProcessorListener {
final MBeanServer mbeanServer;
final boolean jmxEnabled;
static Logger log = LoggerFactory.getLogger(ManagementService.class.getName());
final String tld = "yamcs";
static ManagementService managementService;
Map<Integer, ClientControlImpl> clients = Collections.synchronizedMap(new HashMap<Integer, ClientControlImpl>());
AtomicInteger clientId=new AtomicInteger();
List<LinkControlImpl> links=new CopyOnWriteArrayList<LinkControlImpl>();
List<CommandQueueManager> qmanagers=new CopyOnWriteArrayList<>();
// Used to update TM-statistics, and Link State
ScheduledThreadPoolExecutor timer=new ScheduledThreadPoolExecutor(1);
Set<ManagementListener> managementListeners = new CopyOnWriteArraySet<>(); // Processors & Clients. Should maybe split up
Set<LinkListener> linkListeners = new CopyOnWriteArraySet<>();
Set<CommandQueueListener> commandQueueListeners = new CopyOnWriteArraySet<>();
// keep track of registered services
Map<String, Integer> servicesCount = new HashMap<>();
Map<Processor, Statistics> yprocs=new ConcurrentHashMap<Processor, Statistics>();
static final Statistics STATS_NULL=Statistics.newBuilder().setInstance("null").setYProcessorName("null").build();//we use this one because ConcurrentHashMap does not support null values
static public void setup(boolean jmxEnabled) throws NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException, MalformedObjectNameException, NullPointerException {
managementService = new ManagementService(jmxEnabled);
}
static public ManagementService getInstance() {
return managementService;
}
private ManagementService(boolean jmxEnabled) throws NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException, MalformedObjectNameException, NullPointerException {
this.jmxEnabled=jmxEnabled;
if(jmxEnabled)
mbeanServer=ManagementFactory.getPlatformMBeanServer();
else
mbeanServer=null;
Processor.addProcessorListener(this);
timer.scheduleAtFixedRate(() -> updateStatistics(), 1, 1, TimeUnit.SECONDS);
timer.scheduleAtFixedRate(() -> checkLinkUpdate(), 1, 1, TimeUnit.SECONDS);
}
public void shutdown() {
managementListeners.clear();
}
public void registerService(String instance, String serviceName, Service service) {
if(jmxEnabled) {
ServiceControlImpl sci;
try {
sci = new ServiceControlImpl(service);
// if a service with the same name has already been registered, suffix the service name with an index
int serviceCount = 0;
if(servicesCount.containsKey(serviceName)) {
serviceCount = servicesCount.get(serviceName);
servicesCount.remove(serviceName);
}
servicesCount.put(serviceName, ++serviceCount);
if(serviceCount > 1)
serviceName=serviceName + "_" + serviceCount;
// register service
mbeanServer.registerMBean(sci, ObjectName.getInstance(tld+"."+instance+":type=services,name="+serviceName));
} catch (Exception e) {
log.warn("Got exception when registering a service", e);
}
}
}
public void unregisterService(String instance, String serviceName) {
if(jmxEnabled) {
try {
// check if this serviceName has been registered several time
int serviceCount = 0;
String serviceName_ = serviceName;
if(servicesCount.containsKey(serviceName) && (serviceCount = servicesCount.get(serviceName)) > 0) {
if(serviceCount > 1)
serviceName_ = serviceName + "_" + serviceCount;
serviceCount--;
servicesCount.replace(serviceName, serviceCount);
}
// unregister service
mbeanServer.unregisterMBean(ObjectName.getInstance(tld+"."+instance+":type=services,name="+serviceName_));
} catch (Exception e) {
log.warn("Got exception when unregistering a service", e);
}
}
}
public void registerLink(String instance, String name, String streamName, String spec, Link link) {
try {
LinkControlImpl lci = new LinkControlImpl(instance, name, streamName, spec, link);
if(jmxEnabled) {
mbeanServer.registerMBean(lci, ObjectName.getInstance(tld+"."+instance+":type=links,name="+name));
}
links.add(lci);
linkListeners.forEach(l -> l.registerLink(lci.getLinkInfo()));
} catch (Exception e) {
log.warn("Got exception when registering a link: ", e);
}
}
public void unregisterLink(String instance, String name) {
if(jmxEnabled) {
try {
mbeanServer.unregisterMBean(ObjectName.getInstance(tld+"."+instance+":type=links,name="+name));
} catch (Exception e) {
log.warn("Got exception when unregistering a link", e);
}
}
linkListeners.forEach(l -> l.unregisterLink(instance, name));
}
public CommandQueueManager getQueueManager(String instance, String processorName) throws YamcsException {
for(int i=0;i<qmanagers.size();i++) {
CommandQueueManager cqm=qmanagers.get(i);
if(cqm.getInstance().equals(instance) && cqm.getChannelName().equals(processorName)) {
return cqm;
}
}
throw new YamcsException("Cannot find a command queue manager for "+instance+"/"+processorName);
}
public List<CommandQueueManager> getQueueManagers() {
return qmanagers;
}
public void registerYProcessor(Processor yproc) {
try {
ProcessorControlImpl cci = new ProcessorControlImpl(yproc);
if(jmxEnabled) {
mbeanServer.registerMBean(cci, ObjectName.getInstance(tld+"."+yproc.getInstance()+":type=processors,name="+yproc.getName()));
}
} catch (Exception e) {
log.warn("Got exception when registering a processor", e);
}
}
public void unregisterYProcessor(Processor yproc) {
if(jmxEnabled) {
try {
mbeanServer.unregisterMBean(ObjectName.getInstance(tld+"."+yproc.getInstance()+":type=processors,name="+yproc.getName()));
} catch (Exception e) {
log.warn("Got exception when unregistering a processor", e);
}
}
}
public int registerClient(String instance, String yprocName, ProcessorClient client) {
int id=clientId.incrementAndGet();
try {
Processor c=Processor.getInstance(instance, yprocName);
if(c==null) {
throw new YamcsException("Unexisting yprocessor ("+instance+", "+yprocName+") specified");
}
ClientControlImpl cci = new ClientControlImpl(instance, id, client.getUsername(), client.getApplicationName(), yprocName, client);
clients.put(cci.getClientInfo().getId(), cci);
if(jmxEnabled) {
mbeanServer.registerMBean(cci, ObjectName.getInstance(tld+"."+instance+":type=clients,processor="+yprocName+",id="+id));
}
managementListeners.forEach(l -> l.clientRegistered(cci.getClientInfo()));
} catch (Exception e) {
log.warn("Got exception when registering a client", e);
}
return id;
}
public void unregisterClient(int id) {
ClientControlImpl cci=clients.remove(id);
if(cci==null) {
return;
}
ClientInfo ci=cci.getClientInfo();
try {
if(jmxEnabled) {
mbeanServer.unregisterMBean(ObjectName.getInstance(tld+"."+ci.getInstance()+":type=clients,processor="+ci.getProcessorName()+",id="+id));
}
managementListeners.forEach(l -> l.clientUnregistered(ci));
} catch (Exception e) {
log.warn("Got exception when registering a client", e);
}
}
private void switchProcessor(ClientControlImpl cci, Processor yproc, AuthenticationToken authToken) throws ProcessorException {
ClientInfo oldci=cci.getClientInfo();
cci.switchYProcessor(yproc, authToken);
ClientInfo ci=cci.getClientInfo();
try {
if(jmxEnabled) {
mbeanServer.unregisterMBean(ObjectName.getInstance(tld+"."+oldci.getInstance()+":type=clients,processor="+oldci.getProcessorName()+",id="+ci.getId()));
mbeanServer.registerMBean(cci, ObjectName.getInstance(tld+"."+ci.getInstance()+":type=clients,processor="+ci.getProcessorName()+",id="+ci.getId()));
}
managementListeners.forEach(l -> l.clientInfoChanged(ci));
} catch (Exception e) {
log.warn("Got exception when switching processor", e);
}
}
public void createProcessor(ProcessorManagementRequest cr, AuthenticationToken authToken) throws YamcsException{
log.info("Creating new processor instance: {}, name: {}, type: {}, config: {}, persistent: {}",cr.getInstance(), cr.getName(), cr.getType(), cr.getConfig(), cr.getPersistent());
String username;
if (authToken != null && authToken.getPrincipal() != null) {
username = authToken.getPrincipal().toString();
} else {
username = Privilege.getDefaultUser();
}
if(!Privilege.getInstance().hasPrivilege1(authToken, Privilege.SystemPrivilege.MayControlProcessor)) {
if(cr.getPersistent()) {
log.warn("User {} is not allowed to create persistent processors", username);
throw new YamcsException("Permission denied");
}
if(!"Archive".equals(cr.getType())) {
log.warn("User {} is not allowed to create processors of type {}", cr.getType(), username);
throw new YamcsException("Permission denied");
}
for(int i=0;i<cr.getClientIdCount();i++) {
ClientInfo si=clients.get(cr.getClientId(i)).getClientInfo();
if(!username.equals(si.getUsername())) {
log.warn("User {} is not allowed to connect {} to new processor {}", username, si.getUsername(), cr.getName());
throw new YamcsException("Permission denied");
}
}
}
Processor yproc;
try {
int n=0;
Object config = null;
if(cr.hasReplaySpec()) {
config = cr.getReplaySpec();
} else if (cr.hasConfig()){
config = cr.getConfig();
}
yproc = ProcessorFactory.create(cr.getInstance(), cr.getName(), cr.getType(), username, config);
yproc.setPersistent(cr.getPersistent());
for(int i=0; i<cr.getClientIdCount(); i++) {
ClientControlImpl cci = clients.get(cr.getClientId(i));
if(cci!=null) {
switchProcessor(cci, yproc, authToken);
n++;
} else {
log.warn("createProcessor called with invalid client id:"+cr.getClientId(i)+"; ignored.");
}
}
if(n>0 || cr.getPersistent()) {
log.info("Starting new processor '" + yproc.getName() + "' with " + yproc.getConnectedClients() + " clients");
yproc.startAsync();
yproc.awaitRunning();
} else {
yproc.quit();
throw new YamcsException("createProcessor invoked with a list full of invalid client ids");
}
} catch (ProcessorException | ConfigurationException e) {
throw new YamcsException(e.getMessage(), e.getCause());
} catch (IllegalStateException e1) {
Throwable t = e1.getCause();
if(t instanceof YamcsException) {
throw (YamcsException )t;
} else {
throw new YamcsException(t.getMessage(), t.getCause());
}
}
}
public void connectToProcessor(ProcessorManagementRequest cr, AuthenticationToken usertoken) throws YamcsException {
Processor chan=Processor.getInstance(cr.getInstance(), cr.getName());
if(chan==null) {
throw new YamcsException("Unexisting processor "+cr.getInstance()+"/"+cr.getName()+" specified");
}
String username;
if (usertoken != null && usertoken.getPrincipal() != null) {
username = usertoken.getPrincipal().toString();
} else {
username = Privilege.getDefaultUser();
}
log.debug("User {} wants to connect clients {} to processor {}", username, cr.getClientIdList(), cr.getName());
if(!Privilege.getInstance().hasPrivilege1(usertoken, Privilege.SystemPrivilege.MayControlProcessor) &&
!((chan.isPersistent() || chan.getCreator().equals(username)))) {
log.warn("User {} is not allowed to connect users to processor {}", username, cr.getName() );
throw new YamcsException("permission denied");
}
if(!Privilege.getInstance().hasPrivilege1(usertoken, Privilege.SystemPrivilege.MayControlProcessor)) {
for(int i=0; i<cr.getClientIdCount(); i++) {
ClientInfo si=clients.get(cr.getClientId(i)).getClientInfo();
if(!username.equals(si.getUsername())) {
log.warn("User {} is not allowed to connect {} to processor {}", username, si.getUsername(), cr.getName());
throw new YamcsException("Permission denied");
}
}
}
try {
for(int i=0;i<cr.getClientIdCount();i++) {
int id=cr.getClientId(i);
ClientControlImpl cci=clients.get(id);
switchProcessor(cci, chan, usertoken);
}
} catch(ProcessorException e) {
throw new YamcsException(e.toString());
}
}
public void registerCommandQueueManager(String instance, String yprocName, CommandQueueManager cqm) {
try {
for(CommandQueue cq:cqm.getQueues()) {
if(jmxEnabled) {
CommandQueueControlImpl cqci = new CommandQueueControlImpl(instance, yprocName, cqm, cq);
mbeanServer.registerMBean(cqci, ObjectName.getInstance(tld+"."+instance+":type=commandQueues,processor="+yprocName+",name="+cq.getName()));
}
}
qmanagers.add(cqm);
for (CommandQueueListener l : commandQueueListeners) {
cqm.registerListener(l);
for(CommandQueue q:cqm.getQueues()) {
l.updateQueue(q);
}
}
} catch (Exception e) {
log.warn("Got exception when registering a command queue", e);
}
}
public List<CommandQueueManager> getCommandQueueManagers() {
return qmanagers;
}
public CommandQueueManager getCommandQueueManager(Processor processor) {
for (CommandQueueManager mgr : qmanagers) {
if (mgr.getInstance().equals(processor.getInstance())
&& mgr.getChannelName().equals(processor.getName())) {
return mgr;
}
}
return null;
}
public void enableLink(String instance, String name) throws YamcsException {
log.debug("received enableLink for "+instance+"/"+name);
boolean found=false;
for(int i=0;i<links.size();i++) {
LinkControlImpl lci=links.get(i);
LinkInfo li2=lci.getLinkInfo();
if(li2.getInstance().equals(instance) && li2.getName().equals(name)) {
found=true;
lci.enable();
break;
}
}
if(!found) {
throw new YamcsException("There is no link named '"+name+"' in instance "+instance);
}
}
public void disableLink(String instance, String name) throws YamcsException {
log.debug("received disableLink for "+instance+"/"+name);
boolean found=false;
for(int i=0;i<links.size();i++) {
LinkControlImpl lci=links.get(i);
LinkInfo li2=lci.getLinkInfo();
if(li2.getInstance().equals(instance) && li2.getName().equals(name)) {
found=true;
lci.disable();
break;
}
}
if(!found) {
throw new YamcsException("There is no link named '"+name+"' in instance "+instance);
}
}
/**
* Adds a listener that is to be notified when any processor, or any client
* is updated. Calling this multiple times has no extra effects. Either you
* listen, or you don't.
*/
public boolean addManagementListener(ManagementListener l) {
return managementListeners.add(l);
}
/**
* Adds a listener that is to be notified when any processor, or any client
* is updated. Calling this multiple times has no extra effects. Either you
* listen, or you don't.
*/
public boolean addLinkListener(LinkListener l) {
return linkListeners.add(l);
}
public boolean removeManagementListener(ManagementListener l) {
return managementListeners.remove(l);
}
public boolean addCommandQueueListener(CommandQueueListener l) {
return commandQueueListeners.add(l);
}
public boolean removeCommandQueueListener(CommandQueueListener l) {
boolean removed = commandQueueListeners.remove(l);
qmanagers.forEach(m -> m.removeListener(l));
return removed;
}
public boolean removeLinkListener(LinkListener l) {
return linkListeners.remove(l);
}
public List<LinkInfo> getLinkInfo() {
List<LinkInfo> l = new ArrayList<>();
for (LinkControlImpl li : links) {
l.add(li.getLinkInfo());
}
return l;
}
public LinkInfo getLinkInfo(String instance, String name) {
for(int i=0;i<links.size();i++) {
LinkControlImpl lci=links.get(i);
LinkInfo li=lci.getLinkInfo();
if(li.getInstance().equals(instance) && li.getName().equals(name)) {
return li;
}
}
return null;
}
public Set<ClientInfo> getClientInfo() {
synchronized(clients) {
return clients.values().stream()
.map(v -> v.getClientInfo())
.collect(Collectors.toSet());
}
}
public Set<ClientInfo> getClientInfo(String username) {
synchronized(clients) {
return clients.values().stream()
.map(v -> v.getClientInfo())
.filter(c -> c.getUsername().equals(username))
.collect(Collectors.toSet());
}
}
public ClientInfo getClientInfo(int clientId) {
ClientControlImpl cci = clients.get(clientId);
if(cci==null) {
return null;
}
return cci.getClientInfo();
}
private void updateStatistics() {
try {
for(Entry<Processor,Statistics> entry:yprocs.entrySet()) {
Processor yproc=entry.getKey();
Statistics stats=entry.getValue();
ProcessingStatistics ps=yproc.getTmProcessor().getStatistics();
if((stats==STATS_NULL) || (ps.getLastUpdated()>stats.getLastUpdated())) {
stats=ManagementGpbHelper.buildStats(yproc);
yprocs.put(yproc, stats);
}
if(stats!=STATS_NULL) {
for (ManagementListener l : managementListeners) {
l.statisticsUpdated(yproc, stats);
}
}
}
} catch (Exception e) {
log.warn("Error updating statistics ", e);
}
}
private void checkLinkUpdate() {
// see if any link has changed
for(LinkControlImpl lci:links) {
if(lci.hasChanged()) {
LinkInfo li = lci.getLinkInfo();
linkListeners.forEach(l -> l.linkChanged(li));
}
}
}
@Override
public void processorAdded(Processor processor) {
ProcessorInfo pi = ManagementGpbHelper.toProcessorInfo(processor);
managementListeners.forEach(l -> l.processorAdded(pi));
yprocs.put(processor, STATS_NULL);
}
@Override
public void processorClosed(Processor processor) {
ProcessorInfo pi = ManagementGpbHelper.toProcessorInfo(processor);
managementListeners.forEach(l -> l.processorClosed(pi));
yprocs.remove(processor);
}
@Override
public void processorStateChanged(Processor processor) {
ProcessorInfo pi = ManagementGpbHelper.toProcessorInfo(processor);
managementListeners.forEach(l -> l.processorStateChanged(pi));
}
}