diff --git a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/SecurityConfig.java b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/SecurityConfig.java index c40ed74..c57f831 100644 --- a/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/SecurityConfig.java +++ b/gymboard-api/src/main/java/nl/andrewlalis/gymboard_api/config/SecurityConfig.java @@ -43,6 +43,7 @@ public class SecurityConfig { .authorizeHttpRequests() .requestMatchers(// Allow the following GET endpoints to be public. HttpMethod.GET, + "/status", "/exercises", "/leaderboards", "/gyms/**", diff --git a/gymboard-cli/.gitignore b/gymboard-cli/.gitignore new file mode 100644 index 0000000..22448b7 --- /dev/null +++ b/gymboard-cli/.gitignore @@ -0,0 +1,16 @@ +.dub +docs.json +__dummy.html +docs/ +/gymboard-cli +gymboard-cli.so +gymboard-cli.dylib +gymboard-cli.dll +gymboard-cli.a +gymboard-cli.lib +gymboard-cli-test-* +*.exe +*.pdb +*.o +*.obj +*.lst diff --git a/gymboard-cli/dub.json b/gymboard-cli/dub.json new file mode 100644 index 0000000..23edbcc --- /dev/null +++ b/gymboard-cli/dub.json @@ -0,0 +1,9 @@ +{ + "authors": [ + "Andrew Lalis" + ], + "copyright": "Copyright © 2023, Andrew Lalis", + "description": "CLI for developing Gymboard", + "license": "proprietary", + "name": "gymboard-cli" +} \ No newline at end of file diff --git a/gymboard-cli/source/app.d b/gymboard-cli/source/app.d new file mode 100644 index 0000000..f954041 --- /dev/null +++ b/gymboard-cli/source/app.d @@ -0,0 +1,16 @@ +import std.stdio; + +import cli; +import services; + +void main() { + ServiceManager serviceManager = new ServiceManager(); + CliHandler cliHandler = new CliHandler(); + cliHandler.register("service", new ServiceCommand(serviceManager)); + writeln("Gymboard CLI: Type \"help\" for more information. Type \"exit\" to exit the CLI."); + while (!cliHandler.shouldExit) { + cliHandler.readAndHandleCommand(); + } + serviceManager.stopAll(); + writeln("Goodbye!"); +} diff --git a/gymboard-cli/source/cli.d b/gymboard-cli/source/cli.d new file mode 100644 index 0000000..7602432 --- /dev/null +++ b/gymboard-cli/source/cli.d @@ -0,0 +1,87 @@ +module cli; + +import std.stdio; +import std.string; +import std.uni; + +interface CliCommand { + void handle(string[] args); +} + +class CliHandler { + private CliCommand[string] commands; + public bool shouldExit = false; + + public void register(string name, CliCommand command) { + this.commands[name] = command; + } + + public void readAndHandleCommand() { + string[] commandAndArgs = readln().strip().split!isWhite(); + if (commandAndArgs.length == 0) return; + string command = commandAndArgs[0].toLower(); + if (command == "help") { + showHelp(); + } else if (command == "exit") { + shouldExit = true; + } else if (command in commands) { + commands[command].handle(commandAndArgs.length > 1 ? commandAndArgs[1 .. $] : []); + } else { + writefln!"Unknown command \"%s\"."(command); + } + } +} + +void showHelp() { + writeln(q"HELP +Gymboard CLI: A tool for streamlining development. + +Commands: + +help Shows this message. +exit Exits the CLI, stopping any running services. +HELP"); +} + +import services; + +class ServiceCommand : CliCommand { + private ServiceManager serviceManager; + + public this(ServiceManager serviceManager) { + this.serviceManager = serviceManager; + } + + void handle(string[] args) { + if (args.length == 0) { + writeln("Missing subcommand."); + return; + } + string subcommand = args[0]; + if (subcommand == "status") { + auto statuses = serviceManager.getStatus(); + if (statuses.length == 0) { + writeln("No services running."); + } + foreach (status; statuses) { + writefln!"%s: Running = %s, Exit code = %s"(status.name, status.running, status.exitCode); + } + } else if (subcommand == "start") { + if (args.length < 2) { + writeln("Missing service name."); + return; + } + auto result = serviceManager.startService(args[1]); + writeln(result.msg); + } else if (subcommand == "stop") { + if (args.length < 2) { + writeln("Missing service name."); + return; + } + auto result = serviceManager.stopService(args[1]); + writeln(result.msg); + } else { + writeln("Unknown subcommand."); + } + } +} diff --git a/gymboard-cli/source/services.d b/gymboard-cli/source/services.d new file mode 100644 index 0000000..58d0fb7 --- /dev/null +++ b/gymboard-cli/source/services.d @@ -0,0 +1,159 @@ +module services; + +import std.process; +import std.stdio; +import std.string; +import std.typecons; +import core.thread; + +struct ServiceInfo { + string name; + string[] dependencies; + string workingDir; + string startupCommand; +} + +const SERVICES = [ + ServiceInfo( + "api", + ["docker-deps"], + "../gymboard-api", + "./gen_keys.d && ./mvnw spring-boot:run -Dspring-boot.run.profiles=development" + ), + ServiceInfo( + "cdn", + [], + "../gymboard-cdn", + "./mvnw spring-boot:run -Dspring-boot.run.profiles=development" + ), + ServiceInfo( + "search", + ["docker-deps"], + "../gymboard-search", + "./mvnw spring-boot:run -Dspring-boot.run.profiles=development" + ), + ServiceInfo( + "app", + [], + "../gymboard-app", + "npm install && quasar dev" + ), + ServiceInfo( + "docker-deps", + [], + "../", + "docker-compose up" + ) +]; + +Nullable!(const(ServiceInfo)) getServiceByName(string name) { + static foreach (service; SERVICES) { + if (service.name == name) return nullable(service); + } + return Nullable!(const(ServiceInfo)).init; +} + +struct ServiceStatus { + string name; + bool running; + Nullable!int exitCode; +} + +class ServiceManager { + private ServiceRunner[string] serviceRunners; + + public Tuple!(bool, "started", string, "msg") startService(string name) { + auto info = getServiceByName(name); + if (info.isNull) return tuple!("started", "msg")(false, "Invalid service name."); + const ServiceInfo service = info.get(); + if (service.name !in serviceRunners || !serviceRunners[service.name].isRunning) { + // Start all dependencies first. + foreach (string depName; service.dependencies) { + auto result = startService(depName); + if (!result.started) { + return tuple!("started", "msg")( + false, + format!"Couldn't start dependency \"%s\": %s"(depName, result.msg) + ); + } + } + // Then start the process. + writefln!"Starting service: %s"(service.name); + ProcessPipes pipes = pipeShell( + service.startupCommand, + Redirect.all, + null, + Config.none, + service.workingDir + ); + ServiceRunner runner = new ServiceRunner(pipes); + runner.start(); + serviceRunners[service.name] = runner; + return tuple!("started", "msg")(true, "Service started."); + } + return tuple!("started", "msg")(true, "Service already running."); + } + + public Tuple!(bool, "stopped", string, "msg") stopService(string name) { + auto info = getServiceByName(name); + if (info.isNull) return tuple!("stopped", "msg")(false, "Invalid service name."); + const ServiceInfo service = info.get(); + if (service.name in serviceRunners && serviceRunners[service.name].isRunning) { + int exitStatus = serviceRunners[service.name].stopService(); + return tuple!("stopped", "msg")( + true, + format!"Service exited with status %d."(exitStatus) + ); + } + return tuple!("stopped", "msg")(true, "Service already stopped."); + } + + public void stopAll() { + foreach (name, runner; serviceRunners) { + runner.stopService(); + } + } + + public ServiceStatus[] getStatus() { + ServiceStatus[] statuses; + foreach (name, runner; serviceRunners) { + statuses ~= ServiceStatus(name, runner.isRunning, runner.exitStatus); + } + return statuses; + } +} + +class ServiceRunner : Thread { + private Pid processId; + private File processStdin; + private File processStdout; + private File processStderr; + public Nullable!int exitStatus; + + public this(ProcessPipes pipes) { + super(&this.run); + this.processId = pipes.pid(); + this.processStdin = pipes.stdin(); + this.processStdout = pipes.stdout(); + this.processStderr = pipes.stderr(); + } + + private void run() { + Tuple!(bool, "terminated", int, "status") result = tryWait(this.processId); + while (!result.terminated) { + Thread.sleep(msecs(1000)); + result = tryWait(this.processId); + } + this.exitStatus = result.status; + } + + public int stopService() { + version(Posix) { + import core.sys.posix.signal : SIGTERM; + kill(this.processId, SIGTERM); + } else version(Windows) { + kill(this.processId); + } + return wait(this.processId); + } +} diff --git a/runner.d b/runner.d deleted file mode 100755 index 81fd820..0000000 --- a/runner.d +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env rdmd - -/** - * TODO: This module will eventually serve as some sort of setup script for - * if/when it becomes too complicated to just start the services. It should - * run as a CLI thing for entering commands to start/stop things. - */ -module runner; - -import std.process; -import std.stdio; -import std.string; -import std.uni; -import core.thread; - -int main() { - bool running = true; - writeln("Gymboard CLI: Type \"help\" for more information. Type \"exit\" to exit the CLI."); - while (running) { - string[] commandAndArgs = readln().strip.split!isWhite; - if (commandAndArgs.length == 0) continue; - string command = commandAndArgs[0].toLower(); - if (command == "help") { - showHelp(); - } else if (command == "exit") { - running = false; - } else if (command in commands) { - commands[command](commandAndArgs.length > 1 ? commandAndArgs[1 .. $] : []); - } else { - writefln!"Unknown command \"%s\"."(command); - } - } - writeln("Goodbye!"); - return 0; -} - -alias CommandFunction = void function(string[] args); - -CommandFunction[string] commands; - -void registerCommand(string name, CommandFunction func) { - commands[name] = func; -} - -void showHelp() { - writeln(q"HELP -Gymboard CLI: A tool for streamlining development. - -Commands: - -help Shows this message. -exit Exits the CLI, stopping any running services. -HELP"); -} - -class ProcessRunner : Thread { - private Pid processId; - - public this(ProcessPipes pipes) { - super(&this.run); - this.processId = pipes.pid(); - } - - private void run() { - - } -}