- CommandUtils: shared formatting (header, separator, formatBytes, formatDuration, progressBar, truncate) - ToolValidator: input validation (requireString, getString, getInt, getBoolean, validatePathInWorkDir) - ProcessExecutor: consolidated ProcessBuilder with timeout, cleanup, shell execution - AbstractReadOnlyTool: base class for read-only tools - Split AppConfig -> ToolConfiguration + CommandConfiguration + AppConfig (core) - Applied CommandUtils to 16 command files, removed duplicated private methods - Applied ToolValidator + ProcessExecutor to NotificationTool Net: -403 lines removed, 87 tests still passing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>pull/1/head
parent
5929f50b3a
commit
80f43480c1
@ -0,0 +1,159 @@ |
|||||||
|
package com.claudecode.command; |
||||||
|
|
||||||
|
import com.claudecode.console.AnsiStyle; |
||||||
|
|
||||||
|
/** |
||||||
|
* 命令输出格式化工具 —— 消除命令实现中的重复格式化代码。 |
||||||
|
* <p> |
||||||
|
* 提供标准化的命令输出格式:标题、分隔线、成功/失败消息、 |
||||||
|
* 字节/时间格式化、进度条等。 |
||||||
|
*/ |
||||||
|
public final class CommandUtils { |
||||||
|
|
||||||
|
private CommandUtils() {} |
||||||
|
|
||||||
|
// ==================== 标题和分隔线 ====================
|
||||||
|
|
||||||
|
/** |
||||||
|
* 格式化命令标题行。 |
||||||
|
* 输出: "\n ⚡ Title\n ──────────\n\n" |
||||||
|
*/ |
||||||
|
public static String header(String emoji, String title) { |
||||||
|
return "\n" + AnsiStyle.bold(" " + emoji + " " + title + "\n") |
||||||
|
+ separator(40) + "\n\n"; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 格式化命令标题行(无 emoji)。 |
||||||
|
*/ |
||||||
|
public static String header(String title) { |
||||||
|
return "\n" + AnsiStyle.bold(" " + title + "\n") |
||||||
|
+ separator(40) + "\n\n"; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 生成分隔线。 |
||||||
|
*/ |
||||||
|
public static String separator(int width) { |
||||||
|
return " " + "─".repeat(width); |
||||||
|
} |
||||||
|
|
||||||
|
// ==================== 状态消息 ====================
|
||||||
|
|
||||||
|
/** |
||||||
|
* 成功消息(绿色 ✓)。 |
||||||
|
*/ |
||||||
|
public static String success(String message) { |
||||||
|
return " " + AnsiStyle.green("✓") + " " + message; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 错误消息(红色 ✗)。 |
||||||
|
*/ |
||||||
|
public static String error(String message) { |
||||||
|
return " " + AnsiStyle.red("✗") + " " + message; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 警告消息(黄色 ⚠)。 |
||||||
|
*/ |
||||||
|
public static String warning(String message) { |
||||||
|
return " " + AnsiStyle.yellow("⚠") + " " + message; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 信息消息(蓝色 ℹ)。 |
||||||
|
*/ |
||||||
|
public static String info(String message) { |
||||||
|
return " " + AnsiStyle.cyan("ℹ") + " " + message; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 子标题(粗体缩进)。 |
||||||
|
*/ |
||||||
|
public static String subtitle(String title) { |
||||||
|
return AnsiStyle.bold(" " + title); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 键值对行。 |
||||||
|
*/ |
||||||
|
public static String keyValue(String key, Object value) { |
||||||
|
return String.format(" %-12s %s", key + ":", value); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 键值对行(自定义宽度)。 |
||||||
|
*/ |
||||||
|
public static String keyValue(String key, Object value, int keyWidth) { |
||||||
|
return String.format(" %-" + keyWidth + "s %s", key + ":", value); |
||||||
|
} |
||||||
|
|
||||||
|
// ==================== 格式化工具 ====================
|
||||||
|
|
||||||
|
/** |
||||||
|
* 格式化字节数为人类可读形式。 |
||||||
|
*/ |
||||||
|
public static String formatBytes(long bytes) { |
||||||
|
if (bytes < 0) return "N/A"; |
||||||
|
if (bytes >= 1_073_741_824) return String.format("%.1fGB", bytes / 1_073_741_824.0); |
||||||
|
if (bytes >= 1_048_576) return String.format("%.1fMB", bytes / 1_048_576.0); |
||||||
|
if (bytes >= 1_024) return String.format("%.0fKB", bytes / 1_024.0); |
||||||
|
return bytes + "B"; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 格式化秒数为人类可读时长。 |
||||||
|
*/ |
||||||
|
public static String formatDuration(long seconds) { |
||||||
|
if (seconds < 0) return "N/A"; |
||||||
|
if (seconds < 60) return seconds + "s"; |
||||||
|
if (seconds < 3600) return (seconds / 60) + "m " + (seconds % 60) + "s"; |
||||||
|
return (seconds / 3600) + "h " + ((seconds % 3600) / 60) + "m"; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 格式化毫秒为人类可读时长。 |
||||||
|
*/ |
||||||
|
public static String formatMillis(long ms) { |
||||||
|
if (ms < 1000) return ms + "ms"; |
||||||
|
return formatDuration(ms / 1000); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 截断字符串到最大长度。 |
||||||
|
*/ |
||||||
|
public static String truncate(String text, int maxLen) { |
||||||
|
if (text == null) return ""; |
||||||
|
if (text.length() <= maxLen) return text; |
||||||
|
return text.substring(0, maxLen - 3) + "..."; |
||||||
|
} |
||||||
|
|
||||||
|
// ==================== 进度条 ====================
|
||||||
|
|
||||||
|
/** |
||||||
|
* 生成带颜色的进度条。 |
||||||
|
*/ |
||||||
|
public static String progressBar(double ratio, int width) { |
||||||
|
ratio = Math.max(0, Math.min(1.0, ratio)); |
||||||
|
int filled = (int) (ratio * width); |
||||||
|
String bar = "█".repeat(filled); |
||||||
|
String empty = "░".repeat(width - filled); |
||||||
|
|
||||||
|
String colored; |
||||||
|
if (ratio > 0.8) colored = AnsiStyle.red(bar); |
||||||
|
else if (ratio > 0.5) colored = AnsiStyle.yellow(bar); |
||||||
|
else colored = AnsiStyle.green(bar); |
||||||
|
|
||||||
|
return "[" + colored + empty + "] " + String.format("%.0f%%", ratio * 100); |
||||||
|
} |
||||||
|
|
||||||
|
// ==================== 参数解析 ====================
|
||||||
|
|
||||||
|
/** |
||||||
|
* 安全解析命令参数(去空、null→空字符串)。 |
||||||
|
*/ |
||||||
|
public static String parseArgs(String args) { |
||||||
|
return (args == null) ? "" : args.strip(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,89 @@ |
|||||||
|
package com.claudecode.config; |
||||||
|
|
||||||
|
import com.claudecode.command.CommandRegistry; |
||||||
|
import com.claudecode.command.impl.*; |
||||||
|
import com.claudecode.core.TaskManager; |
||||||
|
import com.claudecode.permission.PermissionSettings; |
||||||
|
import com.claudecode.plugin.PluginManager; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
|
||||||
|
/** |
||||||
|
* 命令注册配置 —— 从 AppConfig 拆分出来,专注于 SlashCommand 注册。 |
||||||
|
*/ |
||||||
|
@Configuration |
||||||
|
public class CommandConfiguration { |
||||||
|
|
||||||
|
@Bean |
||||||
|
public CommandRegistry commandRegistry(PluginManager pluginManager, PermissionSettings permissionSettings, |
||||||
|
TaskManager taskManager) { |
||||||
|
ConfigCommand configCommand = new ConfigCommand(permissionSettings); |
||||||
|
CommandRegistry registry = new CommandRegistry(); |
||||||
|
registry.registerAll( |
||||||
|
// 基础命令
|
||||||
|
new HelpCommand(), |
||||||
|
new ClearCommand(), |
||||||
|
new CompactCommand(), |
||||||
|
new CostCommand(), |
||||||
|
new ModelCommand(), |
||||||
|
new StatusCommand(), |
||||||
|
new ContextCommand(), |
||||||
|
new InitCommand(), |
||||||
|
configCommand, |
||||||
|
new HistoryCommand(), |
||||||
|
// 文件/Git 命令
|
||||||
|
new DiffCommand(), |
||||||
|
new VersionCommand(), |
||||||
|
new SkillsCommand(), |
||||||
|
new MemoryCommand(), |
||||||
|
new CopyCommand(), |
||||||
|
new ResumeCommand(), |
||||||
|
new ExportCommand(), |
||||||
|
new CommitCommand(), |
||||||
|
new FilesCommand(), |
||||||
|
new PermissionsCommand(permissionSettings), |
||||||
|
new TasksCommand(taskManager), |
||||||
|
new PlanCommand(permissionSettings), |
||||||
|
// 协作/审查命令
|
||||||
|
new HooksCommand(), |
||||||
|
new ReviewCommand(), |
||||||
|
new StatsCommand(), |
||||||
|
new BranchCommand(), |
||||||
|
new RewindCommand(), |
||||||
|
new TagCommand(), |
||||||
|
new SecurityReviewCommand(), |
||||||
|
new McpCommand(), |
||||||
|
new PluginCommand(), |
||||||
|
// 会话/Agent 命令
|
||||||
|
new DoctorCommand(), |
||||||
|
new SessionCommand(), |
||||||
|
new AgentCommand(), |
||||||
|
new RenameCommand(), |
||||||
|
// UX 命令
|
||||||
|
new BriefCommand(), |
||||||
|
new VimCommand(), |
||||||
|
new ThemeCommand(), |
||||||
|
new UsageCommand(), |
||||||
|
new TipsCommand(), |
||||||
|
new OutputStyleCommand(), |
||||||
|
new EnvCommand(), |
||||||
|
new PerformanceCommand(), |
||||||
|
new PrivacyCommand(), |
||||||
|
new FeedbackCommand(), |
||||||
|
new ReleaseNotesCommand(), |
||||||
|
new KeybindingsCommand(), |
||||||
|
// 调试命令
|
||||||
|
new DebugCommand(), |
||||||
|
new HeapdumpCommand(), |
||||||
|
new TraceCommand(), |
||||||
|
new ContextVizCommand(), |
||||||
|
new ResetLimitsCommand(), |
||||||
|
new SandboxCommand(), |
||||||
|
// Exit
|
||||||
|
new ExitCommand() |
||||||
|
); |
||||||
|
|
||||||
|
pluginManager.registerCommands(registry); |
||||||
|
return registry; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,80 @@ |
|||||||
|
package com.claudecode.config; |
||||||
|
|
||||||
|
import com.claudecode.mcp.McpManager; |
||||||
|
import com.claudecode.tool.ToolContext; |
||||||
|
import com.claudecode.tool.ToolRegistry; |
||||||
|
import com.claudecode.tool.impl.*; |
||||||
|
import org.slf4j.Logger; |
||||||
|
import org.slf4j.LoggerFactory; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import com.claudecode.core.TaskManager; |
||||||
|
import com.claudecode.permission.PermissionSettings; |
||||||
|
|
||||||
|
/** |
||||||
|
* 工具注册配置 —— 从 AppConfig 拆分出来,专注于 Tool 注册。 |
||||||
|
*/ |
||||||
|
@Configuration |
||||||
|
public class ToolConfiguration { |
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ToolConfiguration.class); |
||||||
|
|
||||||
|
@Bean |
||||||
|
public ToolRegistry toolRegistry(TaskManager taskManager, McpManager mcpManager, |
||||||
|
ToolContext toolContext, PermissionSettings permissionSettings) { |
||||||
|
toolContext.set("TASK_MANAGER", taskManager); |
||||||
|
toolContext.set("MCP_MANAGER", mcpManager); |
||||||
|
toolContext.set("PERMISSION_SETTINGS", permissionSettings); |
||||||
|
|
||||||
|
ToolRegistry registry = new ToolRegistry(); |
||||||
|
registry.registerAll( |
||||||
|
// 核心工具
|
||||||
|
new BashTool(), |
||||||
|
new FileReadTool(), |
||||||
|
new FileWriteTool(), |
||||||
|
new FileEditTool(), |
||||||
|
new GlobTool(), |
||||||
|
new GrepTool(), |
||||||
|
new ListFilesTool(), |
||||||
|
new WebFetchTool(), |
||||||
|
new TodoWriteTool(), |
||||||
|
new AgentTool(), |
||||||
|
new NotebookEditTool(), |
||||||
|
new WebSearchTool(), |
||||||
|
new AskUserQuestionTool(), |
||||||
|
// 任务管理工具
|
||||||
|
new TaskCreateTool(), |
||||||
|
new TaskGetTool(), |
||||||
|
new TaskListTool(), |
||||||
|
new TaskUpdateTool(), |
||||||
|
new TaskStopTool(), |
||||||
|
new TaskOutputTool(), |
||||||
|
// 配置/实用工具
|
||||||
|
new ConfigTool(), |
||||||
|
new SleepTool(), |
||||||
|
new ToolSearchTool(), |
||||||
|
new EnterPlanModeTool(), |
||||||
|
new ExitPlanModeTool(), |
||||||
|
new SkillTool(), |
||||||
|
new SendMessageTool(), |
||||||
|
new ListMcpResourcesTool(), |
||||||
|
new ReadMcpResourceTool(), |
||||||
|
new EnterWorktreeTool(), |
||||||
|
new ExitWorktreeTool(), |
||||||
|
// 高级工具
|
||||||
|
new LSPTool(), |
||||||
|
new BriefTool(), |
||||||
|
new NotificationTool() |
||||||
|
); |
||||||
|
|
||||||
|
// MCP 工具桥接
|
||||||
|
for (var client : mcpManager.getClients().values()) { |
||||||
|
for (var mcpTool : client.getTools()) { |
||||||
|
registry.register(new McpToolBridge(client.getServerName(), mcpTool)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
toolContext.set("TOOL_REGISTRY", registry); |
||||||
|
return registry; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
package com.claudecode.tool; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* 只读工具抽象基类 —— 18+ 只读工具共享的基础实现。 |
||||||
|
* <p> |
||||||
|
* 提供: |
||||||
|
* <ul> |
||||||
|
* <li>{@link #isReadOnly()} 固定返回 true</li> |
||||||
|
* <li>{@link #isEnabled()} 默认返回 true</li> |
||||||
|
* <li>{@link #checkPermission} 默认 ALLOW</li> |
||||||
|
* <li>{@link #errorResult(String)} 标准化错误格式</li> |
||||||
|
* </ul> |
||||||
|
* 子类只需实现 name/description/inputSchema/execute。 |
||||||
|
*/ |
||||||
|
public abstract class AbstractReadOnlyTool implements Tool { |
||||||
|
|
||||||
|
@Override |
||||||
|
public final boolean isReadOnly() { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isEnabled() { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public PermissionResult checkPermission(Map<String, Object> input, ToolContext context) { |
||||||
|
return PermissionResult.ALLOW; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String activityDescription(Map<String, Object> input) { |
||||||
|
return "Reading " + name() + "..."; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 标准化错误结果。 |
||||||
|
*/ |
||||||
|
protected String errorResult(String message) { |
||||||
|
return "Error: " + message; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 从 input 中获取必填 String 参数,缺失则返回 null 并可由调用方提前 return errorResult。 |
||||||
|
*/ |
||||||
|
protected String requireParam(Map<String, Object> input, String paramName) { |
||||||
|
String err = ToolValidator.requireString(input, paramName); |
||||||
|
return err == null ? input.get(paramName).toString() : null; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,90 @@ |
|||||||
|
package com.claudecode.tool; |
||||||
|
|
||||||
|
import java.nio.file.Path; |
||||||
|
|
||||||
|
/** |
||||||
|
* 工具输入验证器 —— 消除 Tool 实现中的重复验证代码。 |
||||||
|
* <p> |
||||||
|
* 所有 validate* 方法:返回 null 表示验证通过,返回 String 表示错误消息。 |
||||||
|
* 典型用法: |
||||||
|
* <pre> |
||||||
|
* String err = ToolValidator.requireString(input, "file_path"); |
||||||
|
* if (err != null) return err; |
||||||
|
* </pre> |
||||||
|
*/ |
||||||
|
public final class ToolValidator { |
||||||
|
|
||||||
|
private ToolValidator() {} |
||||||
|
|
||||||
|
/** |
||||||
|
* 验证必填 String 参数(非 null、非空白)。 |
||||||
|
* @return null 表示通过,否则返回错误消息 |
||||||
|
*/ |
||||||
|
public static String requireString(java.util.Map<String, Object> input, String paramName) { |
||||||
|
Object value = input.get(paramName); |
||||||
|
if (value == null) { |
||||||
|
return "Error: '" + paramName + "' is required."; |
||||||
|
} |
||||||
|
if (value instanceof String s && s.isBlank()) { |
||||||
|
return "Error: '" + paramName + "' must not be empty."; |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取 String 参数(带默认值)。 |
||||||
|
*/ |
||||||
|
public static String getString(java.util.Map<String, Object> input, String paramName, String defaultValue) { |
||||||
|
Object value = input.get(paramName); |
||||||
|
if (value instanceof String s && !s.isBlank()) return s; |
||||||
|
return defaultValue; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 安全获取 int 参数(带默认值,防止 ClassCastException)。 |
||||||
|
*/ |
||||||
|
public static int getInt(java.util.Map<String, Object> input, String paramName, int defaultValue) { |
||||||
|
Object value = input.get(paramName); |
||||||
|
if (value instanceof Number n) return n.intValue(); |
||||||
|
if (value instanceof String s) { |
||||||
|
try { return Integer.parseInt(s); } catch (NumberFormatException e) { /* fall through */ } |
||||||
|
} |
||||||
|
return defaultValue; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 安全获取 boolean 参数(带默认值)。 |
||||||
|
*/ |
||||||
|
public static boolean getBoolean(java.util.Map<String, Object> input, String paramName, boolean defaultValue) { |
||||||
|
Object value = input.get(paramName); |
||||||
|
if (value instanceof Boolean b) return b; |
||||||
|
if (value instanceof String s) return Boolean.parseBoolean(s); |
||||||
|
return defaultValue; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 验证文件路径在工作目录内(防止路径遍历)。 |
||||||
|
* @return null 表示通过,否则返回错误消息 |
||||||
|
*/ |
||||||
|
public static String validatePathInWorkDir(String filePath, Path workDir) { |
||||||
|
if (filePath == null || filePath.isBlank()) { |
||||||
|
return "Error: file path is required."; |
||||||
|
} |
||||||
|
Path resolved = workDir.resolve(filePath).normalize(); |
||||||
|
if (!resolved.startsWith(workDir.normalize())) { |
||||||
|
return "Error: Path traversal not allowed. Path must be within the working directory."; |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 解析并验证路径(合并 resolve + normalize + traversal check)。 |
||||||
|
* @return 解析后的路径,如验证失败则为 null(错误通过 errorHolder 返回) |
||||||
|
*/ |
||||||
|
public static Path resolveSafePath(String filePath, Path workDir) { |
||||||
|
if (filePath == null || filePath.isBlank()) return null; |
||||||
|
Path resolved = workDir.resolve(filePath).normalize(); |
||||||
|
if (!resolved.startsWith(workDir.normalize())) return null; |
||||||
|
return resolved; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,115 @@ |
|||||||
|
package com.claudecode.tool.util; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.nio.file.Path; |
||||||
|
import java.util.*; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
|
||||||
|
/** |
||||||
|
* 进程执行器 —— 消除 BashTool、GrepTool、NotificationTool 中的重复 ProcessBuilder 模式。 |
||||||
|
* <p> |
||||||
|
* 封装进程创建、超时管理、输出捕获和资源清理。 |
||||||
|
*/ |
||||||
|
public final class ProcessExecutor { |
||||||
|
|
||||||
|
private ProcessExecutor() {} |
||||||
|
|
||||||
|
/** |
||||||
|
* 进程执行结果。 |
||||||
|
*/ |
||||||
|
public record Result(int exitCode, String stdout, String stderr, boolean timedOut) { |
||||||
|
|
||||||
|
public boolean isSuccess() { |
||||||
|
return exitCode == 0 && !timedOut; |
||||||
|
} |
||||||
|
|
||||||
|
public String output() { |
||||||
|
return stdout != null ? stdout : ""; |
||||||
|
} |
||||||
|
|
||||||
|
public String combinedOutput() { |
||||||
|
StringBuilder sb = new StringBuilder(); |
||||||
|
if (stdout != null && !stdout.isEmpty()) sb.append(stdout); |
||||||
|
if (stderr != null && !stderr.isEmpty()) { |
||||||
|
if (!sb.isEmpty()) sb.append("\n"); |
||||||
|
sb.append(stderr); |
||||||
|
} |
||||||
|
return sb.toString(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 在指定工作目录执行命令,带超时和资源清理。 |
||||||
|
*/ |
||||||
|
public static Result execute(List<String> command, Path workDir, long timeoutMs) { |
||||||
|
return execute(command, workDir, timeoutMs, null); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 在指定工作目录执行命令,带超时、环境变量和资源清理。 |
||||||
|
*/ |
||||||
|
public static Result execute(List<String> command, Path workDir, long timeoutMs, Map<String, String> env) { |
||||||
|
ProcessBuilder pb = new ProcessBuilder(command); |
||||||
|
if (workDir != null) pb.directory(workDir.toFile()); |
||||||
|
pb.redirectErrorStream(false); |
||||||
|
|
||||||
|
if (env != null && !env.isEmpty()) { |
||||||
|
pb.environment().putAll(env); |
||||||
|
} |
||||||
|
|
||||||
|
Process process = null; |
||||||
|
try { |
||||||
|
process = pb.start(); |
||||||
|
|
||||||
|
String stdout; |
||||||
|
String stderr; |
||||||
|
try (var stdoutStream = process.getInputStream(); |
||||||
|
var stderrStream = process.getErrorStream()) { |
||||||
|
stdout = new String(stdoutStream.readAllBytes()); |
||||||
|
stderr = new String(stderrStream.readAllBytes()); |
||||||
|
} |
||||||
|
|
||||||
|
boolean finished = process.waitFor(timeoutMs, TimeUnit.MILLISECONDS); |
||||||
|
if (!finished) { |
||||||
|
process.destroyForcibly(); |
||||||
|
process.waitFor(2, TimeUnit.SECONDS); |
||||||
|
return new Result(-1, stdout, stderr, true); |
||||||
|
} |
||||||
|
|
||||||
|
return new Result(process.exitValue(), stdout, stderr, false); |
||||||
|
} catch (IOException e) { |
||||||
|
return new Result(-1, "", "IOException: " + e.getMessage(), false); |
||||||
|
} catch (InterruptedException e) { |
||||||
|
Thread.currentThread().interrupt(); |
||||||
|
return new Result(-1, "", "Interrupted", false); |
||||||
|
} finally { |
||||||
|
if (process != null && process.isAlive()) { |
||||||
|
process.destroyForcibly(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 快速检查命令是否可用(which/where)。 |
||||||
|
*/ |
||||||
|
public static boolean isCommandAvailable(String command) { |
||||||
|
boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win"); |
||||||
|
String checker = isWindows ? "where" : "which"; |
||||||
|
Result result = execute(List.of(checker, command), null, 5000); |
||||||
|
return result.isSuccess(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 执行 shell 命令(通过 sh -c 或 cmd /c)。 |
||||||
|
*/ |
||||||
|
public static Result executeShell(String shellCommand, Path workDir, long timeoutMs) { |
||||||
|
boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win"); |
||||||
|
List<String> command; |
||||||
|
if (isWindows) { |
||||||
|
command = List.of("cmd.exe", "/c", shellCommand); |
||||||
|
} else { |
||||||
|
command = List.of("/bin/sh", "-c", shellCommand); |
||||||
|
} |
||||||
|
return execute(command, workDir, timeoutMs); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue