Skip to content

Commit 98e3db1

Browse files
committed
fix(process): enforce stable capacity deadlines and bounded native capture
C02 RED at 31bde9c: 2046 tests, 8 failures, 0 errors/skips. Use fixed per-client or explicit shared runtimes, monotonic queue/execution budgets, 8MiB stdout and 2MiB stderr caps, per-call cancellation and owned-root cleanup. Preserve lifecycle reasons, byte counts and unknown descendant status in results. Quarantine runtimes with unconfirmed owned root/reader cleanup; keep legacy API bridge. Remote cancellation and unverified descendant ownership are not claimed.
1 parent 31bde9c commit 98e3db1

8 files changed

Lines changed: 541 additions & 404 deletions

‎src/main/java/io/github/easy4j/opencli/OpenCliProperties.java‎

Lines changed: 22 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -7,119 +7,56 @@
77
import lombok.Data;
88

99
/**
10-
* OpenCLI runtime configuration POJO with no Spring dependency.
11-
* <p>
12-
* 描述可执行文件、工作目录、超时、全局 argv 前缀、远端 Agent 以及需要注入子进程的环境变量
13-
*(例如 {@code OPENCLI_CDP_ENDPOINT})。Spring Boot 可由上层以
14-
* {@code @ConfigurationProperties(prefix = "opencli")} 绑定同名字段。
15-
* </p>
16-
* <p>
17-
* {@link #commandTimeoutMillis} 在本地模式下用于子进程 Watchdog;在
18-
* {@link OpenCliExecutionTarget#REMOTE_AGENT_HTTP} 模式下用作 HTTP 客户端超时上限。
19-
* </p>
20-
*/
21-
@Data/**
22-
23-
* OpenCLI runtime configuration POJO with no Spring dependency.
24-
*
25-
* <p>Describes the executable path, working directory, timeout, global argv prefix,
26-
* remote Agent settings, and environment variables injected into the subprocess
27-
* (e.g.&nbsp;{@code OPENCLI_CDP_ENDPOINT}). Spring Boot applications can bind these
28-
* fields via {@code @ConfigurationProperties(prefix = "opencli")}.</p>
10+
* OpenCLI runtime configuration without Spring dependencies.
11+
* Local execution uses a monotonic submission-to-exit deadline; cleanup has a
12+
* separate finite grace. Remote HTTP retains its own transport timeout semantics.
2913
*
30-
* <p>{@link #commandTimeoutMillis} is used as the subprocess watchdog timeout in local
31-
* mode and as the HTTP client timeout in {@link OpenCliExecutionTarget#REMOTE_AGENT_HTTP} mode.</p>
32-
33-
*
34-
3514
* @author <a href="https://github.com/loong10k">Loong Wan</a>
36-
3715
* @since 3.0.0
38-
3916
*/
40-
17+
@Data
4118
public class OpenCliProperties {
42-
43-
/**
44-
* 执行目标:本机进程或与 opencli-admin 兼容的远端 Agent。
45-
*/
4619
private OpenCliExecutionTarget executionTarget = OpenCliExecutionTarget.LOCAL_PROCESS;
47-
48-
/**
49-
* 远端 Agent 根 URL(不含尾斜杠),例如 {@code http://192.168.1.10:19823}。
50-
* <p>仅当 {@link #executionTarget} 为 {@link OpenCliExecutionTarget#REMOTE_AGENT_HTTP} 时必填。</p>
51-
*/
20+
/** Root URL of an opencli-admin compatible Agent. */
5221
private String remoteAgentBaseUrl;
53-
54-
/**
55-
* 传给 Agent collect 的 {@code mode}:{@code bridge} 或 {@code cdp}(与 Agent 环境一致)。
56-
*/
5722
private String remoteCollectMode = "cdp";
58-
59-
/**
60-
* 传给 Agent collect 的默认 {@code format}(可被 argv 中的 {@code -f} 覆盖)。
61-
*/
6223
private String remoteOutputFormat = "json";
63-
64-
/**
65-
* 对应 collect body 的 {@code cdp_endpoint};空表示由 Agent 使用自身 {@code OPENCLI_CDP_ENDPOINT}。
66-
*/
6724
private String remoteCdpEndpoint = "";
68-
69-
/**
70-
* 为 true 时,远程模式下将 Agent HTTP 响应原文写入 {@link io.github.easy4j.opencli.core.OpenCliResult} 的
71-
* {@code remoteRawHttpBody} 字段;
72-
* 本地模式无效果。大响应时请谨慎开启。
73-
*/
25+
/** Raw HTTP capture is explicitly opt-in and may contain sensitive business data. */
7426
private boolean remoteCaptureRawHttpResponse = false;
75-
76-
/**
77-
* OpenCLI 可执行文件名或绝对路径;默认假定已在 {@code PATH} 中。
78-
*/
7927
private String executable = "opencli";
80-
81-
/**
82-
* 子进程工作目录;为空时使用 JVM 当前目录。
83-
*/
8428
private String workingDirectory;
85-
86-
/**
87-
* 单次调用超时(毫秒):本地模式用于子进程 Watchdog;远程模式用于 Agent HTTP 请求。
88-
*/
29+
/** Total local queue plus execution budget; remote mode uses an HTTP timeout. */
8930
private long commandTimeoutMillis = 300_000L;
90-
91-
/**
92-
* 本机 CLI 子进程最大并发数;小于等于 0 时使用 CPU 核心数与 2 的较大值。
93-
*/
31+
/** Positive per-client capacity, zero for max(2, cores); negative is invalid. Captured at construction. */
9432
private int maxConcurrentExecutions = 0;
95-
96-
/**
97-
* 启动探测({@code opencli list})专用超时(毫秒);小于等于 0 时探测使用 30 秒。
98-
*/
33+
/** Maximum bytes retained from stdout for one local invocation. */
34+
private int maxStdoutBytes = 8 * 1024 * 1024;
35+
/** Maximum bytes retained from stderr for one local invocation. */
36+
private int maxStderrBytes = 2 * 1024 * 1024;
37+
/** Independent bounded cleanup grace, in milliseconds. */
38+
private long cleanupGraceMillis = 5_000L;
9939
private long startupProbeTimeoutMillis = 30_000L;
100-
101-
/**
102-
* 附加到 {@code opencli} 之后的<strong>最前</strong>参数(在 adapter 名之前),便于预留 profile 等扩展。
103-
*/
40+
/** Literal prefix arguments placed before the command identifier. */
10441
private List<String> leadingArguments = new ArrayList<>();
105-
106-
/**
107-
* 合并进子进程环境的键值;覆盖同名系统环境变量。
108-
*/
42+
/** Variables overlaying the inherited process environment. */
10943
private Map<String, String> environment = new LinkedHashMap<>();
11044

11145
/**
112-
* 复制为「仅本机子进程」配置,供边缘 WebSocket Agent 处理中心下发的 {@code collect} 时使用,
113-
* 避免误将 collect 再次转发为 {@link OpenCliExecutionTarget#REMOTE_AGENT_HTTP} 而形成回路。
46+
* Copy into a local-only configuration for reverse-Agent execution without
47+
* forwarding a received command back to a remote Agent.
11448
*
115-
* @return 新实例,不会改变当前对象
49+
* @return an independent local configuration
11650
*/
11751
public OpenCliProperties copyForLocalCliExecution() {
11852
OpenCliProperties c = new OpenCliProperties();
11953
c.setExecutable(this.executable);
12054
c.setWorkingDirectory(this.workingDirectory);
12155
c.setCommandTimeoutMillis(this.commandTimeoutMillis);
12256
c.setMaxConcurrentExecutions(this.maxConcurrentExecutions);
57+
c.setMaxStdoutBytes(this.maxStdoutBytes);
58+
c.setMaxStderrBytes(this.maxStderrBytes);
59+
c.setCleanupGraceMillis(this.cleanupGraceMillis);
12360
c.setStartupProbeTimeoutMillis(this.startupProbeTimeoutMillis);
12461
c.setLeadingArguments(new ArrayList<>(this.leadingArguments));
12562
c.setEnvironment(new LinkedHashMap<>(this.environment));
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.github.easy4j.opencli.core;
2+
3+
import java.util.concurrent.atomic.AtomicBoolean;
4+
5+
/** Cooperative cancellation for one local invocation; cancellation never affects another invocation. */
6+
public final class OpenCliCancellationToken {
7+
private final AtomicBoolean cancelled = new AtomicBoolean();
8+
9+
/** Request cancellation. This operation is idempotent. */
10+
public void cancel() { cancelled.set(true); }
11+
12+
/** @return whether cancellation has been requested */
13+
public boolean isCancelled() { return cancelled.get(); }
14+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.github.easy4j.opencli.core;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
/** Immutable execution evidence. Observed byte counts describe bytes read, not bytes produced remotely. */
7+
@Getter
8+
@Builder
9+
public final class OpenCliExecutionDetails {
10+
/** First terminal condition selected by the invocation owner. */
11+
public enum TerminationReason {
12+
PROCESS_EXIT, QUEUE_TIMEOUT, EXECUTION_TIMEOUT, OUTPUT_LIMIT,
13+
CANCELLED, SPAWN_FAILED, IO_FAILURE, CLEANUP_UNCONFIRMED, RUNTIME_UNAVAILABLE
14+
}
15+
16+
/** Confirmation concerns the directly owned child, not an arbitrary process tree. */
17+
public enum CleanupState { NOT_STARTED, ROOT_EXIT_CONFIRMED, UNCONFIRMED }
18+
19+
private final TerminationReason terminationReason;
20+
private final CleanupState cleanupState;
21+
private final boolean processStarted;
22+
private final boolean streamsDrained;
23+
private final long stdoutCapturedBytes;
24+
private final long stdoutObservedBytes;
25+
private final boolean stdoutTruncated;
26+
private final long stderrCapturedBytes;
27+
private final long stderrObservedBytes;
28+
private final boolean stderrTruncated;
29+
private final long elapsedMillis;
30+
private final long queueWaitMillis;
31+
/** This portable backend does not claim ownership/termination of detached daemon descendants. */
32+
private final boolean descendantsExitConfirmed;
33+
}

0 commit comments

Comments
 (0)