2023-03-24 09:53:00 +00:00
|
|
|
module services;
|
|
|
|
|
2023-03-26 11:33:10 +00:00
|
|
|
import std.array;
|
2023-03-24 09:53:00 +00:00
|
|
|
import std.process;
|
|
|
|
import std.stdio;
|
|
|
|
import std.string;
|
|
|
|
import std.typecons;
|
|
|
|
import core.thread;
|
2023-03-26 11:33:10 +00:00
|
|
|
import core.atomic;
|
|
|
|
|
|
|
|
import consolecolors;
|
2023-03-24 09:53:00 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
2023-03-26 11:33:10 +00:00
|
|
|
public bool startService(const ServiceInfo service) {
|
2023-03-24 09:53:00 +00:00
|
|
|
if (service.name !in serviceRunners || !serviceRunners[service.name].isRunning) {
|
|
|
|
// Start all dependencies first.
|
|
|
|
foreach (string depName; service.dependencies) {
|
2023-03-26 11:33:10 +00:00
|
|
|
bool result = startService(getServiceByName(depName).get);
|
|
|
|
if (!result) {
|
|
|
|
cwritefln("Can't start %s because dependency %s couldn't be started.".red, service.name.orange, depName.orange);
|
|
|
|
return false;
|
2023-03-24 09:53:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Then start the process.
|
2023-03-26 11:33:10 +00:00
|
|
|
cwritefln("Starting service %s".green, service.name.white);
|
2023-03-24 09:53:00 +00:00
|
|
|
ProcessPipes pipes = pipeShell(
|
|
|
|
service.startupCommand,
|
|
|
|
Redirect.all,
|
|
|
|
null,
|
|
|
|
Config.none,
|
|
|
|
service.workingDir
|
|
|
|
);
|
|
|
|
ServiceRunner runner = new ServiceRunner(pipes);
|
|
|
|
runner.start();
|
|
|
|
serviceRunners[service.name] = runner;
|
2023-03-26 11:33:10 +00:00
|
|
|
cwritefln("Service %s started.".green, service.name.white);
|
2023-03-24 09:53:00 +00:00
|
|
|
}
|
2023-03-26 11:33:10 +00:00
|
|
|
cwritefln("Service %s is already started.".green, service.name.white);
|
|
|
|
return true;
|
2023-03-24 09:53:00 +00:00
|
|
|
}
|
|
|
|
|
2023-03-26 11:33:10 +00:00
|
|
|
public bool stopService(const ServiceInfo service) {
|
2023-03-24 09:53:00 +00:00
|
|
|
if (service.name in serviceRunners && serviceRunners[service.name].isRunning) {
|
|
|
|
int exitStatus = serviceRunners[service.name].stopService();
|
2023-03-26 11:33:10 +00:00
|
|
|
cwritefln("Service %s exited with code <orange>%d</orange>.".green, service.name.white, exitStatus);
|
|
|
|
return true;
|
2023-03-24 09:53:00 +00:00
|
|
|
}
|
2023-03-26 11:33:10 +00:00
|
|
|
cwritefln("Service %s already stopped.".green, service.name.white);
|
|
|
|
return true;
|
2023-03-24 09:53:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2023-03-26 11:33:10 +00:00
|
|
|
|
|
|
|
public void showLogs(const ServiceInfo service, size_t lineCount = 50) {
|
|
|
|
if (service.name in serviceRunners) {
|
|
|
|
serviceRunners[service.name].showOutput(lineCount);
|
|
|
|
} else {
|
|
|
|
cwritefln("Service %s has not been started.".red, service.name.orange);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void follow(const ServiceInfo service) {
|
|
|
|
if (service.name in serviceRunners) {
|
|
|
|
serviceRunners[service.name].setFollowing(true);
|
|
|
|
} else {
|
|
|
|
cwritefln("Service %s has not been started.".red, service.name.orange);
|
|
|
|
}
|
|
|
|
}
|
2023-03-24 09:53:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
class ServiceRunner : Thread {
|
|
|
|
private Pid processId;
|
|
|
|
private File processStdin;
|
|
|
|
private File processStdout;
|
|
|
|
private File processStderr;
|
|
|
|
public Nullable!int exitStatus;
|
2023-03-26 11:33:10 +00:00
|
|
|
private FileGobbler stdoutGobbler;
|
|
|
|
private FileGobbler stderrGobbler;
|
2023-03-24 09:53:00 +00:00
|
|
|
|
|
|
|
public this(ProcessPipes pipes) {
|
|
|
|
super(&this.run);
|
|
|
|
this.processId = pipes.pid();
|
|
|
|
this.processStdin = pipes.stdin();
|
|
|
|
this.processStdout = pipes.stdout();
|
|
|
|
this.processStderr = pipes.stderr();
|
2023-03-26 11:33:10 +00:00
|
|
|
this.stdoutGobbler = new FileGobbler(pipes.stdout());
|
|
|
|
this.stderrGobbler = new FileGobbler(pipes.stderr());
|
2023-03-24 09:53:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private void run() {
|
2023-03-26 11:33:10 +00:00
|
|
|
this.stdoutGobbler.start();
|
|
|
|
this.stderrGobbler.start();
|
2023-03-24 09:53:00 +00:00
|
|
|
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() {
|
2023-03-26 11:33:10 +00:00
|
|
|
if (!exitStatus.isNull) return exitStatus.get();
|
|
|
|
|
2023-03-24 09:53:00 +00:00
|
|
|
version(Posix) {
|
2023-03-26 11:33:10 +00:00
|
|
|
import core.sys.posix.signal : SIGINT, SIGTERM;
|
|
|
|
kill(this.processId, SIGINT);
|
2023-03-24 09:53:00 +00:00
|
|
|
kill(this.processId, SIGTERM);
|
|
|
|
} else version(Windows) {
|
|
|
|
kill(this.processId);
|
|
|
|
}
|
|
|
|
return wait(this.processId);
|
|
|
|
}
|
2023-03-26 11:33:10 +00:00
|
|
|
|
|
|
|
public void showOutput(size_t lineCount = 50) {
|
|
|
|
this.stdoutGobbler.showOutput(lineCount);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void showErrorOutput(size_t lineCount = 50) {
|
|
|
|
this.stderrGobbler.showOutput(lineCount);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setFollowing(bool following) {
|
|
|
|
this.stdoutGobbler.setFollowing(following);
|
|
|
|
this.stderrGobbler.setFollowing(following);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class FileGobbler : Thread {
|
|
|
|
private string[] lines;
|
|
|
|
private File file;
|
|
|
|
private bool following;
|
|
|
|
|
|
|
|
public this(File file) {
|
|
|
|
super(&this.run);
|
|
|
|
this.file = file;
|
|
|
|
this.following = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void run() {
|
|
|
|
string line;
|
|
|
|
while ((line = stripRight(this.file.readln())) !is null) {
|
|
|
|
if (atomicLoad(this.following)) {
|
|
|
|
writeln(line);
|
|
|
|
}
|
|
|
|
synchronized(this) {
|
|
|
|
this.lines ~= line;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void showOutput(size_t lineCount = 50) {
|
|
|
|
synchronized(this) {
|
|
|
|
size_t startIdx = 0;
|
|
|
|
if (lines.length > lineCount) {
|
|
|
|
startIdx = lines.length - lineCount;
|
|
|
|
}
|
|
|
|
foreach (line; lines[startIdx .. $]) writeln(line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void setFollowing(bool f) {
|
|
|
|
atomicStore(this.following, f);
|
|
|
|
}
|
2023-03-24 09:53:00 +00:00
|
|
|
}
|