All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.yamcs.YamcsServer Maven / Gradle / Ivy

There is a newer version: 5.10.9
Show newest version
package org.yamcs;

import static java.util.concurrent.TimeUnit.NANOSECONDS;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.ServiceLoader;
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.logging.ConsoleHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.yamcs.Spec.OptionType;
import org.yamcs.commanding.PreparedCommand;
import org.yamcs.logging.ConsoleFormatter;
import org.yamcs.logging.Log;
import org.yamcs.logging.YamcsLogManager;
import org.yamcs.management.ManagementService;
import org.yamcs.mdb.MdbFactory;
import org.yamcs.protobuf.YamcsInstance.InstanceState;
import org.yamcs.security.CryptoUtils;
import org.yamcs.security.SecurityStore;
import org.yamcs.tctm.Link;
import org.yamcs.templating.ParseException;
import org.yamcs.templating.Template;
import org.yamcs.templating.Variable;
import org.yamcs.time.RealtimeTimeService;
import org.yamcs.time.TimeService;
import org.yamcs.utils.ExceptionUtil;
import org.yamcs.utils.SDNotify;
import org.yamcs.utils.TimeEncoding;
import org.yamcs.utils.YObjectLoader;
import org.yamcs.yarch.YarchDatabase;
import org.yamcs.yarch.rocksdb.RDBFactory;
import org.yamcs.yarch.rocksdb.RdbStorageEngine;
import org.yaml.snakeyaml.Yaml;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.google.common.util.concurrent.Service.State;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.common.util.concurrent.UncheckedExecutionException;

import io.netty.util.ResourceLeakDetector;

/**
 *
 * Yamcs server together with the global instances
 */
public class YamcsServer {

    private static final String CFG_SERVER_ID_KEY = "serverId";
    private static final String CFG_SECRET_KEY = "secretKey";
    public static final String CFG_CRASH_HANDLER_KEY = "crashHandler";

    public static final String GLOBAL_INSTANCE = "_global";

    private static final Log LOG = new Log(YamcsServer.class);

    private static final Pattern INSTANCE_PATTERN = Pattern.compile("yamcs\\.(.*)\\.yaml(.offline)?");
    private static final YamcsServer YAMCS = new YamcsServer();

    // used to schedule various tasks throughout the yamcs server (to avoid each service creating its own)
    ScheduledThreadPoolExecutor timer = new ScheduledThreadPoolExecutor(1,
            new ThreadFactoryBuilder().setNameFormat("YamcsServer-general-executor").build());

    /**
     * During shutdown, allow services this number of seconds for stopping
     */
    public static final int SERVICE_STOP_GRACE_TIME = 10;

    static TimeService realtimeTimeService = new RealtimeTimeService();

    // used for unit tests
    static TimeService mockupTimeService;

    private CrashHandler globalCrashHandler;

    private YamcsServerOptions options = new YamcsServerOptions();
    private Properties properties = new Properties();
    private YConfiguration config;
    private Spec spec;
    private Map> sectionSpecs = new HashMap<>();

    private Map commandOptions = new ConcurrentHashMap<>();
    private Set commandOptionListeners = new CopyOnWriteArraySet<>();

    List globalServiceList;
    Map instances = new LinkedHashMap<>();
    Map instanceTemplates = new HashMap<>();
    List readyListeners = new ArrayList<>();

    private SecurityStore securityStore;
    private PluginManager pluginManager;

    private String serverId;
    private byte[] secretKey;
    int maxOnlineInstances = 1000;
    int maxNumInstances = 20;
    @Deprecated
    Path incomingDir;
    Path instanceDefDir;

    // Set when the shutdown hook triggers
    private boolean shuttingDown = false;

    /**
     * Creates services at global (if instance is null) or instance level. The services are not yet initialized. This
     * must be done in a second step, so that components can ask YamcsServer for other service instantiations.
     *
     * @param instance
     *            if null, then start a global service, otherwise an instance service
     * @param services
     *            list of service configuration; each of them is a string (=classname) or a map
     * @param targetLog
     *            the logger to use for any messages
     * @throws IOException
     * @throws ValidationException
     */
    static List createServices(String instance, List servicesConfig, Log targetLog)
            throws ValidationException, IOException {
        ManagementService managementService = ManagementService.getInstance();
        Set names = new HashSet<>();
        List serviceList = new CopyOnWriteArrayList<>();
        for (YConfiguration servconf : servicesConfig) {
            String servclass;
            String name = null;
            servclass = servconf.getString("class");
            YConfiguration args = servconf.getConfigOrEmpty("args");
            name = servconf.getString("name", servclass.substring(servclass.lastIndexOf('.') + 1));
            String candidateName = name;
            int count = 1;
            while (names.contains(candidateName)) {
                candidateName = name + "-" + count;
                count++;
            }
            name = candidateName;
            boolean enabledAtStartup = servconf.getBoolean("enabledAtStartup", true);

            targetLog.info("Loading service {}", name);
            ServiceWithConfig swc;
            try {
                swc = createService(instance, servclass, name, args, enabledAtStartup);
                serviceList.add(swc);
            } catch (NoClassDefFoundError e) {
                targetLog.error("Cannot create service {}, with arguments {}: class {} not found", name, args,
                        e.getMessage());
                throw e;
            } catch (ValidationException e) {
                throw e;
            } catch (Exception e) {
                targetLog.error("Cannot create service {}, with arguments {}: {}", name, args, e.getMessage());
                throw e;
            }
            if (managementService != null) {
                managementService.registerService(instance, name, swc.service);
            }
            names.add(name);
        }

        return serviceList;
    }

    public static void initServices(String instance, List services) throws InitException {
        for (ServiceWithConfig swc : services) {
            swc.service.init(instance, swc.name, swc.args);
        }
    }

    public  void addGlobalService(
            String name, Class serviceClass, YConfiguration args) throws ValidationException, InitException {

        for (ServiceWithConfig otherService : YAMCS.globalServiceList) {
            if (otherService.getName().equals(name)) {
                throw new ConfigurationException(String.format(
                        "A service named '%s' already exists", name));
            }
        }

        LOG.info("Loading service {}", name);
        ServiceWithConfig swc = createService(null, serviceClass.getName(), name, args, true);
        swc.service.init(null, name, swc.args);
        YAMCS.globalServiceList.add(swc);

        ManagementService managementService = ManagementService.getInstance();
        managementService.registerService(null, name, swc.service);
    }

    /**
     * Starts the specified list of services.
     *
     * @param serviceList
     *            list of service configurations
     * @throws ConfigurationException
     */
    public static void startServices(List serviceList) throws ConfigurationException {
        for (ServiceWithConfig swc : serviceList) {
            if (!swc.enableAtStartup) {
                LOG.debug("NOT starting service {} because enableAtStartup=false (can be manually started)",
                        swc.getName());
                continue;
            }
            LOG.debug("Starting service {}", swc.getName());
            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 void shutDown() {
        long t0 = System.nanoTime();
        LOG.info("Yamcs is shutting down");
        if (SDNotify.isSupported()) {
            SDNotify.sendStoppingNotification();
        }
        for (YamcsServerInstance ys : instances.values()) {
            ys.stopAsync();
        }
        for (YamcsServerInstance ys : instances.values()) {
            LOG.debug("Awaiting termination of instance {}", ys.getName());
            ys.awaitOffline();
            LOG.info("Stopped instance '{}'", ys.getName());
        }
        if (globalServiceList != null) {
            for (ServiceWithConfig swc : globalServiceList) {
                swc.getService().stopAsync();
            }
            for (ServiceWithConfig swc : globalServiceList) {
                LOG.info("Awaiting termination of service {}", swc.getName());
                swc.getService().awaitTerminated();
            }
        }
        instances.clear();
        YarchDatabase.removeInstance(GLOBAL_INSTANCE);

        // Shutdown database when we're sure no services are using it.
        RdbStorageEngine.getInstance().shutdown();

        long stopTime = System.nanoTime() - t0;

        LOG.info("Yamcs stopped in {}ms", NANOSECONDS.toMillis(stopTime));
        YamcsLogManager.shutdown();
        timer.shutdown();
    }

    public static boolean hasInstance(String instance) {
        return YAMCS.instances.containsKey(instance);
    }

    public boolean hasInstanceTemplate(String template) {
        return instanceTemplates.containsKey(template);
    }

    /**
     * The serverId has to be unique among Yamcs servers connected to eachother.
     * 

* It is used to distinguish the data generated by one particular server. * * @return */ public String getServerId() { return serverId; } public byte[] getSecretKey() { return secretKey; } /** * Registers the system-wide availability of a {@link CommandOption}. Command options represent additional arguments * that commands may require, but that are not used by Yamcs in building telecommand binary. *

* An example use case would be a custom TC {@link Link} that may support additional arguments for controlling its * behaviour. *

* While not enforced we recommend to call this method from a {@link Plugin#onLoad(YConfiguration)} hook as this * will avoid registering an option multiple times (attempts to do so would generate an error). * * @param option * the new command option. */ @Experimental public void addCommandOption(CommandOption option) { CommandOption previous = commandOptions.putIfAbsent(option.getId(), option); if (previous != null) { throw new IllegalArgumentException( "A command option '" + option.getId() + "' was already registered with Yamcs"); } if (PreparedCommand.isReservedColumn(option.getId())) { throw new IllegalArgumentException( "Command options may not be named '" + option.getId() + "'. This name is reserved"); } commandOptionListeners.forEach(l -> l.commandOptionAdded(option)); } /** * Returns the command options registered to this instance. */ public Collection getCommandOptions() { return commandOptions.values(); } public boolean hasCommandOption(String id) { return commandOptions.containsKey(id); } public CommandOption getCommandOption(String id) { return commandOptions.get(id); } public void addCommandOptionListener(CommandOptionListener listener) { commandOptionListeners.add(listener); } public void removeCommandOptionListener(CommandOptionListener listener) { commandOptionListeners.remove(listener); } /** * Returns the main Yamcs configuration */ public YConfiguration getConfig() { return config; } /** * Returns the configuration specification for the config returned by {@link #getConfig()}. */ public Spec getSpec() { return spec; } private int getOnlineInstanceCount() { return (int) instances.values().stream().filter(ysi -> ysi.state() != InstanceState.OFFLINE).count(); } /** * Restarts a yamcs instance. * * @param instanceName * the name of the instance * * @return the newly created instance * @throws IOException */ public YamcsServerInstance restartInstance(String instanceName) throws IOException { YamcsServerInstance ysi = instances.get(instanceName); if (ysi.state() == InstanceState.RUNNING || ysi.state() == InstanceState.FAILED) { try { ysi.stop(); } catch (IllegalStateException e) { LOG.warn("Instance did not terminate normally", e); } } YarchDatabase.removeInstance(instanceName); MdbFactory.remove(instanceName); StreamConfig.removeInstance(instanceName); LOG.info("Re-loading instance '{}'", instanceName); YConfiguration instanceConfig = loadInstanceConfig(instanceName); ysi.init(instanceConfig); ysi.startAsync(); try { ysi.awaitRunning(); } catch (IllegalStateException e) { Throwable t = ExceptionUtil.unwind(e.getCause()); LOG.warn("Failed to start instance", t); throw new UncheckedExecutionException(t); } return ysi; } private YConfiguration loadInstanceConfig(String instanceName) { Path configFile = instanceDefDir.resolve(configFileName(instanceName)); if (Files.exists(configFile)) { try (InputStream is = Files.newInputStream(configFile)) { String confPath = configFile.toAbsolutePath().toString(); return new YConfiguration("yamcs." + instanceName, is, confPath); } catch (IOException e) { throw new ConfigurationException("Cannot load configuration from " + configFile.toAbsolutePath(), e); } } else { return YConfiguration.getConfiguration("yamcs." + instanceName); } } private InstanceMetadata loadInstanceMetadata(String instanceName) throws IOException { Path metadataFile = instanceDefDir.resolve("yamcs." + instanceName + ".metadata"); if (Files.exists(metadataFile)) { try (InputStream in = Files.newInputStream(metadataFile)) { Map map = new Yaml().loadAs(in, Map.class); return new InstanceMetadata(map); } } return new InstanceMetadata(); } /** * Stop the instance (it will be offline after this) * * @param instanceName * the name of the instance * * @return the instance * @throws IOException */ public YamcsServerInstance stopInstance(String instanceName) throws IOException { YamcsServerInstance ysi = instances.get(instanceName); if (ysi.state() != InstanceState.OFFLINE) { try { ysi.stop(); } catch (IllegalStateException e) { LOG.error("Instance did not terminate normally", e); } } YarchDatabase.removeInstance(instanceName); MdbFactory.remove(instanceName); Path f = instanceDefDir.resolve(configFileName(instanceName)); if (Files.exists(f)) { LOG.debug("Renaming {} to {}.offline", f.toAbsolutePath(), f.getFileName()); Files.move(f, f.resolveSibling(configFileName(instanceName) + ".offline")); } return ysi; } public void removeInstance(String instanceName) throws IOException { stopInstance(instanceName); Files.deleteIfExists(instanceDefDir.resolve(configFileName(instanceName))); Files.deleteIfExists(instanceDefDir.resolve(configFileName(instanceName) + ".offline")); instances.remove(instanceName); } /** * Start the instance. If the instance is already started, do nothing. * * If the instance is FAILED, restart the instance * * If the instance is OFFLINE, rename the <instance>.yaml.offline to <instance>.yaml and start the * instance * * * @param instanceName * the name of the instance * * @return the instance * @throws IOException */ public YamcsServerInstance startInstance(String instanceName) throws IOException { YamcsServerInstance ysi = instances.get(instanceName); if (ysi.state() == InstanceState.RUNNING) { return ysi; } else if (ysi.state() == InstanceState.FAILED) { return restartInstance(instanceName); } if (getOnlineInstanceCount() >= maxOnlineInstances) { throw new LimitExceededException("Number of online instances already at the limit " + maxOnlineInstances); } if (ysi.state() == InstanceState.OFFLINE) { Path f = instanceDefDir.resolve(configFileName(instanceName) + ".offline"); if (Files.exists(f)) { Files.move(f, instanceDefDir.resolve(configFileName(instanceName))); } YConfiguration instanceConfig = loadInstanceConfig(instanceName); ysi.init(instanceConfig); } ysi.startAsync(); ysi.awaitRunning(); return ysi; } public PluginManager getPluginManager() { return pluginManager; } /** * Add the definition of an additional configuration section to the root Yamcs spec (yamcs.yaml). * * @param key * the name of this section. This represent a direct subkey of the main app config * @param spec * the specification of this configuration section. */ public void addConfigurationSection(String key, Spec spec) { addConfigurationSection(ConfigScope.YAMCS, key, spec); } /** * Add the definition of an additional configuration section to a particulat configuration type * * @param scope * the scope where this section belongs. When using file-based configuration this can be thought of as * the type of the configuration file. * @param key * the name of this section. This represent a direct subkey of the main app config * @param spec * the specification of this configuration section. */ public void addConfigurationSection(ConfigScope scope, String key, Spec spec) { Map specs = sectionSpecs.computeIfAbsent(scope, x -> new HashMap<>()); specs.put(key, spec); } public Map getConfigurationSections(ConfigScope scope) { Map specs = sectionSpecs.get(scope); return specs != null ? specs : Collections.emptyMap(); } /** * Creates a new yamcs instance. * * If the instance already exists an IllegalArgumentException is thrown * * @param name * the name of the new instance * * @param metadata * the metadata associated to this instance (labels or other attributes) * @param offline * if true, the instance will be created offline and it does not need a config * @param config * the configuration for this instance (equivalent of yamcs.instance.yaml) * @return the newly created instance */ public synchronized YamcsServerInstance addInstance(String name, InstanceMetadata metadata, boolean offline, YConfiguration config) { if (instances.containsKey(name)) { throw new IllegalArgumentException(String.format("There already exists an instance named '%s'", name)); } LOG.info("Loading {} instance '{}'", offline ? "offline" : "online", name); YamcsServerInstance ysi = new YamcsServerInstance(name, metadata); ysi.addStateListener(new InstanceStateListener() { @Override public void failed(Throwable failure) { LOG.error("Instance {} failed", name, ExceptionUtil.unwind(failure)); } }); instances.put(name, ysi); if (!offline) { ysi.init(config); } ManagementService.getInstance().registerYamcsInstance(ysi); return ysi; } /** * Create a new instance based on a template. * * @param name * the name of the instance * @param templateName * the name of an available template * @param templateArgs * arguments to use while processing the template * @param labels * labels associated to this instance * @param customMetadata * custom metadata associated with this instance. * @throws IOException * when a disk operation failed * @return the newly create instance */ public synchronized YamcsServerInstance createInstance(String name, String templateName, Map templateArgs, Map labels, Map customMetadata) throws IOException { if (instances.containsKey(name)) { throw new IllegalArgumentException(String.format("There already exists an instance named '%s'", name)); } if (!instanceTemplates.containsKey(templateName)) { throw new IllegalArgumentException(String.format("Unknown template '%s'", templateName)); } Template template = instanceTemplates.get(templateName); // Build instance metadata as a combination of internal properties and custom metadata from the caller InstanceMetadata metadata = new InstanceMetadata(); metadata.setTemplate(templateName); metadata.setTemplateArgs(templateArgs); metadata.setTemplateSource(template.getSource()); metadata.setLabels(labels); customMetadata.forEach((k, v) -> metadata.put(k, v)); String processed = template.process(metadata.getTemplateArgs()); Path confFile = instanceDefDir.resolve(configFileName(name)); try (Writer writer = Files.newBufferedWriter(confFile)) { writer.write(processed); } Path metadataFile = instanceDefDir.resolve("yamcs." + name + ".metadata"); try (Writer writer = Files.newBufferedWriter(metadataFile)) { Map metadataMap = metadata.toMap(); new Yaml().dump(metadataMap, writer); } YConfiguration instanceConfig; try (InputStream fis = Files.newInputStream(confFile)) { String subSystem = "yamcs." + name; String confPath = confFile.toString(); instanceConfig = new YConfiguration(subSystem, fis, confPath); } return addInstance(name, metadata, false, instanceConfig); } public synchronized YamcsServerInstance reconfigureInstance(String name, Map templateArgs, Map labels) throws IOException { YamcsServerInstance ysi = instances.get(name); if (ysi == null) { throw new IllegalArgumentException(String.format("Unknown instance '%s'", name)); } String templateName = ysi.getTemplate(); if (!instanceTemplates.containsKey(templateName)) { throw new IllegalArgumentException(String.format("Unknown template '%s'", templateName)); } Template template = instanceTemplates.get(templateName); // Build instance metadata as a combination of internal properties and custom metadata from the caller InstanceMetadata metadata = ysi.metadata; metadata.setLabels(labels); metadata.setTemplateArgs(templateArgs); metadata.setTemplateSource(template.getSource()); String processed = template.process(metadata.getTemplateArgs()); Path confFile = instanceDefDir.resolve(configFileName(name)); try (Writer writer = Files.newBufferedWriter(confFile)) { writer.write(processed); } Path metadataFile = instanceDefDir.resolve("yamcs." + name + ".metadata"); try (Writer writer = Files.newBufferedWriter(metadataFile)) { Map metadataMap = metadata.toMap(); new Yaml().dump(metadataMap, writer); } return ysi; } private String deriveServerId() { try { String id; if (config.containsKey(CFG_SERVER_ID_KEY)) { id = config.getString(CFG_SERVER_ID_KEY); } else { id = InetAddress.getLocalHost().getHostName(); } serverId = id; LOG.debug("Using serverId {}", serverId); return serverId; } catch (ConfigurationException e) { throw e; } catch (UnknownHostException e) { String msg = "Cannot resolve local host. Make sure it's defined properly or alternatively add 'serverId: ' to yamcs.yaml"; LOG.warn(msg); throw new ConfigurationException(msg, e); } } private void deriveSecretKey() { if (config.containsKey(CFG_SECRET_KEY)) { // Should maybe only allow base64 encoded secret keys secretKey = config.getString(CFG_SECRET_KEY).getBytes(StandardCharsets.UTF_8); } else { LOG.warn("Generating random non-persisted secret key." + " Cryptographic verifications will not work across server restarts." + " Set 'secretKey: ' in yamcs.yaml to avoid this message."); secretKey = CryptoUtils.generateRandomSecretKey(); } } public static List getInstances() { return new ArrayList<>(YAMCS.instances.values()); } public YamcsServerInstance getInstance(String yamcsInstance) { return instances.get(yamcsInstance); } public Set