44import okhttp3 .extension .logging .HttpLogLevel ;
55import org .apache .commons .exec .CommandLine ;
66import org .apache .commons .exec .DefaultExecutor ;
7+ import org .apache .commons .exec .ExecuteException ;
78import org .apache .commons .exec .ExecuteWatchdog ;
89import org .slf4j .Logger ;
910import org .slf4j .LoggerFactory ;
1314import java .io .IOException ;
1415import java .nio .charset .StandardCharsets ;
1516import java .util .Objects ;
17+ import java .util .concurrent .Semaphore ;
1618
1719/**
1820 * Executor for the local {@code opencode} CLI subprocess.
@@ -37,13 +39,21 @@ public class OpenCodeCliExecutor {
3739 */
3840 private final OpenCodeCliConfig config ;
3941
42+ /**
43+ * 并发执行闸门;{@code maxConcurrentExecutions <= 0} 时为 {@code null}
44+ * (不限并发)。每个 executor 实例独立一把。
45+ */
46+ private final Semaphore executionGate ;
47+
4048 /**
4149 * 创建 open code cli executor 实例,并按传入依赖确定资源所有权。
4250 *
4351 * @param config 客户端配置;不得为 {@code null}
4452 */
4553 public OpenCodeCliExecutor (OpenCodeCliConfig config ) {
4654 this .config = Objects .requireNonNull (config , "config" );
55+ int max = config .getMaxConcurrentExecutions ();
56+ this .executionGate = max > 0 ? new Semaphore (max ) : null ;
4757 }
4858
4959 /**
@@ -53,6 +63,24 @@ public OpenCodeCliExecutor(OpenCodeCliConfig config) {
5363 * @return CLI 的退出状态、标准输出和错误输出
5464 */
5565 public OpenCodeCliResult execute (String ... args ) {
66+ Semaphore gate = executionGate ;
67+ if (gate == null ) {
68+ return runProcess (args );
69+ }
70+ try {
71+ gate .acquire ();
72+ } catch (InterruptedException e ) {
73+ Thread .currentThread ().interrupt ();
74+ return new OpenCodeCliResult (-1 , "" , "interrupted while waiting for the CLI execution gate" );
75+ }
76+ try {
77+ return runProcess (args );
78+ } finally {
79+ gate .release ();
80+ }
81+ }
82+
83+ private OpenCodeCliResult runProcess (String ... args ) {
5684 CommandLine cmd = CommandLine .parse (config .getExecutable ());
5785 for (String arg : args ) {
5886 // handleQuoting=false:子进程经 exec(argv) 启动而非 shell,
@@ -76,6 +104,7 @@ public OpenCodeCliResult execute(String... args) {
76104 ExecuteWatchdog watchdog = new ExecuteWatchdog (timeoutMs );
77105 executor .setWatchdog (watchdog );
78106
107+ long startNanos = System .nanoTime ();
79108 try {
80109 int exitCode = executor .execute (cmd );
81110 // 显式 UTF-8 解码:toString() 走平台默认字符集,GBK 默认字符集的
@@ -89,7 +118,22 @@ public OpenCodeCliResult execute(String... args) {
89118 if (config .getDebug ().allows (HttpLogLevel .BODY )) {
90119 log .debug ("OpenCode CLI output: stdout={}, stderr={}" , truncate (out ), truncate (err ));
91120 }
121+ if (watchdog .killedProcess ()) {
122+ return new OpenCodeCliResult (-1 , out , "opencode CLI timed out after " + timeoutMs + " ms\n " + err );
123+ }
92124 return new OpenCodeCliResult (exitCode , out , err );
125+ } catch (ExecuteException e ) {
126+ // commons-exec 对每次非零退出抛 ExecuteException;泵线程在抛出前
127+ // 已 join,两路缓冲完整——连同真实退出码一并返回,不再折叠为
128+ // -1 + 空输出。超时判定用截止时间法,规避 killedProcess() 观察竞态。
129+ String out = stdout .toString (StandardCharsets .UTF_8 ).trim ();
130+ String err = stderr .toString (StandardCharsets .UTF_8 ).trim ();
131+ boolean timedOut = watchdog .killedProcess ()
132+ || System .nanoTime () - startNanos >= timeoutMs * 1_000_000L ;
133+ if (timedOut ) {
134+ return new OpenCodeCliResult (-1 , out , "opencode CLI timed out after " + timeoutMs + " ms\n " + err );
135+ }
136+ return new OpenCodeCliResult (e .getExitValue (), out , err );
93137 } catch (IOException e ) {
94138 return new OpenCodeCliResult (-1 , "" , e .getMessage ());
95139 }
0 commit comments