package org.yamcs; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yamcs.archive.ReplayServer; import org.yamcs.management.ManagementService; import org.yamcs.protobuf.YamcsManagement.MissionDatabase; import org.yamcs.protobuf.YamcsManagement.ServiceInfo; import org.yamcs.protobuf.YamcsManagement.ServiceState; import org.yamcs.protobuf.YamcsManagement.YamcsInstance; import org.yamcs.protobuf.YamcsManagement.YamcsInstances; import org.yamcs.time.RealtimeTimeService; import org.yamcs.time.TimeService; import org.yamcs.utils.LoggingUtils; import org.yamcs.utils.YObjectLoader; import org.yamcs.xtce.Header; import org.yamcs.xtce.XtceDb; import org.yamcs.xtceproc.XtceDbFactory; import org.yamcs.yarch.management.JMXService; import com.google.common.util.concurrent.Service; import com.google.common.util.concurrent.Service.State; /** * * Main yamcs server, starts a Yarch instance for each defined instance * Handles basic requests for retrieving the configured instances, database versions * and retrieve databases in serialized form * * @author nm * */ public class YamcsServer { static Map<String, YamcsServer> instances=new LinkedHashMap<>(); final static private String SERVER_ID_KEY="serverId"; String instance; ReplayServer replay; //instance specific services List<ServiceWithConfig> serviceList; //global services static List<ServiceWithConfig> globalServiceList = null; Logger log; static Logger staticlog = LoggerFactory.getLogger(YamcsServer.class); /**in the shutdown, allow services this number of seconds for stopping*/ public static int SERVICE_STOP_GRACE_TIME = 10; TimeService timeService; static TimeService realtimeTimeService = new RealtimeTimeService(); //used for unit tests static TimeService mockupTimeService; private static String serverId; static YObjectLoader<Service> objLoader = new YObjectLoader<>(); static CrashHandler globalCrashHandler; private CrashHandler crashHandler; YamcsServer(String instance) throws IOException { this.instance = instance; instances.put(instance, this); log = LoggingUtils.getLogger(YamcsServer.class, instance); YConfiguration conf = YConfiguration.getConfiguration("yamcs."+instance); loadTimeService(); ManagementService managementService = ManagementService.getInstance(); StreamInitializer.createStreams(instance); Processor.addProcessorListener(managementService); if(conf.containsKey("crashHandler")) { crashHandler = loadCrashHandler(conf); } else { crashHandler = globalCrashHandler; } List<Object> services = conf.getList("services"); serviceList = createServices(instance, services); } private static CrashHandler loadCrashHandler( YConfiguration conf) throws ConfigurationException, IOException { if(conf.containsKey("crashHandler", "args")) { return YObjectLoader.loadObject(conf.getString("crashHandler", "class"), conf.getMap("crashHandler", "args")); } else { return YObjectLoader.loadObject(conf.getString("crashHandler", "class")); } } /** * Creates services either server-wide (if instance is null) or instance-specific. * The services are not yet started. This must be done in a second step, so * that components can ask YamcsServer for other service instantiations. * * @param services - list of service configuration; each of them is a string (=classname) or a map * @param instance - if null, then start a server-wide service, otherwise an instance-specific service * @throws IOException * @throws ConfigurationException */ @SuppressWarnings("unchecked") private static List<ServiceWithConfig> createServices(String instance, List<Object> servicesConfig) throws ConfigurationException, IOException { ManagementService managementService = ManagementService.getInstance(); List<ServiceWithConfig> serviceList = new CopyOnWriteArrayList<>(); for(Object servobj:servicesConfig) { String servclass; Object args = null; if(servobj instanceof String) { servclass = (String)servobj; } else if (servobj instanceof Map<?, ?>) { Map<String, Object> m = (Map<String, Object>) servobj; servclass = YConfiguration.getString(m, "class"); args = m.get("args"); } else { throw new ConfigurationException("Services can either be specified by classname, or by {class: classname, args: ....} map. Cannot load a service from "+servobj); } staticlog.info("Loading {} service {}", (instance==null)?"server-wide":instance, servclass); ServiceWithConfig swc; try { swc = createService(instance, servclass, servclass, args); serviceList.add(swc); } catch (NoClassDefFoundError e) { staticlog.error("Cannot create service {}, with arguments {}: class {} not found", servclass, args, e.getMessage()); throw e; } catch (Exception t) { staticlog.error("Cannot create service {}, with arguments {}: {}", servclass, args, t.getMessage()); throw t; } managementService.registerService(instance, servclass, swc.service); } return serviceList; } /** * Registers an instance-specific service and starts it up */ public void createAndStartService(String serviceClass, Map<String, Object> args) throws ConfigurationException, IOException { Map<String, Object> serviceConf = new HashMap<>(2); serviceConf.put("class", serviceClass); serviceConf.put("args", args); List<ServiceWithConfig> newServices = createServices(instance, Arrays.asList(serviceConf)); serviceList.addAll(newServices); startServices(newServices); } /** * Starts the specified list of services. * * @param serviceList list of service configurations * @throws ConfigurationException */ public static void startServices(List<ServiceWithConfig> serviceList) throws ConfigurationException { for(ServiceWithConfig swc:serviceList) { swc.service.startAsync(); try { swc.service.awaitRunning(); } catch (IllegalStateException e) { //this happens when it fails, the next check will throw an error in this case } State result = swc.service.state(); if(result==State.FAILED) { throw new ConfigurationException("Failed to start service "+swc.service, swc.service.failureCause()); } } } public static void shutDown() { for(YamcsServer ys: instances.values()) { ys.stop(); } } public void stop() { for(int i = serviceList.size()-1; i>=0; i--) { ServiceWithConfig swc = serviceList.get(i); Service s = swc.service; s.stopAsync(); try { s.awaitTerminated(SERVICE_STOP_GRACE_TIME, TimeUnit.SECONDS); } catch (TimeoutException e) { log.error("Service {} did not stop in {} seconds", s.getClass().getName(), SERVICE_STOP_GRACE_TIME); } catch (IllegalStateException e) { log.error("Service {} was in a bad state: {}", s.getClass().getName(), e.getMessage()); } } } public static boolean hasInstance(String instance) { return instances.containsKey(instance); } public static String getServerId() { return serverId; } public static void setupYamcsServer() throws Exception { YConfiguration c = YConfiguration.getConfiguration("yamcs"); if(c.containsKey("crashHandler")) { globalCrashHandler = loadCrashHandler(c); } else { globalCrashHandler = new LogCrashHandler(); } if(c.containsKey("services")) { List<Object> services=c.getList("services"); globalServiceList = createServices(null, services); } List<String> instArray = null; if (c.containsKey("instances")) { instArray = c.getList("instances"); for(String inst:instArray) { createYamcsInstance(inst); } } if(globalServiceList!=null) { startServices(globalServiceList); } if(instArray!=null) { for(String inst:instArray) { instances.get(inst).start(); } } Thread.setDefaultUncaughtExceptionHandler((t, thrown) -> { String msg = "Uncaught exception '"+thrown+"' in thread "+t+": "+Arrays.toString(thrown.getStackTrace()); staticlog.error(msg); globalCrashHandler.handleCrash("UncaughtException", msg); }); if(System.getenv("YAMCS_DAEMON")==null) { staticlog.info("Server running... press ctrl-c to stop"); } else {//the init.d/yamcs-server depends on this line on the standard output, do not change it (without changing the script also)! System.out.println("yamcsstartup success"); } } /* * Starts all the services */ private void start() { startServices(serviceList); } public static void createYamcsInstance(String name) throws IOException { staticlog.info("Loading instance '{}'", name); if (instances.containsKey(name)) { throw new ConfigurationException(String.format("There already exists an instance named '%s'", name)); } instances.put(name, new YamcsServer(name)); } public static Set<String> getYamcsInstanceNames() { return instances.keySet(); } public static YamcsInstances getYamcsInstances() { YamcsInstances.Builder aisb=YamcsInstances.newBuilder(); for(String name : instances.keySet()) { aisb.addInstance(getYamcsInstance(name)); } return aisb.build(); } public static YamcsInstance getYamcsInstance(String name) { if (!hasInstance(name)) { return null; } YamcsInstance.Builder aib=YamcsInstance.newBuilder(); aib.setName(name); try { MissionDatabase.Builder mdb = MissionDatabase.newBuilder(); YConfiguration c = YConfiguration.getConfiguration("yamcs."+name); if (!c.isList("mdb")) { String configName = c.getString("mdb"); mdb.setConfigName(configName); } XtceDb xtcedb=XtceDbFactory.getInstance(name); mdb.setName(xtcedb.getRootSpaceSystem().getName()); Header h =xtcedb.getRootSpaceSystem().getHeader(); if((h!=null) && (h.getVersion()!=null)) { mdb.setVersion(h.getVersion()); } aib.setMissionDatabase(mdb.build()); } catch (ConfigurationException e) { staticlog.warn("Got error when finding the mission database for instance {}", name, e); } return aib.build(); } private static String deriveServerId() { try { YConfiguration yconf = YConfiguration.getConfiguration("yamcs"); String id; if(yconf.containsKey(SERVER_ID_KEY)) { id = yconf.getString(SERVER_ID_KEY); } else { id = InetAddress.getLocalHost().getHostName(); } serverId = id; staticlog.debug("Using serverId {}", serverId); return serverId; } catch (ConfigurationException e) { throw e; } catch (UnknownHostException e) { String msg = "Java cannot resolve local host (InetAddress.getLocalHost()). Make sure it's defined properly or alternatively add 'serverId: <name>' to yamcs.yaml"; staticlog.warn(msg); throw new ConfigurationException(msg, e); } } private void loadTimeService() throws ConfigurationException, IOException { YConfiguration conf = YConfiguration.getConfiguration("yamcs."+instance); if(conf.containsKey("timeService")) { Map<String, Object> m = conf.getMap("timeService"); String servclass = YConfiguration.getString(m, "class"); Object args = m.get("args"); if(args == null) { timeService = YObjectLoader.loadObject(servclass, instance); } else { timeService = YObjectLoader.loadObject(servclass, instance, args); } } else { timeService = new RealtimeTimeService(); } } public static YamcsServer getInstance(String yamcsInstance) { return instances.get(yamcsInstance); } public TimeService getTimeService() { return timeService; } /** * @param args */ public static void main(String[] args) { if(args.length>0) { printOptionsAndExit(); } try { YConfiguration.setup(); serverId = deriveServerId(); setupSecurity(); JMXService.setup(true); ManagementService.setup(true); setupYamcsServer(); } catch (ConfigurationException e) { staticlog.error("Could not start Yamcs Server", e); System.err.println(e.toString()); System.exit(-1); } catch (Exception e) { staticlog.error("Could not start Yamcs Server", e); System.exit(-1); } } private static void setupSecurity() { org.yamcs.security.Privilege.getInstance(); } private static void printOptionsAndExit() { System.err.println("Usage: yamcs-server.sh"); System.err.println("\t All options are taken from yamcs.yaml"); System.exit(-1); } public static TimeService getTimeService(String yamcsInstance) { if(instances.containsKey(yamcsInstance)) { return instances.get(yamcsInstance).getTimeService(); } else { if(mockupTimeService!=null) { return mockupTimeService; } else { return realtimeTimeService; //happens from unit tests } } } public List<ServiceInfo> getServices() { return getServiceInfo(instance, serviceList); } public static List<ServiceInfo> getGlobalServices() { return getServiceInfo(null, globalServiceList); } private static List<ServiceInfo> getServiceInfo(String instance, List<ServiceWithConfig> serviceList) { List<ServiceInfo> r = new ArrayList<>(serviceList.size()); for(ServiceWithConfig swc: serviceList) { ServiceInfo.Builder sib = ServiceInfo.newBuilder().setName(swc.name).setClassName(swc.serviceClass).setState(ServiceState.valueOf(swc.service.state().name())); if(instance!=null) { sib.setInstance(instance); } r.add(sib.build()); } return r; } public static <T extends Service> T getService(String yamcsInstance, Class<T> serviceClass) { YamcsServer ys = YamcsServer.getInstance(yamcsInstance); if(ys==null) { return null; } return ys.getService(serviceClass); } public static void setMockupTimeService(TimeService timeService) { mockupTimeService = timeService; } public Service getService(String serviceName) { if(serviceList==null) { return null; } for(ServiceWithConfig swc: serviceList) { Service s = swc.service; if(s.getClass().getName().equals(serviceName)) { return s; } } return null; } @SuppressWarnings("unchecked") public <T extends Service> T getService(Class<T> serviceClass) { return (T) getService(serviceClass.getName()); } public static Service getGlobalService(String serviceName) { if(globalServiceList==null) { return null; } synchronized(globalServiceList) { for(ServiceWithConfig swc: globalServiceList) { Service s = swc.service; if(s.getClass().getName().equals(serviceName)) { return s; } } } return null; } @SuppressWarnings("unchecked") public static <T extends Service> T getGlobalService(Class<T> serviceClass) { return (T) getGlobalService(serviceClass.getName()); } private static ServiceWithConfig createService(String instance, String serviceClass, String serviceName, Object args) throws ConfigurationException, IOException { Service serv; if(instance!=null) { if(args == null){ serv = YObjectLoader.loadObject(serviceClass, instance); } else { serv = YObjectLoader.loadObject(serviceClass, instance, args); } } else { if(args == null) { serv = YObjectLoader.loadObject(serviceClass); } else { serv = YObjectLoader.loadObject(serviceClass, args); } } return new ServiceWithConfig(serv, serviceClass, serviceName, args); } //starts a service that has stopped or not yet started private static Service startService(String instance, String serviceName, List<ServiceWithConfig> serviceList) throws ConfigurationException, IOException { for(int i=0; i<serviceList.size(); i++) { ServiceWithConfig swc = serviceList.get(i); if(swc.name.equals(serviceName)) { switch(swc.service.state()) { case RUNNING: case STARTING: //do nothing, service is already starting break; case NEW: //not yet started, start it now swc.service.startAsync(); break; case FAILED: case STOPPING: case TERMINATED: //start a new one swc = createService(instance, swc.serviceClass, serviceName, swc.args); serviceList.set(i, swc); swc.service.startAsync(); break; } return swc.service; } } return null; } public static void startGlobalService(String serviceName) throws ConfigurationException, IOException { startService(null, serviceName, globalServiceList); } public void startService(String serviceName) throws ConfigurationException, IOException { startService(instance, serviceName, serviceList); } private static class ServiceWithConfig { final Service service; final String serviceClass; final String name; final Object args; public ServiceWithConfig(Service service, String serviceClass, String name, Object args) { super(); this.service = service; this.serviceClass = serviceClass; this.name = name; this.args = args; } } public static CrashHandler getCrashHandler(String yamcsInstance) { YamcsServer ys = getInstance(yamcsInstance); if(ys!=null) { return ys.getCrashHandler(); } else { return globalCrashHandler; //may happen if the instance name is not valid (in unit tests) } } private CrashHandler getCrashHandler() { return crashHandler; } }