Gymboard/gymboard-cli/source/services.d

160 lines
4.7 KiB
D

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