diff --git a/.idea/misc.xml b/.idea/misc.xml index d760821e..1cf4f9ca 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,27 +1,28 @@ - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/Partner-Core/src/main/resources/logback.xml b/Partner-Core/src/main/resources/logback.xml index b27d71d3..8bf0eece 100644 --- a/Partner-Core/src/main/resources/logback.xml +++ b/Partner-Core/src/main/resources/logback.xml @@ -1,6 +1,7 @@ + @@ -24,7 +25,7 @@ - + diff --git a/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/RunCommand.kt b/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/RunCommand.kt index 5cdbc7ed..c4eafe34 100644 --- a/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/RunCommand.kt +++ b/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/RunCommand.kt @@ -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 diff --git a/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/control/PartnerRuntime.kt b/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/control/PartnerRuntime.kt index 92a764e6..4f1d9887 100644 --- a/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/control/PartnerRuntime.kt +++ b/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/control/PartnerRuntime.kt @@ -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 { @@ -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 { + 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() } + }" + ) + } +} diff --git a/PartnerCtl/src/main/resources/i18n/messages.properties b/PartnerCtl/src/main/resources/i18n/messages.properties index 40043c70..0da3c321 100644 --- a/PartnerCtl/src/main/resources/i18n/messages.properties +++ b/PartnerCtl/src/main/resources/i18n/messages.properties @@ -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. diff --git a/PartnerCtl/src/main/resources/i18n/messages_zh_CN.properties b/PartnerCtl/src/main/resources/i18n/messages_zh_CN.properties index 04c876bc..4c98e739 100644 --- a/PartnerCtl/src/main/resources/i18n/messages_zh_CN.properties +++ b/PartnerCtl/src/main/resources/i18n/messages_zh_CN.properties @@ -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 模块。