mirror of
https://github.com/slhaf/Partner.git
synced 2026-05-12 08:43:02 +08:00
Merge branch 'codex-review-localrunnerclient-v2'
This commit is contained in:
@@ -176,6 +176,7 @@ public class LocalRunnerClient extends RunnerClient {
|
|||||||
if (!closed.compareAndSet(false, true)) {
|
if (!closed.compareAndSet(false, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
mcpConfigWatcher.unregisterPolicyListener();
|
||||||
closeQuietly(mcpConfigWatcher);
|
closeQuietly(mcpConfigWatcher);
|
||||||
closeQuietly(dynamicActionMcpManager);
|
closeQuietly(dynamicActionMcpManager);
|
||||||
closeQuietly(mcpDescWatcher);
|
closeQuietly(mcpDescWatcher);
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
package work.slhaf.partner.core.action.runner.execution;
|
package work.slhaf.partner.core.action.runner.execution;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.File;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
public class CommandExecutionService {
|
public class CommandExecutionService {
|
||||||
|
|
||||||
public static final CommandExecutionService INSTANCE = new CommandExecutionService();
|
public static final CommandExecutionService INSTANCE = new CommandExecutionService();
|
||||||
|
|
||||||
private final ExecutorService readerExecutor = Executors.newVirtualThreadPerTaskExecutor();
|
|
||||||
|
|
||||||
private CommandExecutionService() {
|
private CommandExecutionService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,17 +35,15 @@ public class CommandExecutionService {
|
|||||||
return exec(commands.toArray(new String[0]));
|
return exec(commands.toArray(new String[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result exec(String... commands) {
|
public Result exec(WrappedLaunchSpec launchSpec) {
|
||||||
Result result = new Result();
|
Result result = new Result();
|
||||||
List<String> output = new ArrayList<>();
|
List<String> output = new ArrayList<>();
|
||||||
List<String> error = new ArrayList<>();
|
List<String> error = new ArrayList<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Process process = new ProcessBuilder(commands)
|
Process process = startProcess(launchSpec);
|
||||||
.redirectErrorStream(false)
|
|
||||||
.start();
|
|
||||||
|
|
||||||
Thread stdoutThread = new Thread(() -> {
|
Thread stdoutThread = Thread.startVirtualThread(() -> {
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
@@ -57,7 +53,7 @@ public class CommandExecutionService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Thread stderrThread = new Thread(() -> {
|
Thread stderrThread = Thread.startVirtualThread(() -> {
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
@@ -67,9 +63,6 @@ public class CommandExecutionService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
readerExecutor.execute(stdoutThread);
|
|
||||||
readerExecutor.execute(stderrThread);
|
|
||||||
|
|
||||||
int exitCode = process.waitFor();
|
int exitCode = process.waitFor();
|
||||||
stdoutThread.join();
|
stdoutThread.join();
|
||||||
stderrThread.join();
|
stderrThread.join();
|
||||||
@@ -85,15 +78,17 @@ public class CommandExecutionService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Result exec(String... commands) {
|
||||||
|
return exec(defaultLaunchSpec(commands));
|
||||||
|
}
|
||||||
|
|
||||||
public CommandSession createSessionTask(List<String> commands) {
|
public CommandSession createSessionTask(List<String> commands) {
|
||||||
return createSessionTask(commands.toArray(new String[0]));
|
return createSessionTask(commands.toArray(new String[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandSession createSessionTask(String... commands) {
|
public CommandSession createSessionTask(WrappedLaunchSpec launchSpec) {
|
||||||
try {
|
try {
|
||||||
Process process = new ProcessBuilder(commands)
|
Process process = startProcess(launchSpec);
|
||||||
.redirectErrorStream(false)
|
|
||||||
.start();
|
|
||||||
CommandSession session = new CommandSession();
|
CommandSession session = new CommandSession();
|
||||||
StringBuilder stdoutBuffer = new StringBuilder();
|
StringBuilder stdoutBuffer = new StringBuilder();
|
||||||
StringBuilder stderrBuffer = new StringBuilder();
|
StringBuilder stderrBuffer = new StringBuilder();
|
||||||
@@ -101,8 +96,8 @@ public class CommandExecutionService {
|
|||||||
session.setStdoutBuffer(stdoutBuffer);
|
session.setStdoutBuffer(stdoutBuffer);
|
||||||
session.setStderrBuffer(stderrBuffer);
|
session.setStderrBuffer(stderrBuffer);
|
||||||
|
|
||||||
readerExecutor.execute(() -> readToBuffer(process.getInputStream(), stdoutBuffer));
|
Thread.startVirtualThread(() -> readToBuffer(process.getInputStream(), stdoutBuffer));
|
||||||
readerExecutor.execute(() -> readToBuffer(process.getErrorStream(), stderrBuffer));
|
Thread.startVirtualThread(() -> readToBuffer(process.getErrorStream(), stderrBuffer));
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -110,6 +105,10 @@ public class CommandExecutionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandSession createSessionTask(String... commands) {
|
||||||
|
return createSessionTask(defaultLaunchSpec(commands));
|
||||||
|
}
|
||||||
|
|
||||||
private void readToBuffer(java.io.InputStream inputStream, StringBuilder buffer) {
|
private void readToBuffer(java.io.InputStream inputStream, StringBuilder buffer) {
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||||
String line;
|
String line;
|
||||||
@@ -125,6 +124,31 @@ public class CommandExecutionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Process startProcess(WrappedLaunchSpec launchSpec) throws Exception {
|
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder();
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add(launchSpec.getCommand());
|
||||||
|
command.addAll(launchSpec.getArgs());
|
||||||
|
processBuilder.command(command);
|
||||||
|
processBuilder.redirectErrorStream(false);
|
||||||
|
if (launchSpec.getWorkingDirectory() != null && !launchSpec.getWorkingDirectory().isBlank()) {
|
||||||
|
processBuilder.directory(new File(launchSpec.getWorkingDirectory()));
|
||||||
|
}
|
||||||
|
Map<String, String> environment = processBuilder.environment();
|
||||||
|
environment.clear();
|
||||||
|
environment.putAll(launchSpec.getEnvironment());
|
||||||
|
return processBuilder.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private WrappedLaunchSpec defaultLaunchSpec(String... commands) {
|
||||||
|
return new WrappedLaunchSpec(
|
||||||
|
commands[0],
|
||||||
|
List.of(commands).subList(1, commands.length),
|
||||||
|
null,
|
||||||
|
System.getenv()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class Result {
|
public static class Result {
|
||||||
private boolean ok;
|
private boolean ok;
|
||||||
|
|||||||
@@ -29,7 +29,14 @@ public class McpActionExecutor {
|
|||||||
.name(metaAction.getName())
|
.name(metaAction.getName())
|
||||||
.arguments(metaAction.getParams())
|
.arguments(metaAction.getParams())
|
||||||
.build();
|
.build();
|
||||||
McpSchema.CallToolResult callToolResult = mcpClient.callTool(callToolRequest);
|
McpSchema.CallToolResult callToolResult;
|
||||||
|
try {
|
||||||
|
callToolResult = mcpClient.callTool(callToolRequest);
|
||||||
|
} catch (Exception e) {
|
||||||
|
response.setOk(false);
|
||||||
|
response.setData("MCP tool call failed: " + e.getMessage());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
Boolean error = callToolResult.isError();
|
Boolean error = callToolResult.isError();
|
||||||
response.setOk(error == null || !error);
|
response.setOk(error == null || !error);
|
||||||
response.setData(extractResponseData(callToolResult));
|
response.setData(extractResponseData(callToolResult));
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
|||||||
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
|
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static work.slhaf.partner.core.action.ActionCore.ORIGIN_LOCATION;
|
import static work.slhaf.partner.core.action.ActionCore.ORIGIN_LOCATION;
|
||||||
|
|
||||||
@@ -22,10 +20,7 @@ public class OriginExecutionService {
|
|||||||
File file = new File(resolveOriginPath(metaAction));
|
File file = new File(resolveOriginPath(metaAction));
|
||||||
String[] commands = CommandExecutionService.INSTANCE.buildFileExecutionCommands(metaAction.getLauncher(), metaAction.getParams(), file.getAbsolutePath());
|
String[] commands = CommandExecutionService.INSTANCE.buildFileExecutionCommands(metaAction.getLauncher(), metaAction.getParams(), file.getAbsolutePath());
|
||||||
WrappedLaunchSpec wrapped = ExecutionPolicyRegistry.INSTANCE.prepare(Arrays.stream(commands).toList());
|
WrappedLaunchSpec wrapped = ExecutionPolicyRegistry.INSTANCE.prepare(Arrays.stream(commands).toList());
|
||||||
List<String> wrappedCommands = new ArrayList<>();
|
CommandExecutionService.Result execResult = CommandExecutionService.INSTANCE.exec(wrapped);
|
||||||
wrappedCommands.add(wrapped.getCommand());
|
|
||||||
wrappedCommands.addAll(wrapped.getArgs());
|
|
||||||
CommandExecutionService.Result execResult = CommandExecutionService.INSTANCE.exec(wrappedCommands);
|
|
||||||
response.setOk(execResult.isOk());
|
response.setOk(execResult.isOk());
|
||||||
response.setData(execResult.getTotal());
|
response.setData(execResult.getTotal());
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import work.slhaf.partner.common.mcp.InProcessMcpTransport;
|
|||||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||||
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException;
|
import work.slhaf.partner.core.action.exception.ActionInfrastructureStartupException;
|
||||||
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
|
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||||
import work.slhaf.partner.framework.agent.support.DirectoryWatchSupport;
|
import work.slhaf.partner.framework.agent.support.DirectoryWatchSupport;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -331,7 +332,9 @@ public class DynamicActionMcpManager implements AutoCloseable {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
return Mono.fromCallable(() -> {
|
return Mono.fromCallable(() -> {
|
||||||
CommandExecutionService.Result execResult = commandExecutionService.exec(commands);
|
CommandExecutionService.Result execResult = commandExecutionService.exec(
|
||||||
|
ExecutionPolicyRegistry.INSTANCE.prepare(List.of(commands))
|
||||||
|
);
|
||||||
McpSchema.CallToolResult.Builder builder = McpSchema.CallToolResult.builder()
|
McpSchema.CallToolResult.Builder builder = McpSchema.CallToolResult.builder()
|
||||||
.isError(!execResult.isOk());
|
.isError(!execResult.isOk());
|
||||||
List<String> resultList = execResult.getResultList();
|
List<String> resultList = execResult.getResultList();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import java.nio.file.Path;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
@@ -95,14 +96,7 @@ public class McpConfigWatcher implements AutoCloseable, RunnerExecutionPolicyLis
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (String clientId : fileRecord.paramsCacheMap().keySet()) {
|
for (String clientId : fileRecord.paramsCacheMap().keySet()) {
|
||||||
McpSyncClient client = mcpClientRegistry.detach(clientId);
|
unregisterClient(clientId);
|
||||||
if (client == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (McpSchema.Tool tool : client.listTools().tools()) {
|
|
||||||
existedMetaActions.remove(clientId + "::" + tool.name());
|
|
||||||
}
|
|
||||||
client.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +120,39 @@ public class McpConfigWatcher implements AutoCloseable, RunnerExecutionPolicyLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void unregisterClient(String clientId) {
|
||||||
|
McpSyncClient client = mcpClientRegistry.detach(clientId);
|
||||||
|
removeClientActions(clientId, client);
|
||||||
|
if (client != null) {
|
||||||
|
try {
|
||||||
|
client.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[{}] MCP client close failed", clientId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeClientActions(String clientId, McpSyncClient client) {
|
||||||
|
boolean removedByListing = false;
|
||||||
|
if (client != null) {
|
||||||
|
try {
|
||||||
|
List<McpSchema.Tool> tools = client.listTools().tools();
|
||||||
|
if (tools != null) {
|
||||||
|
for (McpSchema.Tool tool : tools) {
|
||||||
|
existedMetaActions.remove(clientId + "::" + tool.name());
|
||||||
|
}
|
||||||
|
removedByListing = true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[{}] MCP client listTools failed during unregister, fallback to key scan", clientId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!removedByListing) {
|
||||||
|
String prefix = clientId + "::";
|
||||||
|
existedMetaActions.keySet().removeIf(actionKey -> actionKey.startsWith(prefix));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private cn.hutool.json.JSONObject readJson(File file) {
|
private cn.hutool.json.JSONObject readJson(File file) {
|
||||||
try {
|
try {
|
||||||
return JSONUtil.readJSONObject(file, StandardCharsets.UTF_8);
|
return JSONUtil.readJSONObject(file, StandardCharsets.UTF_8);
|
||||||
@@ -237,13 +264,16 @@ public class McpConfigWatcher implements AutoCloseable, RunnerExecutionPolicyLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateMcpClients(HashMap<String, McpTransportConfig> changedMap, HashSet<String> existingMcpIdSet) {
|
private void updateMcpClients(HashMap<String, McpTransportConfig> changedMap, HashSet<String> existingMcpIdSet) {
|
||||||
changedMap.forEach(this::registerMcpClient);
|
changedMap.forEach((clientId, config) -> {
|
||||||
|
unregisterClient(clientId);
|
||||||
|
registerMcpClient(clientId, config);
|
||||||
|
});
|
||||||
for (String clientId : mcpClientRegistry.listIds()) {
|
for (String clientId : mcpClientRegistry.listIds()) {
|
||||||
if (clientId.equals(LocalRunnerClient.MCP_NAME_DESC) || clientId.equals(LocalRunnerClient.MCP_NAME_DYNAMIC)) {
|
if (clientId.equals(LocalRunnerClient.MCP_NAME_DESC) || clientId.equals(LocalRunnerClient.MCP_NAME_DYNAMIC)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!existingMcpIdSet.contains(clientId)) {
|
if (!existingMcpIdSet.contains(clientId)) {
|
||||||
mcpClientRegistry.remove(clientId);
|
unregisterClient(clientId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
existedMetaActions.keySet().removeIf(actionKey -> {
|
existedMetaActions.keySet().removeIf(actionKey -> {
|
||||||
|
|||||||
@@ -150,4 +150,8 @@ interface RunnerExecutionPolicyListener {
|
|||||||
fun registerPolicyListener() {
|
fun registerPolicyListener() {
|
||||||
ExecutionPolicyRegistry.addListener(this)
|
ExecutionPolicyRegistry.addListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun unregisterPolicyListener() {
|
||||||
|
ExecutionPolicyRegistry.removeListener(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import com.alibaba.fastjson2.JSONObject;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||||
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
|
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -19,6 +22,7 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
|||||||
private static final int DEFAULT_READ_LIMIT = 4096;
|
private static final int DEFAULT_READ_LIMIT = 4096;
|
||||||
private static final int SUMMARY_MAX_LINES = 5;
|
private static final int SUMMARY_MAX_LINES = 5;
|
||||||
private static final int SUMMARY_MAX_LENGTH = 2048;
|
private static final int SUMMARY_MAX_LENGTH = 2048;
|
||||||
|
private static final Duration COMMAND_SESSION_TTL = Duration.ofMinutes(10);
|
||||||
|
|
||||||
private final Set<String> basicTags = Set.of("Builtin MetaAction", "System Command Tool");
|
private final Set<String> basicTags = Set.of("Builtin MetaAction", "System Command Tool");
|
||||||
|
|
||||||
@@ -58,7 +62,7 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
|||||||
);
|
);
|
||||||
Function<Map<String, Object>, String> invoker = params -> {
|
Function<Map<String, Object>, String> invoker = params -> {
|
||||||
List<String> commands = requireCommandArguments(params);
|
List<String> commands = requireCommandArguments(params);
|
||||||
CommandExecutionService.Result result = commandExecutionService.exec(commands);
|
CommandExecutionService.Result result = commandExecutionService.exec(wrapCommands(commands));
|
||||||
return JSONObject.of("result", result.getTotal()).toJSONString();
|
return JSONObject.of("result", result.getTotal()).toJSONString();
|
||||||
};
|
};
|
||||||
return new BuiltinActionRegistry.BuiltinActionDefinition(
|
return new BuiltinActionRegistry.BuiltinActionDefinition(
|
||||||
@@ -91,9 +95,10 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
|||||||
JSONObject.of("executionId", "Command execution session id.")
|
JSONObject.of("executionId", "Command execution session id.")
|
||||||
);
|
);
|
||||||
Function<Map<String, Object>, String> invoker = params -> {
|
Function<Map<String, Object>, String> invoker = params -> {
|
||||||
|
cleanupExpiredHandles();
|
||||||
String desc = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "desc");
|
String desc = BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "desc");
|
||||||
List<String> commands = requireCommandArguments(params);
|
List<String> commands = requireCommandArguments(params);
|
||||||
CommandExecutionService.CommandSession session = commandExecutionService.createSessionTask(commands);
|
CommandExecutionService.CommandSession session = commandExecutionService.createSessionTask(wrapCommands(commands));
|
||||||
String executionId = UUID.randomUUID().toString();
|
String executionId = UUID.randomUUID().toString();
|
||||||
CommandHandle handle = new CommandHandle(
|
CommandHandle handle = new CommandHandle(
|
||||||
executionId,
|
executionId,
|
||||||
@@ -143,6 +148,7 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
Function<Map<String, Object>, String> invoker = params -> {
|
Function<Map<String, Object>, String> invoker = params -> {
|
||||||
|
cleanupExpiredHandles();
|
||||||
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
|
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
|
||||||
return JSONObject.of(
|
return JSONObject.of(
|
||||||
"executionId", handle.executionId,
|
"executionId", handle.executionId,
|
||||||
@@ -194,6 +200,7 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
Function<Map<String, Object>, String> invoker = params -> {
|
Function<Map<String, Object>, String> invoker = params -> {
|
||||||
|
cleanupExpiredHandles();
|
||||||
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
|
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
|
||||||
String stream = BuiltinActionRegistry.BuiltinActionDefinition.optionalString(params, "stream", "stdout");
|
String stream = BuiltinActionRegistry.BuiltinActionDefinition.optionalString(params, "stream", "stdout");
|
||||||
if (!"stdout".equals(stream) && !"stderr".equals(stream)) {
|
if (!"stdout".equals(stream) && !"stderr".equals(stream)) {
|
||||||
@@ -254,6 +261,7 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
Function<Map<String, Object>, String> invoker = params -> {
|
Function<Map<String, Object>, String> invoker = params -> {
|
||||||
|
cleanupExpiredHandles();
|
||||||
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
|
CommandHandle handle = requireHandle(BuiltinActionRegistry.BuiltinActionDefinition.requireString(params, "id"));
|
||||||
if (handle.process.isAlive()) {
|
if (handle.process.isAlive()) {
|
||||||
handle.process.destroy();
|
handle.process.destroy();
|
||||||
@@ -306,6 +314,7 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
Function<Map<String, Object>, String> invoker = params -> {
|
Function<Map<String, Object>, String> invoker = params -> {
|
||||||
|
cleanupExpiredHandles();
|
||||||
List<JSONObject> items = commandHandles.values().stream()
|
List<JSONObject> items = commandHandles.values().stream()
|
||||||
.sorted(Comparator.comparing(handle -> handle.startAt))
|
.sorted(Comparator.comparing(handle -> handle.startAt))
|
||||||
.map(handle -> JSONObject.of(
|
.map(handle -> JSONObject.of(
|
||||||
@@ -336,6 +345,20 @@ class BuiltinCommandActionProvider implements BuiltinActionProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void cleanupExpiredHandles() {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
commandHandles.entrySet().removeIf(entry -> isExpired(entry.getValue(), now));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isExpired(CommandHandle handle, Instant now) {
|
||||||
|
Instant exitTime = handle.exitAt;
|
||||||
|
return exitTime != null && !exitTime.plus(COMMAND_SESSION_TTL).isAfter(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WrappedLaunchSpec wrapCommands(List<String> commands) {
|
||||||
|
return ExecutionPolicyRegistry.INSTANCE.prepare(commands);
|
||||||
|
}
|
||||||
|
|
||||||
private CommandHandle requireHandle(String id) {
|
private CommandHandle requireHandle(String id) {
|
||||||
CommandHandle handle = commandHandles.get(id);
|
CommandHandle handle = commandHandles.get(id);
|
||||||
if (handle == null) {
|
if (handle == null) {
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package work.slhaf.partner.core.action.runner;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
class LocalRunnerClientCloseTest {
|
||||||
|
|
||||||
|
private static String originalUserHome;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void prepareTestHome() throws IOException {
|
||||||
|
originalUserHome = System.getProperty("user.home");
|
||||||
|
Path tempHome = Files.createTempDirectory("partner-test-home");
|
||||||
|
System.setProperty("user.home", tempHome.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void restoreUserHome() {
|
||||||
|
if (originalUserHome != null) {
|
||||||
|
System.setProperty("user.home", originalUserHome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLocalRunnerClientCloseUnregistersPolicyListener(@TempDir Path tempDir) throws Exception {
|
||||||
|
Field listenersField = ExecutionPolicyRegistry.class.getDeclaredField("listeners");
|
||||||
|
listenersField.setAccessible(true);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
CopyOnWriteArraySet<Object> listeners = (CopyOnWriteArraySet<Object>) listenersField.get(ExecutionPolicyRegistry.INSTANCE);
|
||||||
|
int before = listeners.size();
|
||||||
|
|
||||||
|
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
|
||||||
|
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
|
||||||
|
LocalRunnerClient client = new LocalRunnerClient(existedMetaActions, executor, tempDir.toString());
|
||||||
|
Assertions.assertEquals(before + 1, listeners.size());
|
||||||
|
client.close();
|
||||||
|
Assertions.assertEquals(before, listeners.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,16 +3,25 @@ package work.slhaf.partner.core.action.runner;
|
|||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
import work.slhaf.partner.core.action.entity.MetaAction;
|
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||||
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||||
|
import work.slhaf.partner.core.action.runner.mcp.McpClientRegistry;
|
||||||
|
import work.slhaf.partner.core.action.runner.mcp.McpConfigWatcher;
|
||||||
|
import work.slhaf.partner.core.action.runner.mcp.McpMetaRegistry;
|
||||||
|
import work.slhaf.partner.core.action.runner.mcp.McpTransportConfig;
|
||||||
|
import work.slhaf.partner.core.action.runner.mcp.McpTransportFactory;
|
||||||
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicy;
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicy;
|
||||||
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||||
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
|
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
|
||||||
import work.slhaf.partner.module.action.builtin.BuiltinActionRegistry;
|
import work.slhaf.partner.module.action.builtin.BuiltinActionRegistry;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -31,6 +40,22 @@ import static work.slhaf.partner.core.action.runner.LocalRunnerClientTest.Fs.*;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class LocalRunnerClientTest {
|
public class LocalRunnerClientTest {
|
||||||
|
|
||||||
|
private static String originalUserHome;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void prepareTestHome() throws IOException {
|
||||||
|
originalUserHome = System.getProperty("user.home");
|
||||||
|
Path tempHome = Files.createTempDirectory("partner-test-home");
|
||||||
|
System.setProperty("user.home", tempHome.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void restoreUserHome() {
|
||||||
|
if (originalUserHome != null) {
|
||||||
|
System.setProperty("user.home", originalUserHome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("LoggingSimilarMessage")
|
@SuppressWarnings("LoggingSimilarMessage")
|
||||||
static class Fs {
|
static class Fs {
|
||||||
static void writeRunFile(Path actionDir) throws IOException {
|
static void writeRunFile(Path actionDir) throws IOException {
|
||||||
@@ -839,6 +864,60 @@ public class LocalRunnerClientTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMcpConfigWatcherDeleteFallsBackWhenClientListFails(@TempDir Path tempDir) throws Exception {
|
||||||
|
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
|
||||||
|
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
McpClientRegistry clientRegistry = new McpClientRegistry();
|
||||||
|
McpMetaRegistry metaRegistry = new McpMetaRegistry(existedMetaActions);
|
||||||
|
McpConfigWatcher watcher = new McpConfigWatcher(
|
||||||
|
tempDir,
|
||||||
|
existedMetaActions,
|
||||||
|
clientRegistry,
|
||||||
|
new McpTransportFactory(),
|
||||||
|
metaRegistry,
|
||||||
|
executor
|
||||||
|
);
|
||||||
|
Path configFile = tempDir.resolve("servers.json");
|
||||||
|
Files.writeString(configFile, "{\n}\n");
|
||||||
|
existedMetaActions.put("demo::stale_tool", buildMetaActionInfo("stale"));
|
||||||
|
clientRegistry.register("demo", buildThrowingMcpClient());
|
||||||
|
|
||||||
|
try {
|
||||||
|
Field cacheField = McpConfigWatcher.class.getDeclaredField("mcpConfigFileCache");
|
||||||
|
cacheField.setAccessible(true);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<java.io.File, Object> cache = (Map<java.io.File, Object>) cacheField.get(watcher);
|
||||||
|
|
||||||
|
Class<?> recordClass = Arrays.stream(McpConfigWatcher.class.getDeclaredClasses())
|
||||||
|
.filter(Class::isRecord)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
var constructor = recordClass.getDeclaredConstructor(long.class, long.class, Map.class);
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
Object fileRecord = constructor.newInstance(
|
||||||
|
Files.getLastModifiedTime(configFile).toMillis(),
|
||||||
|
Files.size(configFile),
|
||||||
|
new HashMap<>(Map.of(
|
||||||
|
"demo",
|
||||||
|
new McpTransportConfig.Http(30, "http://127.0.0.1:9", "", Map.of())
|
||||||
|
))
|
||||||
|
);
|
||||||
|
cache.put(configFile.toFile(), fileRecord);
|
||||||
|
|
||||||
|
Method handleDelete = McpConfigWatcher.class.getDeclaredMethod("handleDelete", Path.class, Path.class);
|
||||||
|
handleDelete.setAccessible(true);
|
||||||
|
handleDelete.invoke(watcher, tempDir, configFile);
|
||||||
|
|
||||||
|
Assertions.assertFalse(existedMetaActions.containsKey("demo::stale_tool"));
|
||||||
|
Assertions.assertFalse(clientRegistry.contains("demo"));
|
||||||
|
} finally {
|
||||||
|
watcher.close();
|
||||||
|
metaRegistry.close();
|
||||||
|
executor.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@@ -979,4 +1058,15 @@ public class LocalRunnerClientTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static io.modelcontextprotocol.client.McpSyncClient buildThrowingMcpClient() {
|
||||||
|
try {
|
||||||
|
Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
|
||||||
|
unsafeField.setAccessible(true);
|
||||||
|
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) unsafeField.get(null);
|
||||||
|
return (io.modelcontextprotocol.client.McpSyncClient) unsafe.allocateInstance(io.modelcontextprotocol.client.McpSyncClient.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("failed to build throwing mcp client", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package work.slhaf.partner.core.action.runner.execution;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.WrappedLaunchSpec;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -105,6 +106,32 @@ class CommandExecutionServiceTest {
|
|||||||
Assertions.assertEquals("oops", session.getStderrBuffer().toString());
|
Assertions.assertEquals("oops", session.getStderrBuffer().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecWrappedLaunchSpecAppliesWorkingDirectory(@org.junit.jupiter.api.io.TempDir Path tempDir) {
|
||||||
|
CommandExecutionService.Result result = service.exec(new WrappedLaunchSpec(
|
||||||
|
"sh",
|
||||||
|
List.of("-lc", "pwd"),
|
||||||
|
tempDir.toString(),
|
||||||
|
System.getenv()
|
||||||
|
));
|
||||||
|
|
||||||
|
Assertions.assertTrue(result.isOk());
|
||||||
|
Assertions.assertEquals(tempDir.toString(), result.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecWrappedLaunchSpecAppliesEnvironmentOverride() {
|
||||||
|
CommandExecutionService.Result result = service.exec(new WrappedLaunchSpec(
|
||||||
|
"sh",
|
||||||
|
List.of("-lc", "printf '%s' \"$PARTNER_TEST_ENV\""),
|
||||||
|
null,
|
||||||
|
Map.of("PARTNER_TEST_ENV", "applied")
|
||||||
|
));
|
||||||
|
|
||||||
|
Assertions.assertTrue(result.isOk());
|
||||||
|
Assertions.assertEquals("applied", result.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
private void waitForBufferContains(StringBuilder buffer, String expected) throws InterruptedException {
|
private void waitForBufferContains(StringBuilder buffer, String expected) throws InterruptedException {
|
||||||
long deadline = System.currentTimeMillis() + 2000;
|
long deadline = System.currentTimeMillis() + 2000;
|
||||||
while (System.currentTimeMillis() < deadline) {
|
while (System.currentTimeMillis() < deadline) {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package work.slhaf.partner.core.action.runner.execution;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||||
|
import work.slhaf.partner.core.action.runner.mcp.McpClientRegistry;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
class McpActionExecutorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRunReturnsFailureWhenClientThrows() {
|
||||||
|
McpClientRegistry clientRegistry = new McpClientRegistry();
|
||||||
|
clientRegistry.register("broken", buildThrowingMcpClient());
|
||||||
|
McpActionExecutor executor = new McpActionExecutor(clientRegistry);
|
||||||
|
|
||||||
|
MetaAction metaAction = new MetaAction("demo-tool", false, null, MetaAction.Type.MCP, "broken");
|
||||||
|
metaAction.getParams().putAll(Map.of("value", "demo"));
|
||||||
|
|
||||||
|
var response = executor.run(metaAction);
|
||||||
|
|
||||||
|
Assertions.assertFalse(response.isOk());
|
||||||
|
Assertions.assertTrue(response.getData().startsWith("MCP tool call failed:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private io.modelcontextprotocol.client.McpSyncClient buildThrowingMcpClient() {
|
||||||
|
try {
|
||||||
|
Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
|
||||||
|
unsafeField.setAccessible(true);
|
||||||
|
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) unsafeField.get(null);
|
||||||
|
return (io.modelcontextprotocol.client.McpSyncClient) unsafe.allocateInstance(io.modelcontextprotocol.client.McpSyncClient.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("failed to build throwing mcp client", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package work.slhaf.partner.core.action.runner.execution;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import work.slhaf.partner.core.action.entity.MetaAction;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicy;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
class OriginExecutionServiceTest {
|
||||||
|
|
||||||
|
private static String originalUserHome;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void prepareTestHome() throws IOException {
|
||||||
|
originalUserHome = System.getProperty("user.home");
|
||||||
|
Path tempHome = Files.createTempDirectory("partner-test-home");
|
||||||
|
System.setProperty("user.home", tempHome.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void restoreUserHome() {
|
||||||
|
if (originalUserHome != null) {
|
||||||
|
System.setProperty("user.home", originalUserHome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testOriginExecutionServiceAppliesExecutionPolicyEnvironment(@TempDir Path tempDir) throws IOException {
|
||||||
|
Path script = tempDir.resolve("print_env.py");
|
||||||
|
Files.writeString(script, "import os\nprint(os.getenv('PARTNER_ORIGIN_TEST', ''), end='')\n");
|
||||||
|
|
||||||
|
ExecutionPolicy originalPolicy = new ExecutionPolicy(
|
||||||
|
ExecutionPolicy.Mode.DIRECT,
|
||||||
|
"direct",
|
||||||
|
ExecutionPolicy.Network.ENABLE,
|
||||||
|
true,
|
||||||
|
Map.of(),
|
||||||
|
null,
|
||||||
|
Set.of(),
|
||||||
|
Set.of()
|
||||||
|
);
|
||||||
|
ExecutionPolicyRegistry.INSTANCE.updatePolicy(new ExecutionPolicy(
|
||||||
|
ExecutionPolicy.Mode.DIRECT,
|
||||||
|
"direct",
|
||||||
|
ExecutionPolicy.Network.ENABLE,
|
||||||
|
false,
|
||||||
|
Map.of("PARTNER_ORIGIN_TEST", "origin-applied"),
|
||||||
|
null,
|
||||||
|
Set.of(),
|
||||||
|
Set.of()
|
||||||
|
));
|
||||||
|
|
||||||
|
try {
|
||||||
|
var prepared = ExecutionPolicyRegistry.INSTANCE.prepare(List.of("python3", script.toString()));
|
||||||
|
Assertions.assertEquals("origin-applied", prepared.getEnvironment().get("PARTNER_ORIGIN_TEST"));
|
||||||
|
var directExec = CommandExecutionService.INSTANCE.exec(prepared);
|
||||||
|
Assertions.assertTrue(directExec.isOk());
|
||||||
|
Assertions.assertEquals("origin-applied", directExec.getTotal());
|
||||||
|
OriginExecutionService service = new OriginExecutionService();
|
||||||
|
MetaAction metaAction = new MetaAction("run", false, "python3", MetaAction.Type.ORIGIN, script.toString());
|
||||||
|
var response = service.run(metaAction);
|
||||||
|
Assertions.assertTrue(response.isOk());
|
||||||
|
Assertions.assertEquals("origin-applied", response.getData());
|
||||||
|
} finally {
|
||||||
|
ExecutionPolicyRegistry.INSTANCE.updatePolicy(originalPolicy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package work.slhaf.partner.core.action.runner.mcp;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import work.slhaf.partner.core.action.entity.MetaActionInfo;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicy;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
|
class DynamicActionMcpManagerPolicyTest {
|
||||||
|
|
||||||
|
private static String originalUserHome;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void prepareTestHome() throws IOException {
|
||||||
|
originalUserHome = System.getProperty("user.home");
|
||||||
|
Path tempHome = Files.createTempDirectory("partner-test-home");
|
||||||
|
System.setProperty("user.home", tempHome.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void restoreUserHome() {
|
||||||
|
if (originalUserHome != null) {
|
||||||
|
System.setProperty("user.home", originalUserHome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void testDynamicActionHandlerAppliesExecutionPolicyEnvironment(@TempDir Path tempDir) throws Exception {
|
||||||
|
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
|
||||||
|
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
DynamicActionMcpManager manager = new DynamicActionMcpManager(tempDir, existedMetaActions, executor)) {
|
||||||
|
Path script = tempDir.resolve("run.py");
|
||||||
|
Files.writeString(script, "import os\nprint(os.getenv('PARTNER_DYNAMIC_TEST', ''), end='')\n");
|
||||||
|
|
||||||
|
ExecutionPolicy originalPolicy = new ExecutionPolicy(
|
||||||
|
ExecutionPolicy.Mode.DIRECT,
|
||||||
|
"direct",
|
||||||
|
ExecutionPolicy.Network.ENABLE,
|
||||||
|
true,
|
||||||
|
Map.of(),
|
||||||
|
null,
|
||||||
|
Set.of(),
|
||||||
|
Set.of()
|
||||||
|
);
|
||||||
|
ExecutionPolicyRegistry.INSTANCE.updatePolicy(new ExecutionPolicy(
|
||||||
|
ExecutionPolicy.Mode.DIRECT,
|
||||||
|
"direct",
|
||||||
|
ExecutionPolicy.Network.ENABLE,
|
||||||
|
false,
|
||||||
|
Map.of("PARTNER_DYNAMIC_TEST", "dynamic-applied"),
|
||||||
|
null,
|
||||||
|
Set.of(),
|
||||||
|
Set.of()
|
||||||
|
));
|
||||||
|
|
||||||
|
try {
|
||||||
|
Method method = DynamicActionMcpManager.class.getDeclaredMethod("buildToolHandler", File.class, String.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
BiFunction<?, McpSchema.CallToolRequest, Mono<McpSchema.CallToolResult>> handler =
|
||||||
|
(BiFunction<?, McpSchema.CallToolRequest, Mono<McpSchema.CallToolResult>>) method.invoke(
|
||||||
|
manager,
|
||||||
|
script.toFile(),
|
||||||
|
"python3"
|
||||||
|
);
|
||||||
|
|
||||||
|
McpSchema.CallToolResult result = handler.apply(
|
||||||
|
null,
|
||||||
|
McpSchema.CallToolRequest.builder().name("demo").arguments(Map.of()).build()
|
||||||
|
).block();
|
||||||
|
|
||||||
|
Assertions.assertNotNull(result);
|
||||||
|
Assertions.assertFalse(Boolean.TRUE.equals(result.isError()));
|
||||||
|
Assertions.assertEquals("[dynamic-applied]", String.valueOf(result.structuredContent()));
|
||||||
|
} finally {
|
||||||
|
ExecutionPolicyRegistry.INSTANCE.updatePolicy(originalPolicy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package work.slhaf.partner.module.action.builtin;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicy;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
class BuiltinCommandActionProviderPolicyTest {
|
||||||
|
|
||||||
|
private static String originalUserHome;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void prepareTestHome() throws IOException {
|
||||||
|
originalUserHome = System.getProperty("user.home");
|
||||||
|
Path tempHome = Files.createTempDirectory("partner-test-home");
|
||||||
|
System.setProperty("user.home", tempHome.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void restoreUserHome() {
|
||||||
|
if (originalUserHome != null) {
|
||||||
|
System.setProperty("user.home", originalUserHome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteAppliesExecutionPolicyEnvironment() {
|
||||||
|
BuiltinCommandActionProvider provider = new BuiltinCommandActionProvider();
|
||||||
|
BuiltinActionRegistry.BuiltinActionDefinition execute = requireDefinition(
|
||||||
|
provider.provideBuiltinActions(),
|
||||||
|
"builtin::command::execute"
|
||||||
|
);
|
||||||
|
|
||||||
|
ExecutionPolicy originalPolicy = new ExecutionPolicy(
|
||||||
|
ExecutionPolicy.Mode.DIRECT,
|
||||||
|
"direct",
|
||||||
|
ExecutionPolicy.Network.ENABLE,
|
||||||
|
true,
|
||||||
|
Map.of(),
|
||||||
|
null,
|
||||||
|
Set.of(),
|
||||||
|
Set.of()
|
||||||
|
);
|
||||||
|
ExecutionPolicyRegistry.INSTANCE.updatePolicy(new ExecutionPolicy(
|
||||||
|
ExecutionPolicy.Mode.DIRECT,
|
||||||
|
"direct",
|
||||||
|
ExecutionPolicy.Network.ENABLE,
|
||||||
|
false,
|
||||||
|
Map.of("PARTNER_BUILTIN_TEST", "builtin-applied"),
|
||||||
|
null,
|
||||||
|
Set.of(),
|
||||||
|
Set.of()
|
||||||
|
));
|
||||||
|
|
||||||
|
try {
|
||||||
|
String result = execute.invoker().apply(Map.of(
|
||||||
|
"arg", "sh",
|
||||||
|
"arg1", "-lc",
|
||||||
|
"arg2", "printf '%s' \"$PARTNER_BUILTIN_TEST\""
|
||||||
|
));
|
||||||
|
Assertions.assertEquals("builtin-applied", JSONObject.parseObject(result).getString("result"));
|
||||||
|
} finally {
|
||||||
|
ExecutionPolicyRegistry.INSTANCE.updatePolicy(originalPolicy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStartAppliesExecutionPolicyEnvironment() throws Exception {
|
||||||
|
BuiltinCommandActionProvider provider = new BuiltinCommandActionProvider();
|
||||||
|
List<BuiltinActionRegistry.BuiltinActionDefinition> definitions = provider.provideBuiltinActions();
|
||||||
|
BuiltinActionRegistry.BuiltinActionDefinition start = requireDefinition(definitions, "builtin::command::start");
|
||||||
|
BuiltinActionRegistry.BuiltinActionDefinition inspect = requireDefinition(definitions, "builtin::command::inspect");
|
||||||
|
|
||||||
|
ExecutionPolicy originalPolicy = new ExecutionPolicy(
|
||||||
|
ExecutionPolicy.Mode.DIRECT,
|
||||||
|
"direct",
|
||||||
|
ExecutionPolicy.Network.ENABLE,
|
||||||
|
true,
|
||||||
|
Map.of(),
|
||||||
|
null,
|
||||||
|
Set.of(),
|
||||||
|
Set.of()
|
||||||
|
);
|
||||||
|
ExecutionPolicyRegistry.INSTANCE.updatePolicy(new ExecutionPolicy(
|
||||||
|
ExecutionPolicy.Mode.DIRECT,
|
||||||
|
"direct",
|
||||||
|
ExecutionPolicy.Network.ENABLE,
|
||||||
|
false,
|
||||||
|
Map.of("PARTNER_BUILTIN_TEST", "builtin-session"),
|
||||||
|
null,
|
||||||
|
Set.of(),
|
||||||
|
Set.of()
|
||||||
|
));
|
||||||
|
|
||||||
|
try {
|
||||||
|
String startResult = start.invoker().apply(Map.of(
|
||||||
|
"desc", "policy-session",
|
||||||
|
"arg", "sh",
|
||||||
|
"arg1", "-lc",
|
||||||
|
"arg2", "printf '%s' \"$PARTNER_BUILTIN_TEST\""
|
||||||
|
));
|
||||||
|
String executionId = JSONObject.parseObject(startResult).getString("executionId");
|
||||||
|
Assertions.assertNotNull(executionId);
|
||||||
|
|
||||||
|
JSONObject inspectResult = waitForInspectExit(inspect, executionId);
|
||||||
|
Assertions.assertTrue(inspectResult.getString("stdoutSummary").contains("builtin-session"));
|
||||||
|
} finally {
|
||||||
|
ExecutionPolicyRegistry.INSTANCE.updatePolicy(originalPolicy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BuiltinActionRegistry.BuiltinActionDefinition requireDefinition(
|
||||||
|
List<BuiltinActionRegistry.BuiltinActionDefinition> definitions,
|
||||||
|
String actionKey
|
||||||
|
) {
|
||||||
|
return definitions.stream()
|
||||||
|
.filter(definition -> actionKey.equals(definition.actionKey()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("definition not found: " + actionKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject waitForInspectExit(BuiltinActionRegistry.BuiltinActionDefinition inspectDefinition, String executionId) throws Exception {
|
||||||
|
long deadline = System.currentTimeMillis() + 3000;
|
||||||
|
while (System.currentTimeMillis() < deadline) {
|
||||||
|
JSONObject inspect = JSONObject.parseObject(inspectDefinition.invoker().apply(Map.of(
|
||||||
|
"id", executionId
|
||||||
|
)));
|
||||||
|
if (inspect.get("exitCode") != null) {
|
||||||
|
return inspect;
|
||||||
|
}
|
||||||
|
Thread.sleep(20);
|
||||||
|
}
|
||||||
|
throw new AssertionError("command session did not exit in time");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package work.slhaf.partner.module.action.builtin;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicy;
|
||||||
|
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicyRegistry;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
class BuiltinCommandActionProviderTtlTest {
|
||||||
|
|
||||||
|
private static String originalUserHome;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void prepareTestHome() throws IOException {
|
||||||
|
originalUserHome = System.getProperty("user.home");
|
||||||
|
Path tempHome = Files.createTempDirectory("partner-test-home");
|
||||||
|
System.setProperty("user.home", tempHome.toString());
|
||||||
|
ExecutionPolicyRegistry.INSTANCE.updatePolicy(new ExecutionPolicy(
|
||||||
|
ExecutionPolicy.Mode.DIRECT,
|
||||||
|
"direct",
|
||||||
|
ExecutionPolicy.Network.ENABLE,
|
||||||
|
true,
|
||||||
|
Map.of(),
|
||||||
|
null,
|
||||||
|
Set.of(),
|
||||||
|
Set.of()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void restoreUserHome() {
|
||||||
|
if (originalUserHome != null) {
|
||||||
|
System.setProperty("user.home", originalUserHome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testOverviewRemovesExpiredFinishedSessions() throws Exception {
|
||||||
|
BuiltinCommandActionProvider provider = new BuiltinCommandActionProvider();
|
||||||
|
List<BuiltinActionRegistry.BuiltinActionDefinition> definitions = provider.provideBuiltinActions();
|
||||||
|
BuiltinActionRegistry.BuiltinActionDefinition start = requireDefinition(definitions, "builtin::command::start");
|
||||||
|
BuiltinActionRegistry.BuiltinActionDefinition overview = requireDefinition(definitions, "builtin::command::overview");
|
||||||
|
BuiltinActionRegistry.BuiltinActionDefinition inspect = requireDefinition(definitions, "builtin::command::inspect");
|
||||||
|
|
||||||
|
String startResult = start.invoker().apply(Map.of(
|
||||||
|
"desc", "ttl-session",
|
||||||
|
"arg", "sh",
|
||||||
|
"arg1", "-lc",
|
||||||
|
"arg2", "printf 'done'"
|
||||||
|
));
|
||||||
|
String executionId = JSONObject.parseObject(startResult).getString("executionId");
|
||||||
|
waitForInspectExit(inspect, executionId);
|
||||||
|
|
||||||
|
expireHandle(provider, executionId);
|
||||||
|
|
||||||
|
JSONObject overviewResult = JSONObject.parseObject(overview.invoker().apply(Map.of()));
|
||||||
|
JSONArray result = overviewResult.getJSONArray("result");
|
||||||
|
Assertions.assertTrue(result.stream().map(item -> (JSONObject) item)
|
||||||
|
.noneMatch(item -> executionId.equals(item.getString("executionId"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInspectRejectsExpiredFinishedSession() throws Exception {
|
||||||
|
BuiltinCommandActionProvider provider = new BuiltinCommandActionProvider();
|
||||||
|
List<BuiltinActionRegistry.BuiltinActionDefinition> definitions = provider.provideBuiltinActions();
|
||||||
|
BuiltinActionRegistry.BuiltinActionDefinition start = requireDefinition(definitions, "builtin::command::start");
|
||||||
|
BuiltinActionRegistry.BuiltinActionDefinition inspect = requireDefinition(definitions, "builtin::command::inspect");
|
||||||
|
|
||||||
|
String startResult = start.invoker().apply(Map.of(
|
||||||
|
"desc", "ttl-session-inspect",
|
||||||
|
"arg", "sh",
|
||||||
|
"arg1", "-lc",
|
||||||
|
"arg2", "printf 'done'"
|
||||||
|
));
|
||||||
|
String executionId = JSONObject.parseObject(startResult).getString("executionId");
|
||||||
|
waitForInspectExit(inspect, executionId);
|
||||||
|
|
||||||
|
expireHandle(provider, executionId);
|
||||||
|
|
||||||
|
Assertions.assertThrows(IllegalArgumentException.class, () -> inspect.invoker().apply(Map.of(
|
||||||
|
"id", executionId
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expireHandle(BuiltinCommandActionProvider provider, String executionId) throws Exception {
|
||||||
|
Field handlesField = BuiltinCommandActionProvider.class.getDeclaredField("commandHandles");
|
||||||
|
handlesField.setAccessible(true);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ConcurrentHashMap<String, Object> handles = (ConcurrentHashMap<String, Object>) handlesField.get(provider);
|
||||||
|
Object handle = handles.get(executionId);
|
||||||
|
Assertions.assertNotNull(handle);
|
||||||
|
|
||||||
|
Field exitCodeField = handle.getClass().getDeclaredField("exitCode");
|
||||||
|
exitCodeField.setAccessible(true);
|
||||||
|
exitCodeField.set(handle, 0);
|
||||||
|
|
||||||
|
Field exitAtField = handle.getClass().getDeclaredField("exitAt");
|
||||||
|
exitAtField.setAccessible(true);
|
||||||
|
exitAtField.set(handle, Instant.now().minus(11, ChronoUnit.MINUTES));
|
||||||
|
}
|
||||||
|
|
||||||
|
private BuiltinActionRegistry.BuiltinActionDefinition requireDefinition(
|
||||||
|
List<BuiltinActionRegistry.BuiltinActionDefinition> definitions,
|
||||||
|
String actionKey
|
||||||
|
) {
|
||||||
|
return definitions.stream()
|
||||||
|
.filter(definition -> actionKey.equals(definition.actionKey()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("definition not found: " + actionKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject waitForInspectExit(BuiltinActionRegistry.BuiltinActionDefinition inspectDefinition, String executionId) throws Exception {
|
||||||
|
long deadline = System.currentTimeMillis() + 3000;
|
||||||
|
while (System.currentTimeMillis() < deadline) {
|
||||||
|
JSONObject inspect = JSONObject.parseObject(inspectDefinition.invoker().apply(Map.of(
|
||||||
|
"id", executionId
|
||||||
|
)));
|
||||||
|
if (inspect.get("exitCode") != null) {
|
||||||
|
return inspect;
|
||||||
|
}
|
||||||
|
Thread.sleep(20);
|
||||||
|
}
|
||||||
|
throw new AssertionError("command session did not exit in time");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -157,7 +157,11 @@ public class DirectoryWatchSupport implements Closeable {
|
|||||||
if (handler == null) {
|
if (handler == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
handler.handle(thisDir, resolvedContext);
|
try {
|
||||||
|
handler.handle(thisDir, resolvedContext);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("监听事件处理失败: dir={}, kind={}, context={}", thisDir, kind.name(), resolvedContext, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
log.info("监听线程被中断,准备退出...");
|
log.info("监听线程被中断,准备退出...");
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ import java.nio.file.Path;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
class DirectoryWatchSupportTest {
|
class DirectoryWatchSupportTest {
|
||||||
@@ -115,6 +118,41 @@ class DirectoryWatchSupportTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHandlerExceptionDoesNotStopWatching(@TempDir Path tempDir) throws Exception {
|
||||||
|
AtomicBoolean shouldThrow = new AtomicBoolean(true);
|
||||||
|
CountDownLatch goodEventLatch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
|
||||||
|
DirectoryWatchSupport watchSupport = new DirectoryWatchSupport(
|
||||||
|
new DirectoryWatchSupport.Context(tempDir),
|
||||||
|
executor,
|
||||||
|
0,
|
||||||
|
null
|
||||||
|
)) {
|
||||||
|
List<String> events = new CopyOnWriteArrayList<>();
|
||||||
|
watchSupport.onCreate((thisDir, context) -> {
|
||||||
|
if (context == null || Files.isDirectory(context)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String relative = tempDir.relativize(context).toString().replace('\\', '/');
|
||||||
|
if (shouldThrow.getAndSet(false)) {
|
||||||
|
throw new IllegalStateException("boom");
|
||||||
|
}
|
||||||
|
events.add(relative);
|
||||||
|
goodEventLatch.countDown();
|
||||||
|
});
|
||||||
|
|
||||||
|
watchSupport.start();
|
||||||
|
|
||||||
|
Files.writeString(tempDir.resolve("first.txt"), "first");
|
||||||
|
Files.writeString(tempDir.resolve("second.txt"), "second");
|
||||||
|
|
||||||
|
Assertions.assertTrue(goodEventLatch.await(2, TimeUnit.SECONDS));
|
||||||
|
Assertions.assertEquals(List.of("second.txt"), events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private WatchHarness createWatchSupport(Path root, ExecutorService executor, int watchDepth) throws IOException {
|
private WatchHarness createWatchSupport(Path root, ExecutorService executor, int watchDepth) throws IOException {
|
||||||
DirectoryWatchSupport watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, watchDepth, null);
|
DirectoryWatchSupport watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, watchDepth, null);
|
||||||
List<String> events = new CopyOnWriteArrayList<>();
|
List<String> events = new CopyOnWriteArrayList<>();
|
||||||
|
|||||||
Reference in New Issue
Block a user