mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 08:43:02 +08:00
feat(partnerctl): add run and shutdown commands with process lifecycle handling
This commit is contained in:
@@ -14,6 +14,7 @@ import kotlin.system.exitProcess
|
||||
subcommands = [
|
||||
InitCommand::class,
|
||||
RunCommand::class,
|
||||
ShutdownCommand::class,
|
||||
ChatCommand::class,
|
||||
ConfigCommand::class,
|
||||
ModuleCommand::class,
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package work.slhaf.partner.ctl.commands
|
||||
|
||||
import picocli.CommandLine
|
||||
import work.slhaf.partner.ctl.i18n.I18n.text
|
||||
import work.slhaf.partner.ctl.support.CommandInterrupted
|
||||
import work.slhaf.partner.ctl.support.inheritCommand
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
@CommandLine.Command(name = "run", description = ["Start Partner agent."])
|
||||
class RunCommand : Runnable {
|
||||
|
||||
override fun run() {
|
||||
val home = resolvePartnerHome()
|
||||
val partnerJar = resolvePartnerJar(home)
|
||||
|
||||
if (!Files.isRegularFile(partnerJar)) {
|
||||
throw CommandInterrupted(text("control.run.error.jarNotFound", partnerJar))
|
||||
}
|
||||
|
||||
val exitCode = inheritCommand(
|
||||
command = listOf("java", "-jar", partnerJar.toString()),
|
||||
environment = mapOf("PARTNER_HOME" to home.toString()),
|
||||
)
|
||||
|
||||
if (exitCode != 0) {
|
||||
throw CommandInterrupted(text("control.run.error.exited", exitCode), exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CommandLine.Command(name = "shutdown", description = ["Shutdown Partner agent."])
|
||||
class ShutdownCommand : Runnable {
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--timeout"],
|
||||
description = ["Seconds to wait after graceful termination before failing or forcing shutdown."]
|
||||
)
|
||||
var timeoutSeconds: Long = 10
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-f", "--force"],
|
||||
description = ["Forcefully kill matching Partner process if it does not exit before timeout."]
|
||||
)
|
||||
var force: Boolean = false
|
||||
|
||||
override fun run() {
|
||||
val home = resolvePartnerHome()
|
||||
val partnerJar = resolvePartnerJar(home)
|
||||
val processes = findPartnerProcesses(partnerJar)
|
||||
|
||||
if (processes.isEmpty()) {
|
||||
println(text("control.shutdown.info.notRunning", partnerJar))
|
||||
return
|
||||
}
|
||||
|
||||
var failed = false
|
||||
processes.forEach { process ->
|
||||
val pid = process.pid()
|
||||
println(text("control.shutdown.info.stopping", pid))
|
||||
process.destroy()
|
||||
|
||||
val stopped = waitForExit(process, timeoutSeconds)
|
||||
if (stopped) {
|
||||
println(text("control.shutdown.success.stopped", pid))
|
||||
return@forEach
|
||||
}
|
||||
|
||||
if (force) {
|
||||
println(text("control.shutdown.warn.force", pid))
|
||||
process.destroyForcibly()
|
||||
if (waitForExit(process, timeoutSeconds)) {
|
||||
println(text("control.shutdown.success.stopped", pid))
|
||||
} else {
|
||||
failed = true
|
||||
println(text("control.shutdown.error.notStopped", pid))
|
||||
}
|
||||
} else {
|
||||
failed = true
|
||||
println(text("control.shutdown.error.notStoppedUseForce", pid))
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
throw CommandInterrupted(text("control.shutdown.error.failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolvePartnerHome(): Path {
|
||||
val home = System.getenv("PARTNER_HOME")
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.let { Paths.get(it) }
|
||||
?: Paths.get(System.getProperty("user.home"), ".partner")
|
||||
|
||||
return home.toAbsolutePath().normalize()
|
||||
}
|
||||
|
||||
private fun resolvePartnerJar(home: Path): Path {
|
||||
return home.resolve("resource").resolve("partner-core.jar").toAbsolutePath().normalize()
|
||||
}
|
||||
|
||||
private fun findPartnerProcesses(partnerJar: Path): List<ProcessHandle> {
|
||||
val currentPid = ProcessHandle.current().pid()
|
||||
val jarPath = partnerJar.toString()
|
||||
|
||||
return ProcessHandle.allProcesses()
|
||||
.filter { it.pid() != currentPid }
|
||||
.filter { it.isAlive }
|
||||
.filter { process ->
|
||||
val info = process.info()
|
||||
val arguments = info.arguments().orElse(emptyArray()) ?: emptyArray()
|
||||
val commandLine = info.commandLine().orElse("") ?: ""
|
||||
|
||||
arguments.any { it == jarPath } || commandLine.contains(jarPath)
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun waitForExit(process: ProcessHandle, timeoutSeconds: Long): Boolean {
|
||||
return try {
|
||||
process.onExit().get(timeoutSeconds, TimeUnit.SECONDS)
|
||||
true
|
||||
} catch (_: TimeoutException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package work.slhaf.partner.ctl.commands
|
||||
|
||||
import picocli.CommandLine
|
||||
|
||||
@CommandLine.Command(name = "run")
|
||||
class RunCommand : Runnable{
|
||||
override fun run() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@@ -89,3 +89,13 @@ sourceBuild.tool.java.reason=Required to run Maven. Command failed: java --versi
|
||||
sourceBuild.tool.javac.reason=Required to compile Partner from source. Install a JDK, not just a JRE. Command failed: javac --version
|
||||
sourceBuild.tool.git.reason=Required to clone Partner source. Command failed: git --version
|
||||
sourceBuild.tool.mvn.reason=Required to build Partner from source. Command failed: mvn --version
|
||||
|
||||
control.run.error.jarNotFound=Partner runtime jar does not exist: {0}. Run partnerctl init first to initialize Partner.
|
||||
control.run.error.exited=Partner exited with code {0}
|
||||
control.shutdown.info.notRunning=No running Partner process found for {0}
|
||||
control.shutdown.info.stopping=Stopping Partner process pid={0}
|
||||
control.shutdown.success.stopped=Partner process pid={0} stopped
|
||||
control.shutdown.warn.force=Force killing Partner process pid={0}
|
||||
control.shutdown.error.notStopped=Partner process pid={0} did not stop
|
||||
control.shutdown.error.notStoppedUseForce=Partner process pid={0} did not stop. Use --force to kill it forcibly.
|
||||
control.shutdown.error.failed=Failed to stop one or more Partner processes
|
||||
|
||||
@@ -89,3 +89,13 @@ sourceBuild.tool.java.reason=运行 Maven 需要 java。命令失败:java --ve
|
||||
sourceBuild.tool.javac.reason=从源码编译 Partner 需要 javac。请安装 JDK,而不只是 JRE。命令失败:javac --version
|
||||
sourceBuild.tool.git.reason=克隆 Partner 源码需要 git。命令失败:git --version
|
||||
sourceBuild.tool.mvn.reason=从源码构建 Partner 需要 mvn。命令失败:mvn --version
|
||||
|
||||
control.run.error.jarNotFound=Partner runtime jar 不存在:{0}。请先执行 partnerctl init 初始化 Partner。
|
||||
control.run.error.exited=Partner 退出,退出码:{0}
|
||||
control.shutdown.info.notRunning=没有找到对应 {0} 的运行中 Partner 进程
|
||||
control.shutdown.info.stopping=正在停止 Partner 进程 pid={0}
|
||||
control.shutdown.success.stopped=Partner 进程 pid={0} 已停止
|
||||
control.shutdown.warn.force=正在强制结束 Partner 进程 pid={0}
|
||||
control.shutdown.error.notStopped=Partner 进程 pid={0} 未停止
|
||||
control.shutdown.error.notStoppedUseForce=Partner 进程 pid={0} 未停止。使用 --force 可强制结束。
|
||||
control.shutdown.error.failed=一个或多个 Partner 进程停止失败
|
||||
|
||||
Reference in New Issue
Block a user