feat(partnerctl): add run and shutdown commands with process lifecycle handling

This commit is contained in:
2026-05-05 23:39:17 +08:00
parent 9073f88117
commit bd4d5164d3
6 changed files with 171 additions and 28 deletions

37
.idea/misc.xml generated
View File

@@ -1,26 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="EntryPointsManager"> <component name="EntryPointsManager">
<list size="19"> <list size="20">
<item index="0" class="java.lang.String" itemvalue="lombok.Data" /> <item index="0" class="java.lang.String" itemvalue="lombok.Data" />
<item index="1" class="java.lang.String" itemvalue="net.bytebuddy.implementation.bind.annotation.RuntimeType" /> <item index="1" class="java.lang.String" itemvalue="net.bytebuddy.implementation.bind.annotation.RuntimeType" />
<item index="2" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" /> <item index="2" class="java.lang.String" itemvalue="picocli.CommandLine.Command" />
<item index="3" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore" /> <item index="3" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" />
<item index="4" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" /> <item index="4" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityCore" />
<item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" /> <item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" />
<item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" /> <item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" />
<item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.component.annotation.Init" /> <item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" />
<item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" /> <item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.component.annotation.Init" />
<item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" /> <item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" />
<item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" /> <item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" />
<item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" /> <item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" />
<item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" /> <item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" />
<item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" /> <item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" />
<item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" /> <item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" />
<item index="15" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" /> <item index="15" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" />
<item index="16" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore" /> <item index="16" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" />
<item index="17" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod" /> <item index="17" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityCore" />
<item index="18" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent" /> <item index="18" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.capability.annotation.CapabilityMethod" />
<item index="19" class="java.lang.String" itemvalue="work.slhaf.partner.framework.agent.factory.component.annotation.AgentComponent" />
</list> </list>
<writeAnnotations> <writeAnnotations>
<writeAnnotation name="work.slhaf.partner.api.agent.factory.capability.annotation.InjectCapability" /> <writeAnnotation name="work.slhaf.partner.api.agent.factory.capability.annotation.InjectCapability" />

View File

@@ -14,6 +14,7 @@ import kotlin.system.exitProcess
subcommands = [ subcommands = [
InitCommand::class, InitCommand::class,
RunCommand::class, RunCommand::class,
ShutdownCommand::class,
ChatCommand::class, ChatCommand::class,
ConfigCommand::class, ConfigCommand::class,
ModuleCommand::class, ModuleCommand::class,

View File

@@ -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
}
}

View File

@@ -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")
}
}

View File

@@ -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.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.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 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

View File

@@ -89,3 +89,13 @@ sourceBuild.tool.java.reason=运行 Maven 需要 java。命令失败java --ve
sourceBuild.tool.javac.reason=从源码编译 Partner 需要 javac。请安装 JDK而不只是 JRE。命令失败javac --version sourceBuild.tool.javac.reason=从源码编译 Partner 需要 javac。请安装 JDK而不只是 JRE。命令失败javac --version
sourceBuild.tool.git.reason=克隆 Partner 源码需要 git。命令失败git --version sourceBuild.tool.git.reason=克隆 Partner 源码需要 git。命令失败git --version
sourceBuild.tool.mvn.reason=从源码构建 Partner 需要 mvn。命令失败mvn --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 进程停止失败