From 8cb80d8ada72a6523a65d0fc9166be67ab4061c7 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 10 Mar 2026 21:11:49 -0600 Subject: [PATCH] Sync dev stack --- pom.xml | 18 ++ .../petshop/backend/DevStackApplication.java | 154 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 src/main/java/com/petshop/backend/DevStackApplication.java diff --git a/pom.xml b/pom.xml index 4357996d..9a3e6414 100644 --- a/pom.xml +++ b/pom.xml @@ -117,6 +117,24 @@ exec-maven-plugin 3.1.0 + + docker-up + + exec + + + docker + + compose + -f + docker-compose.dev.yml + up + -d + --wait + db + + + docker-down diff --git a/src/main/java/com/petshop/backend/DevStackApplication.java b/src/main/java/com/petshop/backend/DevStackApplication.java new file mode 100644 index 00000000..a049b7ee --- /dev/null +++ b/src/main/java/com/petshop/backend/DevStackApplication.java @@ -0,0 +1,154 @@ +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) { + } +}