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

org.praxislive.launcher.Launcher Maven / Gradle / Ivy

Go to download

Forest-of-actors runtime supporting real-time systems and real-time recoding - bringing aspects of Erlang, Smalltalk and Extempore to Java.

The newest version!
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2021 Neil C Smith.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License version 3
 * along with this work; if not, see http://www.gnu.org/licenses/
 * 
 *
 * Please visit https://www.praxislive.org if you need additional information or
 * have any questions.
 */
package org.praxislive.launcher;

import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.praxislive.core.MainThread;
import org.praxislive.hub.Hub;
import java.io.File;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.concurrent.Callable;
import java.util.function.BiConsumer;
import org.praxislive.code.CodeCompilerService;
import org.praxislive.core.Lookup;
import org.praxislive.core.Root;
import org.praxislive.core.services.LogLevel;
import org.praxislive.core.services.LogService;
import org.praxislive.core.services.SystemManagerService;
import org.praxislive.hub.net.NetworkCoreFactory;
import picocli.CommandLine;

/**
 * Main entry point for parsing command line arguments and launching a
 * {@link Hub}. This launcher uses {@link NetworkCoreFactory} to build a
 * distributed hub capable of proxying to other local or remote hubs. It can
 * also support a server to allow the built hub to be controlled externally.
 * 

* This class is mainly intended to be called from the main method entry point. * The additional required context should provide support to launch another * instance of this process. *

* This launcher understands the following command line switches - *

    *
  • -f / --file {file} : a script file or project directory to run.
  • *
  • -p / --port {auto | 0 .. 65535} : launch a server on the specified port. * If 0 or auto, a port is automatically chosen. Unless --network is specified, * connections are only supported over local loopback. The port is reported to * standard out as "Listening at : [port]".
  • *
  • -n / --network {all | CIDR mask} : launch a server that supports remote * connections, from all addresses or matching mask. Implies --port auto if not * otherwise specified.
  • *
  • -i / --interactive : allow for controlling the hub via PCL commands over * the command line.
  • *
  • --child : configure the process to run as a child process. Implies --port * auto unless specified.
  • *
*

* For other purposes, use the {@link Hub#builder()} directly to create and * launch a Hub. */ public class Launcher { static final String LISTENING_STATUS = "Listening at : "; /** * Main entry point. * * @param context a context implementation providing for launching a child * of this process * @param args the command line arguments, possibly amended */ public static void main(Launcher.Context context, String[] args) { int ret = new CommandLine(new Exec(context)).execute(args); System.exit(ret); } static SocketAddress parseListeningLine(String line) { if (line.startsWith(LISTENING_STATUS)) { try { int port = Integer.parseInt(line.substring(LISTENING_STATUS.length()).trim()); return new InetSocketAddress(InetAddress.getLoopbackAddress(), port); } catch (Exception ex) { throw new IllegalArgumentException(ex); } } throw new IllegalArgumentException(); } /** * Context for launching child processes. */ public static interface Context { /** * Create a {@link ProcessBuilder} to launch another instance of this * process. * * @param javaOptions additional JVM options to pass to process * @param arguments additional command line arguments for process * @return process builder */ public ProcessBuilder createChildProcessBuilder(List javaOptions, List arguments); /** * Provide an optional file to be run on launch, eg. for embedding the * launcher in a project. If the context provides an auto-run file and * the file option is specified, an exception will be thrown on launch. * An implementation that doesn't want this behaviour should return an * empty optional if a file is specified. * * @return optional file to run on launch */ public default Optional autoRunFile() { return Optional.empty(); } } @CommandLine.Command(mixinStandardHelpOptions = true) private static class Exec implements Callable { @CommandLine.Option(names = {"-f", "--file"}, description = "A script file or project directory to run.") private File file; @CommandLine.Option(names = {"-p", "--port"}, converter = PortConverter.class, description = "{auto | 0 .. 65535} : Launch a server on the specified port. " + "If 0 or auto, a port is automatically chosen. " + "Unless --network is specified, connections are only supported over local loopback. " + "The port is reported to standard out as \"Listening at : [port]\".") private Integer port; @CommandLine.Option(names = {"-n", "--network"}, description = "{all | CIDR mask} : Launch a server that supports remote " + "connections, from all addresses or matching mask. " + "Implies --port auto if not otherwise specified.") private String network; @CommandLine.Option(names = {"-i", "--interactive"}, description = {"Allow for controlling the hub via PCL commands over the command line."}) private boolean interactive; @CommandLine.Option(names = "--child", description = {"Configure the process to run as a child process. " + "Implies --port auto unless specified.", "A child process may not respond to normal termination signals, " + "instead relying on the parent process to be terminated." }) private boolean child; @CommandLine.Option(names = "--show-environment", description = {"Output useful debugging information about process environment."}) private boolean showEnv; @CommandLine.Option(names = "--no-signal-handlers", description = "Don't install signal handlers.", hidden = true) private boolean noSignals; @CommandLine.Parameters(description = "Extra command line arguments.") private List extraArgs; private final Launcher.Context context; private Exec(Launcher.Context context) { this.context = context; } @Override public Integer call() throws Exception { if (showEnv) { outputEnvironmentInfo(); } if (child) { if (port == null) { port = 0; } if (!noSignals) { installChildSignalOverrides(); } // assert script == null? } final boolean requireServer = network != null || port != null; if (requireServer && port == null) { port = 0; } final boolean allowRemote = requireServer && network != null; final String cidr; if (allowRemote && !network.equalsIgnoreCase("all")) { cidr = network; } else { cidr = null; } var autoRun = context.autoRunFile(); if (file != null) { if (child) { error("Cannot specify --file and --child"); return 1; } if (autoRun.isPresent()) { error("Cannot specify --file when auto-run file exists"); return 1; } } final String script; var scriptFile = autoRun.orElse(file); if (scriptFile != null) { scriptFile = scriptFile.getAbsoluteFile(); if (scriptFile.isDirectory()) { scriptFile = new File(scriptFile, "project.pxp"); } if (!scriptFile.exists()) { error("No file found at " + scriptFile); return 1; } try { script = "set _PWD " + scriptFile.getParentFile().toURI() + "\n" + Files.readString(scriptFile.toPath()); } catch (Exception ex) { error("Unable to read script at " + scriptFile); return 1; } } else { script = null; } if (!requireServer && !interactive && script == null) { if (showEnv) { return 0; } else { error("WARNING : Nothing to do, exiting."); error("Use --help to see options"); return 1; } } final var main = new MainThreadImpl(); int exitValue = 0; do { var coreBuilder = NetworkCoreFactory.builder() .childLauncher(new ChildLauncherImpl(context)) .exposeServices(List.of( CodeCompilerService.class, LogService.class, SystemManagerService.class )); if (requireServer) { coreBuilder.enableServer(); coreBuilder.serverPort(port); } if (allowRemote && cidr != null) { coreBuilder.allowRemoteServerConnection(cidr); } else if (allowRemote) { coreBuilder.allowRemoteServerConnection(); } var coreFactory = coreBuilder.build(); var hubBuilder = Hub.builder() .setCoreRootFactory(coreFactory) .extendLookup(main); if (script != null) { hubBuilder.addExtension(new ScriptRunner(List.of(script))); } if (interactive) { var terminalIO = createTerminalIO(); hubBuilder.addExtension(terminalIO); } var logLevel = LogLevel.INFO; hubBuilder.addExtension(new LogServiceImpl(logLevel)); hubBuilder.extendLookup(logLevel); var hub = hubBuilder.build(); hub.start(); if (requireServer) { var serverInfo = coreFactory.awaitInfo(30, TimeUnit.SECONDS); port = serverInfo.serverAddress() .filter(a -> a instanceof InetSocketAddress) .map(a -> (InetSocketAddress) a) .orElseThrow().getPort(); out(LISTENING_STATUS + port); } main.run(hub); exitValue = hub.exitValue(); } while (requireServer); return exitValue; } private void installChildSignalOverrides() { // override same as JLine terminal var signals = new String[]{"INT", "QUIT", "TSTP", "CONT", "INFO", "WINCH"}; try { ServiceLoader.load(Signals.class) .findFirst() .ifPresent(s -> { for (String signal : signals) { s.register(signal, () -> { System.getLogger(Launcher.class.getName()) .log(System.Logger.Level.DEBUG, () -> "Received signal : " + signal); }); } }); } catch (Exception ex) { System.getLogger(Launcher.class.getName()) .log(System.Logger.Level.ERROR, "Error registering signal handler", ex); } } private Root createTerminalIO() { try { return ServiceLoader.load(TerminalIOProvider.class) .findFirst() .map(p -> p.createTerminalIO(Lookup.EMPTY)) .orElseGet(FallbackTerminalIO::new); } catch (Exception ex) { System.getLogger(Launcher.class.getName()) .log(System.Logger.Level.ERROR, "Error creating terminal IO, defaulting to fallback", ex); return new FallbackTerminalIO(); } } private void outputEnvironmentInfo() { try { var handle = ProcessHandle.current(); handle.info().command().ifPresent(s -> out("Command :\n" + s + "\n")); handle.info().arguments().ifPresent(args -> out("Arguments :\n" + (Arrays.toString(args)) + "\n")); handle.info().commandLine().ifPresent(s -> out("Full command line :\n" + s + "\n")); var modulePath = System.getProperty("jdk.module.path"); out("Java module path :"); out((modulePath == null || modulePath.isBlank() ? "[EMPTY]" : modulePath)); out(""); var classPath = System.getProperty("java.class.path"); out("Java class path :"); out((classPath == null || classPath.isBlank() ? "[EMPTY]" : classPath)); out(""); out("Environment variables :"); out("" + System.getenv()); out(""); } catch (Exception e) { System.getLogger(Launcher.class.getName()) .log(System.Logger.Level.DEBUG, "Exception thrown outputting environment info.", e); } } private void out(String msg) { System.out.println(msg); } private void error(String msg) { var ansiMsg = CommandLine.Help.Ansi.AUTO.string( "@|bold,red " + msg + "|@" ); System.out.println(ansiMsg); } } private static class PortConverter implements CommandLine.ITypeConverter { @Override public Integer convert(String arg0) throws Exception { if (arg0.equalsIgnoreCase("auto")) { return 0; } int port = Integer.parseInt(arg0); if (port < 0 || port > 65535) { throw new IllegalArgumentException("Port value out of range"); } return port; } } private static class MainThreadImpl implements MainThread { private final Thread main; private final BlockingQueue queue; private MainThreadImpl() { this.main = Thread.currentThread(); this.queue = new LinkedBlockingQueue<>(); } @Override public void runLater(Runnable task) { queue.add(task); } @Override public boolean isMainThread() { return Thread.currentThread() == main; } private void run(Hub hub) { while (hub.isAlive()) { try { var task = queue.poll(500, TimeUnit.MILLISECONDS); if (task != null) { task.run(); } } catch (Throwable t) { System.getLogger(Launcher.class.getName()) .log(System.Logger.Level.ERROR, "", t); } } drain: for (;;) { try { var task = queue.poll(500, TimeUnit.MILLISECONDS); if (task != null) { task.run(); } else { break drain; } } catch (Throwable t) { System.getLogger(Launcher.class.getName()) .log(System.Logger.Level.ERROR, "", t); } } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy