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 composeCommand(String... args) { List 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 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 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) { } }