feat(partnerctl): add --log-level option for run command and wire runtime log level to Partner-Core

This commit is contained in:
2026-05-09 14:12:32 +08:00
parent d72b34acfd
commit 8cb876b532
6 changed files with 154 additions and 115 deletions

37
.idea/misc.xml generated
View File

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

View File

@@ -1,6 +1,7 @@
<configuration>
<property name="PARTNER_HOME" value="${PARTNER_HOME:-${user.home}/.partner}"/>
<property name="PARTNER_LOG_DIR" value="${PARTNER_HOME}/state/trace/log"/>
<property name="LOG_LEVEL" value="${PARTNER_LOG_LEVEL}:-${partner.log.level:-INFO}"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
@@ -24,7 +25,7 @@
</encoder>
</appender>
<root level="DEBUG">
<root level="${LOG_LEVEL}">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="PARTNER_FILE"/>
</root>

View File

@@ -4,11 +4,7 @@ import picocli.CommandLine
import work.slhaf.partner.ctl.commands.control.*
import work.slhaf.partner.ctl.i18n.I18n.text
import work.slhaf.partner.ctl.support.CommandInterrupted
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.time.LocalDateTime
@CommandLine.Command(
name = "run",
@@ -26,6 +22,14 @@ class RunCommand : Runnable {
)
var background: Boolean = false
@CommandLine.Option(
names = ["-l", "--log-level"],
descriptionKey = "cli.log.option.level.description",
defaultValue = "INFO",
converter = [LogLevelConverter::class],
)
lateinit var logLevel: LogLevel
override fun run() {
val home = resolvePartnerHome()
val partnerJar = resolvePartnerJar(home)
@@ -35,98 +39,10 @@ class RunCommand : Runnable {
}
if (background) {
runInBackground(home, partnerJar)
runInBackground(home, partnerJar, logLevel)
} else {
runInForeground(home, partnerJar)
runInForeground(home, partnerJar, logLevel)
}
}
}
private fun runInForeground(home: Path, partnerJar: Path) {
val logFile = resolveLogFile(home)
Files.createDirectories(logFile.parent)
val process = createPartnerProcessBuilder(home, partnerJar)
.inheritIO()
.start()
appendControlLog(logFile, text("control.run.log.foregroundStarting", process.pid(), partnerJar))
val shutdownHook = Thread {
if (process.isAlive) {
process.destroy()
}
}
Runtime.getRuntime().addShutdownHook(shutdownHook)
val exitCode = process.waitFor()
runCatching { Runtime.getRuntime().removeShutdownHook(shutdownHook) }
appendControlLog(logFile, text("control.run.log.exited", exitCode))
if (exitCode != 0) {
throw CommandInterrupted(text("control.run.error.exited", exitCode), exitCode)
}
}
private fun runInBackground(home: Path, partnerJar: Path) {
val pidFile = resolvePidFile(home)
val logFile = resolveLogFile(home)
val existingProcess = findPartnerProcesses(home, partnerJar).firstOrNull()
if (existingProcess != null) {
throw CommandInterrupted(text("control.run.error.alreadyRunning", existingProcess.pid()))
}
Files.createDirectories(pidFile.parent)
Files.createDirectories(logFile.parent)
val process = createPartnerProcessBuilder(home, partnerJar)
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.redirectError(ProcessBuilder.Redirect.DISCARD)
.start()
appendControlLog(logFile, text("control.run.log.backgroundStarting", process.pid(), partnerJar))
Thread.sleep(BACKGROUND_START_CHECK_MILLIS)
if (!process.isAlive) {
val exitCode = process.exitValue()
appendControlLog(logFile, text("control.run.log.exited", exitCode))
Files.deleteIfExists(pidFile)
throw CommandInterrupted(text("control.run.error.exited", exitCode), exitCode)
}
Files.writeString(
pidFile,
process.pid().toString(),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
)
println(text("control.run.success.backgroundStarted", process.pid()))
println(text("control.run.info.logFile", logFile))
}
private fun createPartnerProcessBuilder(home: Path, partnerJar: Path): ProcessBuilder {
return ProcessBuilder("java", "-DPARTNER_HOME=$home", "-jar", partnerJar.toString())
.apply {
environment()["PARTNER_HOME"] = home.toString()
}
}
private fun appendControlLog(logFile: Path, message: String) {
Files.createDirectories(logFile.parent)
Files.newOutputStream(
logFile,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND,
).use { output ->
writeControlLog(output, message)
}
}
private fun writeControlLog(output: OutputStream, message: String) {
output.write("[partnerctl ${LocalDateTime.now()}] $message\n".toByteArray())
output.flush()
}
private const val BACKGROUND_START_CHECK_MILLIS = 500L

View File

@@ -1,8 +1,14 @@
package work.slhaf.partner.ctl.commands.control
import picocli.CommandLine
import work.slhaf.partner.ctl.i18n.I18n.text
import work.slhaf.partner.ctl.support.CommandInterrupted
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardOpenOption
import java.time.LocalDateTime
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
@@ -25,7 +31,8 @@ fun resolvePidFile(home: Path): Path {
}
fun resolveLogFile(home: Path): Path {
return home.resolve("state").resolve("trace").resolve("log").resolve("partner-core.log").toAbsolutePath().normalize()
return home.resolve("state").resolve("trace").resolve("log").resolve("partner-core.log").toAbsolutePath()
.normalize()
}
fun findPartnerProcesses(home: Path, partnerJar: Path): List<ProcessHandle> {
@@ -95,3 +102,115 @@ private fun isPartnerProcess(process: ProcessHandle, partnerJar: Path): Boolean
return arguments.any { it == jarPath } || commandLine.contains(jarPath)
}
fun runInForeground(home: Path, partnerJar: Path, logLevel: LogLevel) {
val logFile = resolveLogFile(home)
Files.createDirectories(logFile.parent)
val process = createPartnerProcessBuilder(home, partnerJar, logLevel)
.inheritIO()
.start()
appendControlLog(logFile, text("control.run.log.foregroundStarting", process.pid(), partnerJar))
val shutdownHook = Thread {
if (process.isAlive) {
process.destroy()
}
}
Runtime.getRuntime().addShutdownHook(shutdownHook)
val exitCode = process.waitFor()
runCatching { Runtime.getRuntime().removeShutdownHook(shutdownHook) }
appendControlLog(logFile, text("control.run.log.exited", exitCode))
if (exitCode != 0) {
throw CommandInterrupted(text("control.run.error.exited", exitCode), exitCode)
}
}
fun runInBackground(home: Path, partnerJar: Path, logLevel: LogLevel) {
val pidFile = resolvePidFile(home)
val logFile = resolveLogFile(home)
val existingProcess = findPartnerProcesses(home, partnerJar).firstOrNull()
if (existingProcess != null) {
throw CommandInterrupted(text("control.run.error.alreadyRunning", existingProcess.pid()))
}
Files.createDirectories(pidFile.parent)
Files.createDirectories(logFile.parent)
val process = createPartnerProcessBuilder(home, partnerJar, logLevel)
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.redirectError(ProcessBuilder.Redirect.DISCARD)
.start()
appendControlLog(logFile, text("control.run.log.backgroundStarting", process.pid(), partnerJar))
Thread.sleep(BACKGROUND_START_CHECK_MILLIS)
if (!process.isAlive) {
val exitCode = process.exitValue()
appendControlLog(logFile, text("control.run.log.exited", exitCode))
Files.deleteIfExists(pidFile)
throw CommandInterrupted(text("control.run.error.exited", exitCode), exitCode)
}
Files.writeString(
pidFile,
process.pid().toString(),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
)
println(text("control.run.success.backgroundStarted", process.pid()))
println(text("control.run.info.logFile", logFile))
}
private fun createPartnerProcessBuilder(home: Path, partnerJar: Path, logLevel: LogLevel): ProcessBuilder {
return ProcessBuilder(
"java",
"-Dpartner.log.level=${logLevel.name.uppercase()}",
"-DPARTNER_HOME=$home",
"-jar",
partnerJar.toString()
)
.apply {
environment()["PARTNER_HOME"] = home.toString()
}
}
private fun appendControlLog(logFile: Path, message: String) {
Files.createDirectories(logFile.parent)
Files.newOutputStream(
logFile,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND,
).use { output ->
writeControlLog(output, message)
}
}
private fun writeControlLog(output: OutputStream, message: String) {
output.write("[partnerctl ${LocalDateTime.now()}] $message\n".toByteArray())
output.flush()
}
private const val BACKGROUND_START_CHECK_MILLIS = 500L
enum class LogLevel {
TRACE, DEBUG, INFO, WARN, ERROR
}
class LogLevelConverter : CommandLine.ITypeConverter<LogLevel> {
override fun convert(value: String): LogLevel {
return LogLevel.entries.firstOrNull {
it.name.equals(value, ignoreCase = true)
} ?: throw CommandLine.TypeConversionException(
"invalid log level '$value'. Valid values: ${
LogLevel.entries.joinToString(", ") { it.name.lowercase() }
}"
)
}
}

View File

@@ -8,6 +8,7 @@ cli.shutdown.option.force.description=Forcefully kill matching Partner process i
cli.log.description=Show Partner logs.
cli.log.option.tail.description=Number of log lines to show before exiting or following.
cli.log.option.follow.description=Follow appended log output.
cli.log.option.level.description=Set Partner runtime log level. Available values: TRACE, DEBUG, INFO, WARN, ERROR. Default: INFO.
cli.chat.description=Start an interactive chat client.
cli.config.description=Manage Partner configuration.
cli.module.description=Manage Partner modules.

View File

@@ -2,12 +2,13 @@ cli.partnerctl.description=Partner 命令行工具。
cli.init.description=初始化 Partner agent。
cli.run.description=启动 Partner agent。
cli.run.option.background.description=后台运行 Partner。
cli.shutdown.description=停止 Partner agent。
cli.shutdown.option.timeout.description=优雅停止后等待的秒数,超时后失败或强制停止。
cli.shutdown.option.force.description=如果匹配的 Partner 进程没有在超时前退出,则强制结束进程。
cli.log.description=查看 Partner 日志。
cli.log.option.tail.description=退出或 follow 前显示的日志行数。
cli.log.option.follow.description=持续跟随新增日志输出。
cli.log.option.level.description=指定 Partner 运行时日志等级。可选值TRACE、DEBUG、INFO、WARN、ERROR。默认值INFO。
cli.shutdown.description=停止 Partner agent。
cli.chat.description=启动交互式聊天客户端。
cli.config.description=管理 Partner 配置。
cli.module.description=管理 Partner 模块。