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

com.grey.naf.Launcher Maven / Gradle / Ivy

/*
 * Copyright 2010-2021 Yusef Badri - All rights reserved.
 * NAF is distributed under the terms of the GNU Affero General Public License, Version 3 (AGPLv3).
 */
package com.grey.naf;

import java.time.Clock;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import com.grey.base.config.SysProps;
import com.grey.base.config.XmlConfig;
import com.grey.base.utils.FileOps;
import com.grey.base.utils.CommandParser;
import com.grey.naf.reactor.CM_Listener;
import com.grey.naf.reactor.Dispatcher;
import com.grey.naf.reactor.config.DispatcherConfig;
import com.grey.naf.nafman.NafManClient;
import com.grey.naf.nafman.NafManConfig;
import com.grey.naf.nafman.PrimaryAgent;
import com.grey.naf.errors.NAFConfigException;
import com.grey.logging.Factory;
import com.grey.logging.Parameters;
import com.grey.logging.SinkLogger;
import com.grey.logging.Logger;

public class Launcher
{
	private static final String SYSPROP_BOOTLVL = "greynaf.boot.loglevel";

	private static final int F_DUMPENV = 1 << 0;
	private static final int F_QUIET = 1 << 1;  //mainly for the benefit of the unit tests

	static final String[] options = new String[]{"c:", "appname:", "cmd:", "remote:", "logger:", "dumpenv", "q"};

	private static boolean announcedNAF;
	static {
		announceNAF();
	}

	private static final Clock clock = Clock.systemUTC();

	protected final BaseOptsHandler baseOptions = new BaseOptsHandler();
	protected final CommandParser cmdParser;
	protected final String[] cmdlineArgs;

	/**
	 * Create the default NAFMAN config.
	 * Applications that with to customise the NAFMAN options (before loading naf.xml config file) should override this.
	 * They can disable NAFMAN by returning null.
	 */
	protected NafManConfig.Builder createNafManConfig(NAFConfig nafcfg) {return new NafManConfig.Builder(nafcfg);}

	/**
	 * Applications that need to install NAFMAN commands and resources should override this
	 */
	protected void setupNafMan(ApplicationContextNAF appctx) {}

	/**
	 * This launches default NAF applications driven entirely by command-line options and typically with a naf.xml style config file.
	 * Custom applications (which don't necessarily even have a naf.xml config) would subclass the Launcher and implement their
	 * own main() which would:
	 * - Instantiate their Launcher class
	 * - Call one of the app.execute() variants
	 */
	public static void main(String[] args) throws Exception {
		Launcher app = new Launcher(args);
		app.execute("nafconfig");
	}

	public Launcher(String[] args) {
		cmdlineArgs = args;
		cmdParser = new CommandParser(baseOptions);
	}

	/**
	 * Applications that don't wish to customise NAFConfig can call this from their main() to accept the default setup.
	 * Otherwise they should call the execute(String, NAFConfig.Builder) variant.
	 */
	protected void execute(String dflt_appname) throws Exception {
		execute(dflt_appname, new NAFConfig.Builder());
	}

	/**
	 * This initialises and executes the application, by performing the following actions:
	 * - Parses the command line.
	 * - Loads the naf.xml config file if any, creating the resulting NAFConfig instance.
	 * - Creates the NAFMAN config
	 * - Creates the ApplicationContextNAF instance for this application
	 * - Calls app.Execute() to launch the application
	 * This method also recognises if we're issuing a NAFMAN command rather than launching an application.
	 *
	 * Applications which don't use a naf.xml config file can call this method without specifying one, to get the benefit of
	 * its command-line parsing, while still being able to supply their required NAFConfig and NAFMAN settings.
	 * Then again, NAF applications don't need to use the Launcher at all and can have an independent main() that manually creates
	 * an ApplicationContextNAF and the required Dispatchers, Naflets, listeners, etc.
	 */
	protected void execute(String dflt_appname, NAFConfig.Builder bldrNafConfig) throws Exception {
		int param1 = cmdParser.parse(cmdlineArgs);
		if (param1 == -1) return;

		if (baseOptions.isFlagSet(F_DUMPENV)) {
			SysProps.dump(System.getProperties(), System.out);
			com.grey.base.utils.PkgInfo.dumpEnvironment(getClass(), System.out);
		}
		String appname = baseOptions.appname;
		if (appname == null) appname = dflt_appname;

		Logger bootlog = createBootLogger(baseOptions);
		Logger.setThreadLogger(bootlog);
		System.out.println("Created NAF Boot Logger="+bootlog.getClass().getName()+" for application="+appname);

		NAFConfig nafcfg = loadConfigFile(bldrNafConfig, bootlog);
		if (nafcfg == null) return;

		if (baseOptions.cmd != null) {
			// in client mode - we are simply issuing a NAMAN command
			try {
				issueCommand(appname, nafcfg, bootlog);
			} catch (Throwable ex) {
				System.out.println("Failed to issue NAFMAN command - "+com.grey.base.ExceptionUtils.summary(ex));
			}
			return;
		}

		bootlog.info("NAF Paths - Root = "+nafcfg.getPathRoot());
		bootlog.info("NAF Paths - Config = "+nafcfg.getPathConf());
		bootlog.info("NAF Paths - Var = "+nafcfg.getPathVar());
		bootlog.info("NAF Paths - Logs = "+nafcfg.getPathLogs());
		bootlog.info("NAF Paths - Temp = "+nafcfg.getPathTemp());
		if (nafcfg.getBasePort() != NAFConfig.RSVPORT_ANON) bootlog.info("Base Port="+nafcfg.getBasePort());

		NafManConfig nafmanConfig = configureNafMan(nafcfg);
		bootlog.info("Configured NAFMAN config="+nafmanConfig);

		ApplicationContextNAF appctx = ApplicationContextNAF.create(appname, nafcfg, nafmanConfig);
		bootlog.info("Created Application Context - "+appctx);

		if (nafmanConfig != null) {
			setupNafMan(appctx);
		}
		appExecute(appctx, param1, bootlog);
	}

	/**
	 * This is the default NAF application, which launches a set of Dispatchers driven by a naf.xml style config file.
	 * Custom applications should override this to redirect the execution flow into their own code.
	 * It doesn't make any sense for applications without a naf.xml config file to enter this method as it would be a
	 * no-op without any Dispatchers defined in there.
	 * Conversely, there isn't much point in overriding this for applications which do have a naf.xml config file, as
	 * this represents the logical control flow for them.
	 */
	protected void appExecute(ApplicationContextNAF appctx, int param1, Logger bootlog) throws Exception {
		if (param1 != cmdlineArgs.length) { //we don't expect any params
			cmdParser.usage(cmdlineArgs, 0, "Excess params="+(cmdlineArgs.length-param1));
			return;
		}
		executeDispatchers(appctx, bootlog);
	}

	private NAFConfig loadConfigFile(NAFConfig.Builder bldrNafConfig, Logger bootlog) {
		if (baseOptions.cfgpath != null && !baseOptions.cfgpath.isEmpty()) {
			java.io.File fhConfig = new java.io.File(baseOptions.cfgpath);
			bootlog.info("Loading NAF config file: "+baseOptions.cfgpath+" => "+fhConfig.getAbsolutePath());

			if (!fhConfig.exists()) {
				System.out.println("NAF Config file not found: "+baseOptions.cfgpath);
				return null;
			}
			bldrNafConfig = bldrNafConfig.withConfigFile(fhConfig.getAbsolutePath());
		}
		return bldrNafConfig.build();
	}

	private NafManConfig configureNafMan(NAFConfig nafcfg) {
		NafManConfig nafmanConfig = null;
		NafManConfig.Builder bldrNafMan = createNafManConfig(nafcfg);
		XmlConfig xmlcfgNafMan = nafcfg.getNode("nafman");

		if (xmlcfgNafMan == null || !xmlcfgNafMan.getBool("@enabled", true)) {
			// This means the naf.xml config contains a nafman block that explicitly disables NAFMAN.
			// Even if the application supplied a non-null builder, this overrides.
			bldrNafMan = null;
		}

		if (bldrNafMan != null) {
			bldrNafMan.withXmlConfig(xmlcfgNafMan);
			nafmanConfig = bldrNafMan.build();
		}
		return nafmanConfig;
	}

	private static void executeDispatchers(ApplicationContextNAF appctx, Logger bootlog) throws java.io.IOException {
		long systime_boot = clock.millis();
		bootlog.info("NAF BOOTING in "+new java.io.File(".").getCanonicalPath());

		java.util.List dispatchers = launchConfiguredDispatchers(appctx, bootlog);
		if (dispatchers == null) throw new NAFConfigException("No Dispatchers are configured");

		long systime2 = clock.millis();
		bootlog.info("NAF BOOTED in time="+(systime2-systime_boot)+"ms - Dispatchers="+dispatchers.size());
		FileOps.flush(bootlog);

		// wait for Dispatchers to exit - if they ever do
		while (dispatchers.size() != 0) {
			for (int idx = 0; idx != dispatchers.size(); idx++) {
				Dispatcher d = dispatchers.get(idx);
				Dispatcher.STOPSTATUS stopsts = d.waitStopped(5000, false);
				if (stopsts != Dispatcher.STOPSTATUS.ALIVE) {
					dispatchers.remove(d);
					bootlog.info("Launcher has reaped Dispatcher="+d.getName()+" - remaining="+dispatchers.size());
					break;
				}
			}
		}
		bootlog.info("All Dispatchers have exited - terminating\n\n");
		FileOps.flush(bootlog);
	}

	public static List launchConfiguredDispatchers(ApplicationContextNAF appctx, Logger log) throws java.io.IOException {
		NAFConfig nafcfg = appctx.getConfig();
		XmlConfig[] cfgdispatchers = nafcfg.getDispatchers();
		if (cfgdispatchers == null) return null;
		List dlst = new ArrayList<>(cfgdispatchers.length);
		log.info("NAF: Launching configured Dispatchers="+cfgdispatchers.length);

		// Do separate loops to create and start the Dispatchers, so that they're all guaranteed to be in single-threaded
		// mode until all have initialised.
		for (XmlConfig dcfg : cfgdispatchers) {
			DispatcherConfig def = new DispatcherConfig.Builder().withXmlConfig(dcfg).build();
			Dispatcher dsptch = Dispatcher.create(appctx, def, log);
			dlst.add(dsptch);

			/*
			 * Create any Naflets that are defined in the config file.
			 * Of course applications can also create their own Naflets and inject them into a Dispatcher via the same
			 * loadNaflet() method used here, which can be called before or after the Dispatcher's start() method.
			 */
			XmlConfig[] nafletsConfig = dcfg.getSections("naflets/naflet"+XmlConfig.XPATH_ENABLED);
			if (nafletsConfig != null) {
				dsptch.getLogger().info("Creating Naflets="+nafletsConfig.length+" for Dispatcher="+dsptch.getName());
				for (XmlConfig appcfg : nafletsConfig) {
					Object obj = NAFConfig.createEntity(appcfg, null, Naflet.class, true,
							new Class[]{String.class, dsptch.getClass(), appcfg.getClass()},
							new Object[]{null, dsptch, appcfg});
					Naflet app = Naflet.class.cast(obj);
					dsptch.getLogger().info("Created Naflet="+app.getName()+" for Dispatcher="+dsptch.getName());
					if (app.getName().charAt(0) == '_') {
						throw new NAFConfigException("Dispatcher="+dsptch.getName()+" has invalid Naflet name (starts with underscore) - "+app.getName());
					}
					dsptch.loadRunnable(app);
				}
			}
		}

		// log the initial config
		String txt = dumpConfig(appctx);
		log.info("Initialisation of the configured NAF Dispatchers is now complete\n"+txt);

		// Now starts the multi-threaded phase
		for (int idx = 0; idx != dlst.size(); idx++) {
			dlst.get(idx).start();
		}
		return dlst;
	}

	private void issueCommand(String appname, NAFConfig nafcfg, Logger log) throws java.io.IOException {
		if (baseOptions.isFlagSet(F_QUIET)) log = new SinkLogger(appname);
		String rsp;
		if (baseOptions.hostport != null) {
			rsp = NafManClient.submitCommand(baseOptions.cmd, baseOptions.hostport, log);
		} else {
			rsp = NafManClient.submitLocalCommand(baseOptions.cmd, nafcfg, log);
		}
		if (rsp != null && !baseOptions.isFlagSet(F_QUIET)) System.out.println("NAFMAN Response="+rsp.length()+":\n\n"+rsp);
	}

	// It's ultimately up to the user how they configure the logging framework, but some initialisation code
	// here and there writes directly to stdout, so by default we direct the boot logger to stdout as well, so
	// that it and the raw stdout writes can be captured/redirected as one.
	// The advantage of writing to stdout with bootlog rather than System.out() is that we get properly
	// timestamped entries.
	private static Logger createBootLogger(BaseOptsHandler opts) throws java.io.IOException {
		if (opts.logname != null) return Factory.getLogger(opts.logname);
		Logger.LEVEL lvl = Logger.LEVEL.valueOf(SysProps.get(SYSPROP_BOOTLVL, Logger.LEVEL.TRC.toString()));
		Parameters params = new Parameters.Builder()
				.withStream(System.out)
				.withLogLevel(lvl)
				.build();
		return Factory.getLogger(params, "NAF-bootlog");
	}

	private static String dumpConfig(ApplicationContextNAF appctx) {
		String txt = "Dumping Laucher setup:\n";
		Collection dispatchers = appctx.getDispatchers();
		PrimaryAgent nafmanPrimary = appctx.getNamedItem(PrimaryAgent.class.getName(), null);
		txt += "ApplicationContext="+appctx.getName()+", Dispatchers="+dispatchers.size()+":";
		if (nafmanPrimary != null) txt += ", NAFMAN-Primary="+nafmanPrimary.getDispatcher().getName();

		Collection listeners = appctx.getListeners();
		txt += "\nListeners="+listeners.size();
		for (CM_Listener l : listeners) {
			txt += "\n- "+l.getName()+": Port="+l.getPort()+", Server="+l.getServerFactory()+" - Dispatcher="+l.getDispatcher().getName();
		}
		return txt;
	}

	// This may not look MT-safe, but it only gets called early on during startup, when still single-threaded
	public static synchronized void announceNAF() {
		if (announcedNAF) return;
		announcedNAF = true;
		com.grey.base.utils.PkgInfo.announceJAR(Launcher.class, "NAF", null);
	}


	public static class BaseOptsHandler
		extends CommandParser.OptionsHandler
	{
		public String cfgpath;
		public String appname;
		public String cmd;
		public String hostport;  //qualifies cmd option
		public String logname;
		public int flags;

		public BaseOptsHandler() {super(options, 0, -1);}
		public void setFlag(int f) {flags |= f;}
		public boolean isFlagSet(int f) {return ((flags & f) != 0);}

		@Override
		public void setOption(String opt) {
			if (opt.equals("dumpenv")) {
				setFlag(F_DUMPENV);
			} else if (opt.equals("q")) {
				setFlag(F_QUIET);
			} else {
				throw new Error("Missing case for bool-option="+opt);
			}
		}

		@Override
		public void setOption(String opt, String val) {
			if (opt.equals("c")) {
				cfgpath = val;
			} else if (opt.equals("appname")) {
				appname = val;
			} else if (opt.equals("cmd")) {
				cmd = val;
			} else if (opt.equals("remote")) {
				hostport = val;
			} else if (opt.equals("logger")) {
				logname = val;
			} else {
				throw new Error("Missing case for value-option="+opt);
			}
		}

		@Override
		public String displayUsage() {
			return Launcher.displayUsage();
		}

		@Override
		public String toString() {
			return super.toString()+" - cfgpath="+cfgpath+", appname="+appname+", cmd="+cmd+", hostport="+hostport+", logname="+logname+", flags=0x"+Integer.toHexString(flags);
		}
	}

	public static String displayUsage()
	{
		String txt = "\t[-c naf-config-file] [-appname name] [-logger name] [-dumpenv]";
		txt += "\nClient Mode:";
		txt += "\n\t-cmd cmd-URL [-c naf-config-file] [-q]";
		txt += "\n\t-cmd cmd-URL -remote host:port [-q]";
		return txt;
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy