155 lines
5.8 KiB
Java
155 lines
5.8 KiB
Java
package com.petshop.backend;
|
|
|
|
import org.springframework.boot.builder.SpringApplicationBuilder;
|
|
import org.springframework.context.ConfigurableApplicationContext;
|
|
import org.springframework.context.event.ContextClosedEvent;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Paths;
|
|
import java.time.Duration;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.concurrent.CountDownLatch;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
public class DevStackApplication {
|
|
|
|
private static final Duration WATCH_INTERVAL = Duration.ofSeconds(2);
|
|
|
|
public static void main(String[] args) {
|
|
DevStackController controller = new DevStackController();
|
|
ConfigurableApplicationContext context = null;
|
|
CountDownLatch shutdownLatch = new CountDownLatch(1);
|
|
|
|
try {
|
|
controller.startDatabase();
|
|
context = new SpringApplicationBuilder(BackendApplication.class).run(args);
|
|
ConfigurableApplicationContext appContext = context;
|
|
context.addApplicationListener(event -> {
|
|
if (event instanceof ContextClosedEvent) {
|
|
shutdownLatch.countDown();
|
|
}
|
|
});
|
|
controller.watchDatabase(appContext);
|
|
shutdownLatch.await();
|
|
} catch (InterruptedException ex) {
|
|
Thread.currentThread().interrupt();
|
|
throw new IllegalStateException("Dev stack interrupted", ex);
|
|
} finally {
|
|
controller.stopWatching();
|
|
if (context != null && context.isActive()) {
|
|
context.close();
|
|
}
|
|
controller.stopDatabase();
|
|
}
|
|
}
|
|
|
|
private static final class DevStackController {
|
|
private final Path projectDir = Paths.get("").toAbsolutePath();
|
|
private final AtomicBoolean shuttingDown = new AtomicBoolean(false);
|
|
private ScheduledExecutorService scheduler;
|
|
|
|
private void startDatabase() {
|
|
runCommand(composeCommand("up", "-d", "--wait", "db"));
|
|
}
|
|
|
|
private void stopDatabase() {
|
|
if (!shuttingDown.compareAndSet(false, true)) {
|
|
return;
|
|
}
|
|
stopWatching();
|
|
runCommand(composeCommand("stop", "db"));
|
|
}
|
|
|
|
private void watchDatabase(ConfigurableApplicationContext context) {
|
|
scheduler = Executors.newSingleThreadScheduledExecutor();
|
|
scheduler.scheduleWithFixedDelay(() -> {
|
|
if (shuttingDown.get()) {
|
|
return;
|
|
}
|
|
if (!isDatabaseRunning()) {
|
|
shuttingDown.set(true);
|
|
if (context.isActive()) {
|
|
context.close();
|
|
}
|
|
}
|
|
}, WATCH_INTERVAL.toSeconds(), WATCH_INTERVAL.toSeconds(), TimeUnit.SECONDS);
|
|
}
|
|
|
|
private void stopWatching() {
|
|
if (scheduler != null) {
|
|
scheduler.shutdownNow();
|
|
scheduler = null;
|
|
}
|
|
}
|
|
|
|
private boolean isDatabaseRunning() {
|
|
CommandResult result = runCommand(composeCommand("ps", "--status", "running", "--services", "db"), false);
|
|
return result.exitCode == 0 && result.output.lines()
|
|
.map(String::trim)
|
|
.anyMatch("db"::equals);
|
|
}
|
|
|
|
private List<String> composeCommand(String... args) {
|
|
List<String> command = new ArrayList<>();
|
|
command.add(resolveDockerExecutable());
|
|
command.add("compose");
|
|
command.add("-f");
|
|
command.add("docker-compose.dev.yml");
|
|
for (String arg : args) {
|
|
command.add(arg);
|
|
}
|
|
return command;
|
|
}
|
|
|
|
private String resolveDockerExecutable() {
|
|
String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
|
|
return os.contains("win") ? "docker.exe" : "docker";
|
|
}
|
|
|
|
private void runCommand(List<String> command) {
|
|
CommandResult result = runCommand(command, true);
|
|
if (result.exitCode != 0) {
|
|
throw new IllegalStateException(result.output.isBlank() ? "Command failed: " + String.join(" ", command) : result.output);
|
|
}
|
|
}
|
|
|
|
private CommandResult runCommand(List<String> command, boolean printOutput) {
|
|
ProcessBuilder builder = new ProcessBuilder(command);
|
|
builder.directory(projectDir.toFile());
|
|
builder.redirectErrorStream(true);
|
|
|
|
try {
|
|
Process process = builder.start();
|
|
StringBuilder output = new StringBuilder();
|
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
|
String line;
|
|
while ((line = reader.readLine()) != null) {
|
|
output.append(line).append(System.lineSeparator());
|
|
if (printOutput) {
|
|
System.out.println(line);
|
|
}
|
|
}
|
|
}
|
|
int exitCode = process.waitFor();
|
|
return new CommandResult(exitCode, output.toString().trim());
|
|
} catch (IOException ex) {
|
|
throw new IllegalStateException("Unable to run docker command", ex);
|
|
} catch (InterruptedException ex) {
|
|
Thread.currentThread().interrupt();
|
|
throw new IllegalStateException("Docker command interrupted", ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
private record CommandResult(int exitCode, String output) {
|
|
}
|
|
}
|