From 35999e39dc8540883143af100faf474ac4451317 Mon Sep 17 00:00:00 2001 From: Satya <35016438+satran004@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:20:11 +0530 Subject: [PATCH] Kill any previously running processes before starting yaci-cli. (#96) * #94 Create pid files for each process and perform cleanup during CLI startup * #94 If yaci-cli.pid exists, kill the process and then invoke yaci-cli * Bump version --- .../cardano/yacicli/YaciCliApplication.java | 10 ++ .../localcluster/ClusterStartService.java | 13 +- .../localcluster/ogmios/OgmiosService.java | 18 ++- .../yacistore/YaciStoreService.java | 12 ++ .../cardano/yacicli/util/ProcessUtil.java | 118 ++++++++++++++++++ config/version | 2 +- npm/yaci-devkit/package-lock.json | 12 +- npm/yaci-devkit/start.mjs | 55 +++++++- 8 files changed, 222 insertions(+), 18 deletions(-) diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/YaciCliApplication.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/YaciCliApplication.java index 95ceda5..876c1ff 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/YaciCliApplication.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/YaciCliApplication.java @@ -1,8 +1,11 @@ package com.bloxbean.cardano.yacicli; import com.bloxbean.cardano.yacicli.localcluster.config.GenesisConfig; +import com.bloxbean.cardano.yacicli.util.ProcessUtil; +import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -11,6 +14,8 @@ @EnableConfigurationProperties(GenesisConfig.class) @Slf4j public class YaciCliApplication { + @Autowired + private ProcessUtil processUtil; public static void main(String[] args) { new SpringApplicationBuilder(YaciCliApplication.class) @@ -18,6 +23,11 @@ public static void main(String[] args) { .run(args); } + @PostConstruct + public void stopRunningProcesses() { + processUtil.killRunningProcesses(); + } + @PreDestroy public void onShutDown() { } diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterStartService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterStartService.java index 98a542e..d866e3c 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterStartService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ClusterStartService.java @@ -33,6 +33,8 @@ @RequiredArgsConstructor @Slf4j public class ClusterStartService { + public static final String SUBMIT_API_PROCESS_NAME = "submit-api"; + public static final String NODE_PROCESS_NAME = "node"; private final ClusterConfig clusterConfig; private final ClusterPortInfoHelper clusterPortInfoHelper; private final ProcessUtil processUtil; @@ -123,6 +125,7 @@ public void stopCluster(Consumer writer) { if (processes != null && processes.size() > 0) writer.accept(info("Trying to stop the running cluster ...")); + boolean error = false; for (Process process : processes) { if (process != null && process.isAlive()) { process.descendants().forEach(processHandle -> { @@ -139,6 +142,12 @@ public void stopCluster(Consumer writer) { } } + if (!error) { + //clean pid files + processUtil.deletePidFile(NODE_PROCESS_NAME); + processUtil.deletePidFile(SUBMIT_API_PROCESS_NAME); + } + logs.clear(); submitApiLogs.clear(); } catch (Exception e) { @@ -200,7 +209,7 @@ private Process startNode(Path clusterFolder, ClusterInfo clusterInfo, Consumer< builder.directory(nodeStartDir); writer.accept(success("Starting node from directory : " + nodeStartDir.getAbsolutePath())); - Process process = processUtil.startLongRunningProcess("Node", builder, logs, writer); + Process process = processUtil.startLongRunningProcess(NODE_PROCESS_NAME, builder, logs, writer); if (process == null) return null; Path nodeSocketPath = clusterFolder.resolve(ClusterConfig.NODE_FOLDER_PREFIX).resolve("node.sock"); @@ -234,7 +243,7 @@ private Process startSubmitApi(ClusterInfo clusterInfo, Path clusterFolder, Cons File submitApiStartDir = new File(clusterFolderPath); builder.directory(submitApiStartDir); - Process process = processUtil.startLongRunningProcess("SubmitApi", builder, submitApiLogs, writer); + Process process = processUtil.startLongRunningProcess(SUBMIT_API_PROCESS_NAME, builder, submitApiLogs, writer); if (process == null) return null; writer.accept(success("Started submit api : http://localhost:" + clusterPortInfoHelper.getSubmitApiPort(clusterInfo))); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ogmios/OgmiosService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ogmios/OgmiosService.java index 3b2d86f..785d980 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ogmios/OgmiosService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/ogmios/OgmiosService.java @@ -34,19 +34,17 @@ @RequiredArgsConstructor @Slf4j public class OgmiosService { + private final static String OGMIOS_PROCESS_NAME = "ogmios"; + private final static String KUPO_PROCESS_NAME = "kupo"; private final ApplicationConfig appConfig; private final ClusterService clusterService; private final ClusterConfig clusterConfig; private final ClusterPortInfoHelper clusterPortInfoHelper; + private final TemplateEngine templateEngine; + private final ProcessUtil processUtil; private List processes = new ArrayList<>(); - @Autowired - private TemplateEngine templateEngine; - - @Autowired - private ProcessUtil processUtil; - private Queue ogmiosLogs = EvictingQueue.create(300); private Queue kupoLogs = EvictingQueue.create(300); @@ -181,6 +179,7 @@ public void handleClusterStopped(ClusterStopped clusterStopped) { if (processes != null && processes.size() > 0) writeLn(info("Trying to stop ogmios/kupo ...")); + boolean error = false; for (Process process : processes) { if (process != null && process.isAlive()) { process.descendants().forEach(processHandle -> { @@ -195,10 +194,17 @@ public void handleClusterStopped(ClusterStopped clusterStopped) { writeLn(success("Killed : " + process)); } else { writeLn(error("Process could not be killed : " + process)); + error = true; } } } + if (!error) { + //clean pid files + processUtil.deletePidFile(OGMIOS_PROCESS_NAME); + processUtil.deletePidFile(KUPO_PROCESS_NAME); + } + ogmiosLogs.clear(); } catch (Exception e) { log.error("Error stopping process", e); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java index a561f9a..18c43c3 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/localcluster/yacistore/YaciStoreService.java @@ -12,6 +12,7 @@ import com.bloxbean.cardano.yacicli.localcluster.events.ClusterStopped; import com.bloxbean.cardano.yacicli.util.PortUtil; import com.bloxbean.cardano.yacicli.util.ProcessStream; +import com.bloxbean.cardano.yacicli.util.ProcessUtil; import com.google.common.collect.EvictingQueue; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,12 +36,14 @@ @RequiredArgsConstructor @Slf4j public class YaciStoreService { + private static final String STORE_PROCESS_NAME = "yaci-store"; private final ApplicationConfig appConfig; private final ClusterService clusterService; private final ClusterConfig clusterConfig; private final JreResolver jreResolver; private final YaciStoreConfigBuilder yaciStoreConfigBuilder; private final YaciStoreCustomDbHelper customDBHelper; + private final ProcessUtil processUtil; private List processes = new ArrayList<>(); @@ -73,6 +76,7 @@ public void handleClusterStarted(ClusterStarted clusterStarted) { Process process = startStoreApp(clusterInfo, era); if (process != null) processes.add(process); + // Process viewerProcess = startViewerApp(clusterStarted.getClusterName()); // processes.add(viewerProcess); } catch (Exception e) { @@ -197,6 +201,8 @@ private Process startStoreApp(ClusterInfo clusterInfo, Era era) throws IOExcepti writeLn("###########################################################################################################################"); } + processUtil.createProcessId(STORE_PROCESS_NAME, process); + return process; } @@ -249,6 +255,7 @@ public void handleClusterStopped(ClusterStopped clusterStopped) { if (processes != null && processes.size() > 0) writeLn(info("Trying to stop yaci-store ...")); + boolean error = false; for (Process process : processes) { if (process != null && process.isAlive()) { process.descendants().forEach(processHandle -> { @@ -267,6 +274,11 @@ public void handleClusterStopped(ClusterStopped clusterStopped) { } } + if (!error) { + //clean pid files + processUtil.deletePidFile(STORE_PROCESS_NAME); + } + logs.clear(); } catch (Exception e) { log.error("Error stopping process", e); diff --git a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/util/ProcessUtil.java b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/util/ProcessUtil.java index 5652336..1a40c46 100644 --- a/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/util/ProcessUtil.java +++ b/applications/cli/src/main/java/com/bloxbean/cardano/yacicli/util/ProcessUtil.java @@ -1,10 +1,17 @@ package com.bloxbean.cardano.yacicli.util; import com.bloxbean.cardano.yacicli.commands.common.ExecutorHelper; +import com.bloxbean.cardano.yacicli.localcluster.ClusterConfig; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; import java.util.Queue; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -16,6 +23,7 @@ @RequiredArgsConstructor public class ProcessUtil { private final ExecutorHelper executorHelper; + private final ClusterConfig clusterConfig; public boolean executeAndFinish(ProcessBuilder processBuilder, String scriptPurpose, Consumer writer) { try { @@ -75,11 +83,121 @@ public Process startLongRunningProcess(String processName, ProcessBuilder builde return null; } + createProcessId(processName, process); + //stop consuming error stream errorStream.stop(); return process; } + public String createProcessId(String processName, Process process) { + try { + var yaciCliHome = clusterConfig.getYaciCliHome(); + // Validate the yaciCliHome directory + Path homePath = Paths.get(yaciCliHome); + if (!Files.exists(homePath)) { + writeLn(error("yaci-cli home directory does not exist: " + yaciCliHome)); + return null; + } + + if (!Files.isDirectory(homePath)) { + writeLn(error("yaci-cli home path is not a directory: " + yaciCliHome)); + return null; + } + + Path pids = homePath.resolve("pids"); + if (!Files.exists(pids)) + pids.toFile().mkdirs(); + + Path pidFilePath = pids.resolve(processName + ".pid"); + long pid = process.pid(); + Files.writeString(pidFilePath, String.valueOf(pid)); + + // Return the full path to the created PID file as a string + return pidFilePath.toString(); + } catch (IOException e) { + writeLn(error("Failed to create the PID file: " + e.getMessage())); + return null; + } + } + + public void deletePidFile(String processName) { + var yaciCliHome = clusterConfig.getYaciCliHome(); + // Validate the yaciCliHome directory + Path homePath = Paths.get(yaciCliHome); + Path pids = homePath.resolve("pids"); + + var pidPath = pids.resolve(processName + ".pid"); + if (Files.exists(pidPath)) { + pidPath.toFile().delete(); + writeLn(info("Deleted pid file : " + processName + ".pid")); + } + } + + public void killRunningProcesses() { + var yaciCliHome = clusterConfig.getYaciCliHome(); + + // Validate the yaciCliHome directory + Path homePath = Paths.get(yaciCliHome); + if (!Files.exists(homePath)) { + writeLn(error("The yaciCliHome directory does not exist: " + yaciCliHome)); + return; + } + if (!Files.isDirectory(homePath)) { + writeLn(error("The yaciCliHome path is not a directory: " + yaciCliHome)); + return; + } + + // Resolve the pids directory + Path pidsDir = homePath.resolve("pids"); + if (!Files.exists(pidsDir) || !Files.isDirectory(pidsDir)) { + writeLn(error("The pids directory does not exist or is not a directory: " + pidsDir)); + return; + } + + List pidList = new ArrayList<>(); + try (DirectoryStream pidFiles = Files.newDirectoryStream(pidsDir, "*.pid")) { + for (Path pidFile : pidFiles) { + try { + // Read the PID from the file + String pidString = Files.readString(pidFile).trim(); + if (!pidString.isEmpty()) { + pidList.add(Long.parseLong(pidString)); + } + } catch (IOException | NumberFormatException e) { + writeLn(error("Failed to read or parse PID file: " + pidFile + " - " + e.getMessage())); + } + } + } catch (IOException e) { + writeLn(error("Failed to list PID files in directory: " + pidsDir + " : " + e.getMessage())); + return; + } + + var deletedPids = new ArrayList<>(); + for (Long pid : pidList) { + try { + ProcessHandle.of(pid) + .ifPresent(processHandle -> { + processHandle.descendants().forEach(ph -> { + deletedPids.add(ph.pid()); + ph.destroyForcibly(); + }); + var result = processHandle.destroyForcibly(); + if (!result) { + writeLn(error("Failed to kill process with PID : " + pid)); + } else { + deletedPids.add(processHandle.pid()); + } + }); + } catch (Exception e) { + writeLn(error("Failed to kill process with PID: " + pid + " - " + e.getMessage())); + } + } + + if (!deletedPids.isEmpty()) { + writeLn(info("Found existing processes. Killed processes with pids: " + deletedPids)); + } + } public String executeAndReturnOutput(ProcessBuilder processBuilder) { try { StringBuilder sb = new StringBuilder(); diff --git a/config/version b/config/version index 4b55f4b..88974dd 100644 --- a/config/version +++ b/config/version @@ -1,2 +1,2 @@ -tag=0.10.0-preview4 +tag=0.10.0-preview5 revision= diff --git a/npm/yaci-devkit/package-lock.json b/npm/yaci-devkit/package-lock.json index 0a3c828..7751393 100644 --- a/npm/yaci-devkit/package-lock.json +++ b/npm/yaci-devkit/package-lock.json @@ -1,16 +1,16 @@ { "name": "@bloxbean/yaci-devkit", - "version": "0.10.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bloxbean/yaci-devkit", - "version": "0.10.0", + "version": "0.0.1", "license": "MIT", "dependencies": { - "@bloxbean/yaci-devkit-linux-x64": "file:../yaci-devkit-linux-x64", - "@bloxbean/yaci-devkit-macos-arm64": "file:../yaci-devkit-macos-arm64" + "@bloxbean/yaci-devkit-linux-x64": "0.0.1", + "@bloxbean/yaci-devkit-macos-arm64": "0.0.1" }, "bin": { "yaci-devkit": "start.mjs" @@ -25,7 +25,7 @@ }, "../yaci-devkit-linux-x64": { "name": "@bloxbean/yaci-devkit-linux-x64", - "version": "0.10.0", + "version": "0.0.1", "license": "MIT", "bin": { "yaci-devkit-linux-x64": "yaci-cli" @@ -36,7 +36,7 @@ }, "../yaci-devkit-macos-arm64": { "name": "@bloxbean/yaci-devkit-macos-arm64", - "version": "0.10.0", + "version": "0.0.1", "license": "MIT", "bin": { "yaci-devkit-linux-x64": "yaci-cli" diff --git a/npm/yaci-devkit/start.mjs b/npm/yaci-devkit/start.mjs index d8c0e47..3de7f14 100755 --- a/npm/yaci-devkit/start.mjs +++ b/npm/yaci-devkit/start.mjs @@ -1,9 +1,11 @@ #!/usr/bin/env node import { spawn } from "node:child_process"; -import { platform } from "node:os"; -import { fileURLToPath } from 'node:url'; -import { dirname, resolve } from 'node:path'; +import { platform, homedir } from "node:os"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve, join } from "node:path"; +import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs"; + // import { createHash } from "node:crypto"; const osType = platform(); @@ -38,6 +40,42 @@ const devkitDir = dirname(binPath); const configPath = resolve(devkitDir, 'config'); +// Determine the path to the user's `.yaci-cli` directory +const yaciCliHome = join(homedir(), ".yaci-cli"); +if (!existsSync(yaciCliHome)) { + mkdirSync(yaciCliHome, { recursive: true }); +} + +// Path for the PID file +const pidFilePath = join(yaciCliHome, "yaci-cli.pid"); + +function killProcessTree(pid) { + try { + if (osType === "linux" || osType === "darwin") { + // Use `pkill` to kill the entire process tree + spawn("pkill", ["-TERM", "-P", pid], { stdio: "ignore" }); + process.kill(pid, "SIGKILL"); // Kill the main process + } else { + console.error("Unsupported platform for process termination."); + } + } catch (err) { + console.error(`Failed to kill process tree for PID ${pid}: ${err.message}`); + } +} + +if (existsSync(pidFilePath)) { + try { + const existingPid = parseInt(readFileSync(pidFilePath, "utf-8"), 10); + if (!isNaN(existingPid)) { + console.log(`Killing existing yaci-cli process and its subprocesses with PID: ${existingPid}`); + killProcessTree(existingPid); // Kill the process tree + unlinkSync(pidFilePath); // Remove the stale PID file + } + } catch (err) { + console.error(`Failed to clean up existing yaci-cli process: ${err.message}`); + } +} + // const tmpSuffix = createHash('md5').update(workDir).digest("hex"); // const yaciCLIHome = resolve("/tmp", ".yaci-cli" + tmpSuffix ) @@ -55,6 +93,17 @@ const child = spawn(binPath, [additionalConfigArg, ...process.argv.slice(2)], { }); + +writeFileSync(pidFilePath, child.pid.toString()); +console.log(`yaci-cli process started with PID: ${child.pid}`); + +// Handle process exit child.on("close", (code) => { + console.log(`yaci-cli process exited with code: ${code}`); + try { + unlinkSync(pidFilePath); + } catch (err) { + console.error(`Failed to remove PID file: ${err.message}`); + } process.exit(code ?? 0); });