This commit is contained in:
@@ -20,11 +20,20 @@ ZONE_AUTHORING_URL=http://127.0.0.1:8765
|
|||||||
ZONE_WATCHER_UNIT=watcher.service
|
ZONE_WATCHER_UNIT=watcher.service
|
||||||
ZONE_SCRIPTS_LOG_DIR=/home/zaine/logs
|
ZONE_SCRIPTS_LOG_DIR=/home/zaine/logs
|
||||||
|
|
||||||
EMACS_RUN_DIR=/home/zaine
|
EMACS_RUN_DIR=/home/zaine
|
||||||
EMACS_RUN_LOG=/home/zaine/logs/emacs.log
|
EMACS_RUN_LOG=/home/zaine/logs/emacs.log
|
||||||
COMBINED_RUN_LOG=/home/zaine/logs/combined.log
|
COMBINED_RUN_LOG=/home/zaine/logs/combined.log
|
||||||
|
ADVENTURE_RESOURCES_FREE_DIR=/home/zaine
|
||||||
PLAY_RPG_SAVE_DIR=/home/zaine/master-folder/org-platform/org_backend/data/rpg-saves
|
ADVENTURE_RESOURCES_FREE_LOG=/home/zaine/logs/adventure-resources.log
|
||||||
|
ADVENTURE_RESOURCES_FREE_COMMAND="/usr/bin/docker system prune -af"
|
||||||
|
GUACAMOLE_RUN_DIR=/home/zaine
|
||||||
|
GUACAMOLE_CONTAINER_NAME=guacamole
|
||||||
|
GUACAMOLE_START_LOG=/home/zaine/logs/guacamole-start.log
|
||||||
|
GUACAMOLE_STOP_LOG=/home/zaine/logs/guacamole-stop.log
|
||||||
|
NOSTALGIA_RUN_DIR=/home/zaine/master-folder/projects/_personal/nostalgia
|
||||||
|
NOSTALGIA_RUN_LOG=/home/zaine/logs/nostalgia-prod.log
|
||||||
|
|
||||||
|
PLAY_RPG_SAVE_DIR=/home/zaine/master-folder/org-platform/org_backend/data/rpg-saves
|
||||||
|
|
||||||
# Calendar sync
|
# Calendar sync
|
||||||
NEXTCLOUD_CALDAV_URL=https://nextcloud.zainezq.com/remote.php/dav/calendars/zaine/
|
NEXTCLOUD_CALDAV_URL=https://nextcloud.zainezq.com/remote.php/dav/calendars/zaine/
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
@@ -63,6 +64,10 @@ public class BuildController {
|
|||||||
|
|
||||||
private final AtomicReference<Process> webProcess = new AtomicReference<>();
|
private final AtomicReference<Process> webProcess = new AtomicReference<>();
|
||||||
private final AtomicReference<Process> emacsProcess = new AtomicReference<>();
|
private final AtomicReference<Process> emacsProcess = new AtomicReference<>();
|
||||||
|
private final AtomicReference<Process> resourceProcess = new AtomicReference<>();
|
||||||
|
private final AtomicReference<Process> guacamoleStartProcess = new AtomicReference<>();
|
||||||
|
private final AtomicReference<Process> guacamoleStopProcess = new AtomicReference<>();
|
||||||
|
private final AtomicReference<Process> nostalgiaProcess = new AtomicReference<>();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public BuildController(BuildRunStateService buildRunState) {
|
public BuildController(BuildRunStateService buildRunState) {
|
||||||
@@ -78,6 +83,15 @@ public class BuildController {
|
|||||||
@Value("${emacs.run.dir}") private String emacsRunDirectory;
|
@Value("${emacs.run.dir}") private String emacsRunDirectory;
|
||||||
@Value("${emacs.run.log}") private String emacsRunLogFile;
|
@Value("${emacs.run.log}") private String emacsRunLogFile;
|
||||||
@Value("${combined.run.log}") private String combinedRunLogFile;
|
@Value("${combined.run.log}") private String combinedRunLogFile;
|
||||||
|
@Value("${adventure.resources.free.dir:/home/zaine}") private String resourceFreeDirectory;
|
||||||
|
@Value("${adventure.resources.free.log:/home/zaine/logs/adventure-resources.log}") private String resourceFreeLogFile;
|
||||||
|
@Value("${adventure.resources.free.command:/usr/bin/docker system prune -af}") private String resourceFreeCommand;
|
||||||
|
@Value("${guacamole.run.dir:/home/zaine}") private String guacamoleRunDirectory;
|
||||||
|
@Value("${guacamole.container.name:guacamole}") private String guacamoleContainerName;
|
||||||
|
@Value("${guacamole.start.log:/home/zaine/logs/guacamole-start.log}") private String guacamoleStartLogFile;
|
||||||
|
@Value("${guacamole.stop.log:/home/zaine/logs/guacamole-stop.log}") private String guacamoleStopLogFile;
|
||||||
|
@Value("${nostalgia.run.dir:/home/zaine/master-folder/projects/_personal/nostalgia}") private String nostalgiaRunDirectory;
|
||||||
|
@Value("${nostalgia.run.log:/home/zaine/logs/nostalgia-prod.log}") private String nostalgiaRunLogFile;
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
HEALTH + UPTIME
|
HEALTH + UPTIME
|
||||||
@@ -230,6 +244,154 @@ public class BuildController {
|
|||||||
return streamLogs(emacsRunLogFile);
|
return streamLogs(emacsRunLogFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
ADVENTUREOS COMMANDS
|
||||||
|
========================================================= */
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Free AdventureOS resources",
|
||||||
|
description = "Runs the configured resource cleanup command"
|
||||||
|
)
|
||||||
|
@PostMapping("/adventure/resources/free")
|
||||||
|
public ResponseEntity<String> freeAdventureResources() {
|
||||||
|
return startShellCommand(
|
||||||
|
BuildRunStateService.ADVENTURE_RESOURCES,
|
||||||
|
"resource cleanup",
|
||||||
|
resourceFreeDirectory,
|
||||||
|
resourceFreeLogFile,
|
||||||
|
resourceFreeCommand,
|
||||||
|
resourceProcess
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/adventure/resources/free")
|
||||||
|
public ResponseEntity<String> cancelFreeAdventureResources() {
|
||||||
|
return killProcess(
|
||||||
|
resourceProcess,
|
||||||
|
BuildRunStateService.ADVENTURE_RESOURCES,
|
||||||
|
"resource cleanup"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/adventure/resources/free/status")
|
||||||
|
public ResponseEntity<BuildStatus> getFreeAdventureResourcesStatus() {
|
||||||
|
var s = buildRunState.status(BuildRunStateService.ADVENTURE_RESOURCES);
|
||||||
|
return ResponseEntity.ok(new BuildStatus(s.running(), s.lastRun(), s.lastExitCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/adventure/resources/free/logs")
|
||||||
|
public SseEmitter streamFreeAdventureResourcesLogs() {
|
||||||
|
return streamLogs(resourceFreeLogFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Start Guacamole container",
|
||||||
|
description = "Starts the configured Apache Guacamole container"
|
||||||
|
)
|
||||||
|
@PostMapping("/guacamole/start")
|
||||||
|
public ResponseEntity<String> startGuacamole() {
|
||||||
|
return startCommand(
|
||||||
|
BuildRunStateService.GUACAMOLE_START,
|
||||||
|
"guacamole start",
|
||||||
|
guacamoleRunDirectory,
|
||||||
|
guacamoleStartLogFile,
|
||||||
|
List.of("docker", "start", guacamoleContainerName),
|
||||||
|
guacamoleStartProcess
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/guacamole/start")
|
||||||
|
public ResponseEntity<String> cancelStartGuacamole() {
|
||||||
|
return killProcess(
|
||||||
|
guacamoleStartProcess,
|
||||||
|
BuildRunStateService.GUACAMOLE_START,
|
||||||
|
"guacamole start"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/guacamole/start/status")
|
||||||
|
public ResponseEntity<BuildStatus> getStartGuacamoleStatus() {
|
||||||
|
var s = buildRunState.status(BuildRunStateService.GUACAMOLE_START);
|
||||||
|
return ResponseEntity.ok(new BuildStatus(s.running(), s.lastRun(), s.lastExitCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/guacamole/start/logs")
|
||||||
|
public SseEmitter streamStartGuacamoleLogs() {
|
||||||
|
return streamLogs(guacamoleStartLogFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Stop Guacamole container",
|
||||||
|
description = "Stops the configured Apache Guacamole container"
|
||||||
|
)
|
||||||
|
@PostMapping("/guacamole/stop")
|
||||||
|
public ResponseEntity<String> stopGuacamole() {
|
||||||
|
return startCommand(
|
||||||
|
BuildRunStateService.GUACAMOLE_STOP,
|
||||||
|
"guacamole stop",
|
||||||
|
guacamoleRunDirectory,
|
||||||
|
guacamoleStopLogFile,
|
||||||
|
List.of("docker", "stop", guacamoleContainerName),
|
||||||
|
guacamoleStopProcess
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/guacamole/stop")
|
||||||
|
public ResponseEntity<String> cancelStopGuacamole() {
|
||||||
|
return killProcess(
|
||||||
|
guacamoleStopProcess,
|
||||||
|
BuildRunStateService.GUACAMOLE_STOP,
|
||||||
|
"guacamole stop"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/guacamole/stop/status")
|
||||||
|
public ResponseEntity<BuildStatus> getStopGuacamoleStatus() {
|
||||||
|
var s = buildRunState.status(BuildRunStateService.GUACAMOLE_STOP);
|
||||||
|
return ResponseEntity.ok(new BuildStatus(s.running(), s.lastRun(), s.lastExitCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/guacamole/stop/logs")
|
||||||
|
public SseEmitter streamStopGuacamoleLogs() {
|
||||||
|
return streamLogs(guacamoleStopLogFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Run Nostalgia production container",
|
||||||
|
description = "Runs docker compose up -d --build in the Nostalgia project directory"
|
||||||
|
)
|
||||||
|
@PostMapping("/nostalgia/production")
|
||||||
|
public ResponseEntity<String> runNostalgiaProduction() {
|
||||||
|
return startCommand(
|
||||||
|
BuildRunStateService.NOSTALGIA_PROD,
|
||||||
|
"nostalgia production",
|
||||||
|
nostalgiaRunDirectory,
|
||||||
|
nostalgiaRunLogFile,
|
||||||
|
List.of("docker", "compose", "up", "-d", "--build"),
|
||||||
|
nostalgiaProcess
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/nostalgia/production")
|
||||||
|
public ResponseEntity<String> cancelNostalgiaProduction() {
|
||||||
|
return killProcess(
|
||||||
|
nostalgiaProcess,
|
||||||
|
BuildRunStateService.NOSTALGIA_PROD,
|
||||||
|
"nostalgia production"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/nostalgia/production/status")
|
||||||
|
public ResponseEntity<BuildStatus> getNostalgiaProductionStatus() {
|
||||||
|
var s = buildRunState.status(BuildRunStateService.NOSTALGIA_PROD);
|
||||||
|
return ResponseEntity.ok(new BuildStatus(s.running(), s.lastRun(), s.lastExitCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/nostalgia/production/logs")
|
||||||
|
public SseEmitter streamNostalgiaProductionLogs() {
|
||||||
|
return streamLogs(nostalgiaRunLogFile);
|
||||||
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
LAST RUN TIMESTAMPS
|
LAST RUN TIMESTAMPS
|
||||||
========================================================= */
|
========================================================= */
|
||||||
@@ -355,6 +517,73 @@ public class BuildController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<String> startShellCommand(
|
||||||
|
String commandId,
|
||||||
|
String label,
|
||||||
|
String runDir,
|
||||||
|
String logPath,
|
||||||
|
String command,
|
||||||
|
AtomicReference<Process> processRef) {
|
||||||
|
|
||||||
|
return startCommand(
|
||||||
|
commandId,
|
||||||
|
label,
|
||||||
|
runDir,
|
||||||
|
logPath,
|
||||||
|
List.of("bash", "-lc", command),
|
||||||
|
processRef
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<String> startCommand(
|
||||||
|
String commandId,
|
||||||
|
String label,
|
||||||
|
String runDir,
|
||||||
|
String logPath,
|
||||||
|
List<String> command,
|
||||||
|
AtomicReference<Process> processRef) {
|
||||||
|
|
||||||
|
if (!buildRunState.tryStart(commandId)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.CONFLICT).body(label + " already running");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
File logFile = prepareLogFile(logPath);
|
||||||
|
appendToLog(logFile, "$ " + String.join(" ", command) + "\n\n");
|
||||||
|
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(command);
|
||||||
|
pb.directory(Path.of(runDir).toFile());
|
||||||
|
pb.redirectErrorStream(true);
|
||||||
|
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile));
|
||||||
|
|
||||||
|
log.info("Starting {} in {}", label, runDir);
|
||||||
|
Process process = pb.start();
|
||||||
|
processRef.set(process);
|
||||||
|
buildRunState.markStarted(commandId);
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
int exit = process.waitFor();
|
||||||
|
log.info("{} finished with exit code {}", label, exit);
|
||||||
|
buildRunState.finish(commandId, exit);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
buildRunState.clearRunning(commandId);
|
||||||
|
} finally {
|
||||||
|
processRef.set(null);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(label + " started");
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
buildRunState.clearRunning(commandId);
|
||||||
|
log.error("{} failed to start", label, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body("Failed to start " + label + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ResponseEntity<String> killProcess(
|
private ResponseEntity<String> killProcess(
|
||||||
AtomicReference<Process> processRef,
|
AtomicReference<Process> processRef,
|
||||||
boolean webBuild,
|
boolean webBuild,
|
||||||
@@ -375,6 +604,22 @@ public class BuildController {
|
|||||||
return ResponseEntity.ok("Killed " + label);
|
return ResponseEntity.ok("Killed " + label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<String> killProcess(
|
||||||
|
AtomicReference<Process> processRef,
|
||||||
|
String commandId,
|
||||||
|
String label) {
|
||||||
|
|
||||||
|
Process p = processRef.get();
|
||||||
|
if (p == null || !p.isAlive()) {
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("No running " + label);
|
||||||
|
}
|
||||||
|
p.destroyForcibly();
|
||||||
|
buildRunState.clearRunning(commandId);
|
||||||
|
processRef.set(null);
|
||||||
|
log.info("Killed {}", label);
|
||||||
|
return ResponseEntity.ok("Killed " + label);
|
||||||
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
LOG STREAMING (SSE)
|
LOG STREAMING (SSE)
|
||||||
========================================================= */
|
========================================================= */
|
||||||
@@ -599,4 +844,4 @@ public class BuildController {
|
|||||||
this.lastExitCode = lastExitCode;
|
this.lastExitCode = lastExitCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +1,143 @@
|
|||||||
package org.zaine.app.service;
|
package org.zaine.app.service;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class BuildRunStateService {
|
public class BuildRunStateService {
|
||||||
|
|
||||||
private final AtomicBoolean webBuildRunning = new AtomicBoolean(false);
|
public static final String WEB = "org_web";
|
||||||
private final AtomicBoolean emacsRunning = new AtomicBoolean(false);
|
public static final String EMACS = "emacs";
|
||||||
|
public static final String ADVENTURE_RESOURCES = "adventure_resources";
|
||||||
private volatile Instant emacsLastRun;
|
public static final String GUACAMOLE_START = "guacamole_start";
|
||||||
private volatile Integer emacsLastExitCode;
|
public static final String GUACAMOLE_STOP = "guacamole_stop";
|
||||||
private volatile Instant webLastRun;
|
public static final String NOSTALGIA_PROD = "nostalgia_prod";
|
||||||
private volatile Integer webLastExitCode;
|
|
||||||
|
private final Map<String, CommandState> states = new ConcurrentHashMap<>();
|
||||||
public boolean tryStartWeb() {
|
|
||||||
return webBuildRunning.compareAndSet(false, true);
|
public BuildRunStateService() {
|
||||||
}
|
for (String id : new String[] {
|
||||||
|
WEB,
|
||||||
public void markWebStarted() {
|
EMACS,
|
||||||
webLastRun = Instant.now();
|
ADVENTURE_RESOURCES,
|
||||||
}
|
GUACAMOLE_START,
|
||||||
|
GUACAMOLE_STOP,
|
||||||
public void finishWeb(int exitCode) {
|
NOSTALGIA_PROD
|
||||||
webLastRun = Instant.now();
|
}) {
|
||||||
webLastExitCode = exitCode;
|
states.put(id, new CommandState());
|
||||||
webBuildRunning.set(false);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearWebRunning() {
|
public boolean tryStartWeb() {
|
||||||
webBuildRunning.set(false);
|
return tryStart(WEB);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isWebRunning() {
|
public void markWebStarted() {
|
||||||
return webBuildRunning.get();
|
markStarted(WEB);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean tryStartEmacs() {
|
public void finishWeb(int exitCode) {
|
||||||
return emacsRunning.compareAndSet(false, true);
|
finish(WEB, exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markEmacsStarted() {
|
public void clearWebRunning() {
|
||||||
emacsLastRun = Instant.now();
|
clearRunning(WEB);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void finishEmacs(int exitCode) {
|
public boolean isWebRunning() {
|
||||||
emacsLastRun = Instant.now();
|
return status(WEB).running();
|
||||||
emacsLastExitCode = exitCode;
|
}
|
||||||
emacsRunning.set(false);
|
|
||||||
}
|
public boolean tryStartEmacs() {
|
||||||
|
return tryStart(EMACS);
|
||||||
public void clearEmacsRunning() {
|
}
|
||||||
emacsRunning.set(false);
|
|
||||||
}
|
public void markEmacsStarted() {
|
||||||
|
markStarted(EMACS);
|
||||||
public boolean isEmacsRunning() {
|
}
|
||||||
return emacsRunning.get();
|
|
||||||
}
|
public void finishEmacs(int exitCode) {
|
||||||
|
finish(EMACS, exitCode);
|
||||||
public Map<String, Object> lastRunsSnapshot() {
|
}
|
||||||
Map<String, Object> runs = new LinkedHashMap<>();
|
|
||||||
runs.put("org_web", Map.of(
|
public void clearEmacsRunning() {
|
||||||
"lastRun", nullSafe(webLastRun),
|
clearRunning(EMACS);
|
||||||
"exitCode", nullSafe(webLastExitCode),
|
}
|
||||||
"running", webBuildRunning.get()
|
|
||||||
));
|
public boolean isEmacsRunning() {
|
||||||
runs.put("emacs", Map.of(
|
return status(EMACS).running();
|
||||||
"lastRun", nullSafe(emacsLastRun),
|
}
|
||||||
"exitCode", nullSafe(emacsLastExitCode),
|
|
||||||
"running", emacsRunning.get()
|
public boolean tryStart(String id) {
|
||||||
));
|
return state(id).running.compareAndSet(false, true);
|
||||||
return runs;
|
}
|
||||||
}
|
|
||||||
|
public void markStarted(String id) {
|
||||||
public BuildStatus webStatus() {
|
state(id).lastRun = Instant.now();
|
||||||
return new BuildStatus(webBuildRunning.get(), webLastRun, webLastExitCode);
|
}
|
||||||
}
|
|
||||||
|
public void finish(String id, int exitCode) {
|
||||||
public BuildStatus emacsStatus() {
|
CommandState state = state(id);
|
||||||
return new BuildStatus(emacsRunning.get(), emacsLastRun, emacsLastExitCode);
|
state.lastRun = Instant.now();
|
||||||
}
|
state.lastExitCode = exitCode;
|
||||||
|
state.running.set(false);
|
||||||
private static Object nullSafe(Object value) {
|
}
|
||||||
return value != null ? value : "";
|
|
||||||
}
|
public void clearRunning(String id) {
|
||||||
|
state(id).running.set(false);
|
||||||
public record BuildStatus(boolean running, Instant lastRun, Integer lastExitCode) {}
|
}
|
||||||
}
|
|
||||||
|
public Map<String, Object> lastRunsSnapshot() {
|
||||||
|
Map<String, Object> runs = new LinkedHashMap<>();
|
||||||
|
for (String id : new String[] {
|
||||||
|
WEB,
|
||||||
|
EMACS,
|
||||||
|
ADVENTURE_RESOURCES,
|
||||||
|
GUACAMOLE_START,
|
||||||
|
GUACAMOLE_STOP,
|
||||||
|
NOSTALGIA_PROD
|
||||||
|
}) {
|
||||||
|
CommandState state = state(id);
|
||||||
|
runs.put(id, Map.of(
|
||||||
|
"lastRun", nullSafe(state.lastRun),
|
||||||
|
"exitCode", nullSafe(state.lastExitCode),
|
||||||
|
"running", state.running.get()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return runs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BuildStatus webStatus() {
|
||||||
|
return status(WEB);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BuildStatus emacsStatus() {
|
||||||
|
return status(EMACS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BuildStatus status(String id) {
|
||||||
|
CommandState state = state(id);
|
||||||
|
return new BuildStatus(state.running.get(), state.lastRun, state.lastExitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object nullSafe(Object value) {
|
||||||
|
return value != null ? value : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommandState state(String id) {
|
||||||
|
return states.computeIfAbsent(id, ignored -> new CommandState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CommandState {
|
||||||
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
|
private volatile Instant lastRun;
|
||||||
|
private volatile Integer lastExitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BuildStatus(boolean running, Instant lastRun, Integer lastExitCode) {}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user