Removed runner and made actual cli project.

This commit is contained in:
Andrew Lalis 2023-03-24 10:53:00 +01:00
parent ed6db6298d
commit 116bfa07bd
7 changed files with 288 additions and 67 deletions

View File

@ -43,6 +43,7 @@ public class SecurityConfig {
.authorizeHttpRequests() .authorizeHttpRequests()
.requestMatchers(// Allow the following GET endpoints to be public. .requestMatchers(// Allow the following GET endpoints to be public.
HttpMethod.GET, HttpMethod.GET,
"/status",
"/exercises", "/exercises",
"/leaderboards", "/leaderboards",
"/gyms/**", "/gyms/**",

16
gymboard-cli/.gitignore vendored Normal file
View File

@ -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

9
gymboard-cli/dub.json Normal file
View File

@ -0,0 +1,9 @@
{
"authors": [
"Andrew Lalis"
],
"copyright": "Copyright © 2023, Andrew Lalis",
"description": "CLI for developing Gymboard",
"license": "proprietary",
"name": "gymboard-cli"
}

16
gymboard-cli/source/app.d Normal file
View File

@ -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!");
}

87
gymboard-cli/source/cli.d Normal file
View File

@ -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.");
}
}
}

View File

@ -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);
}
}

View File

@ -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() {
}
}