feat: add ProcessExecutor, FAKE mode, fix thread lifecycle crash

- Add ProcessExecutor class wrapping PIProcess with proper cleanup
- Add CMake FAKE option (-DFAKE=ON) for testing without opencode
- Add 3 FAKE-mode tests for full pipeline execution, status transitions, output
- Fix PIThread lifecycle: use startOnce() instead of start(), track threads,
  wait + delete in destructor to prevent use-after-free of PIDeque<PIChar>
- Update build.sh with --fake flag and automatic cache invalidation
- Upgrade CMake to C++17 standard
- Update AGENTS.md with build commands and FAKE mode
This commit is contained in:
2026-07-16 09:51:39 +03:00
parent 46e12ef5fe
commit 5ca6927baa
8 changed files with 361 additions and 34 deletions
+47 -25
View File
@@ -4,12 +4,21 @@
#include <piprocess.h>
#include <pisystemtime.h>
#include <pithread.h>
#include <pitime.h>
PipelineRunner::PipelineRunner(const PIString & pipelines_dir, const PIString & logs_dir)
: pipelines_dir_(pipelines_dir)
, logs_dir_(logs_dir) {}
PipelineRunner::~PipelineRunner() {}
PipelineRunner::~PipelineRunner() {
for (int i = 0; i < threads_.size(); ++i) {
if (threads_[i]) {
threads_[i]->waitForFinish(PISystemTime::fromSeconds(30));
delete threads_[i];
}
}
}
PIString PipelineRunner::startRun(const PIString & pipeline_id) {
Pipeline pipeline = findPipeline(pipelines_dir_, pipeline_id);
@@ -41,12 +50,10 @@ PIString PipelineRunner::startRun(const PIString & pipeline_id) {
appendLog(run_id, "Starting pipeline: " + pipeline.name + " (run_id: " + run_id + ")");
// Execute in a separate thread
// Use a lambda that captures a copy of the pipeline and run_id
PIString rid = run_id;
PIString pdir = pipelines_dir_;
PIString ldir = logs_dir_;
PIThread * thread = new PIThread([this, rid, pipeline, pdir, ldir]() { executePipeline(rid, pipeline); });
thread->start();
PIThread * thread = new PIThread([this, rid, pipeline]() { executePipeline(rid, pipeline); });
thread->startOnce();
threads_ << thread;
return run_id;
}
@@ -67,6 +74,34 @@ bool PipelineRunner::isRunActive(const PIString & run_id) {
void PipelineRunner::executePipeline(const PIString & run_id, const Pipeline & pipeline) {
int total = pipeline.prompts.size();
#ifdef FAKE
// Fake mode: simulate all steps and update state atomically
for (int i = 0; i < total; ++i) {
const Prompt & prompt = pipeline.prompts[i];
appendLog(run_id, "[Step " + PIString::fromNumber(i + 1) + "/" + PIString::fromNumber(total) + "] Starting: " + prompt.title);
appendLog(run_id, "[Step " + PIString::fromNumber(i + 1) + "/" + PIString::fromNumber(total) + "] Prompt: " + prompt.text);
piMSleep(100);
appendLog(run_id, "[Step " + PIString::fromNumber(i + 1) + "/" + PIString::fromNumber(total) + "] COMPLETED (returncode: 0)");
}
// Update all step results and run status in a single mutex section
{
PIMutexLocker ml(mutex_);
if (runs_.contains(run_id)) {
for (int i = 0; i < total; ++i) {
runs_[run_id].current_step = i;
runs_[run_id].steps[i].status = RunStatus::Completed;
runs_[run_id].steps[i].returncode = 0;
runs_[run_id].steps[i].output = "FAKE: executed step " + PIString::fromNumber(i) + ": " + pipeline.prompts[i].title;
runs_[run_id].steps[i].error = "";
}
runs_[run_id].status = RunStatus::Completed;
}
active_runs_.remove(run_id);
}
appendLog(run_id, "Pipeline finished");
#else
for (int i = 0; i < total; ++i) {
const Prompt & prompt = pipeline.prompts[i];
appendLog(run_id, "[Step " + PIString::fromNumber(i + 1) + "/" + PIString::fromNumber(total) + "] Starting: " + prompt.title);
@@ -82,29 +117,15 @@ void PipelineRunner::executePipeline(const PIString & run_id, const Pipeline & p
}
// Build command: opencode run <text> --title <title>
PIProcess proc;
proc.enableReadStdOut(true);
proc.enableReadStdErr(true);
if (!pipeline.working_dir.isEmpty()) {
proc.setWorkingDirectory(pipeline.working_dir);
}
PIStringList args;
args << "run" << prompt.text << "--title" << prompt.title;
proc.exec("opencode", args);
// Wait for process to finish (max 300 seconds per step)
proc.waitForFinish(PISystemTime::fromSeconds(300));
ProcessResult result = ProcessExecutor::run("opencode", args, 300, pipeline.working_dir);
int rc = result.exitCode;
PIString output = result.output;
PIString error = result.error;
PIByteArray stdoutData = proc.readOutput();
PIByteArray stderrData = proc.readError();
int rc = proc.exitCode();
PIString output = PIString::fromUTF8(stdoutData);
PIString error = PIString::fromUTF8(stderrData);
RunStatus stepStatus = (rc == 0) ? RunStatus::Completed : RunStatus::Error;
RunStatus stepStatus = (rc == 0) ? RunStatus::Completed : RunStatus::Error;
appendLog(run_id,
"[Step " + PIString::fromNumber(i + 1) + "/" + PIString::fromNumber(total) + "] " +
runStatusToString(stepStatus).toUpperCase() + " (returncode: " + PIString::fromNumber(rc) + ")");
@@ -142,6 +163,7 @@ void PipelineRunner::executePipeline(const PIString & run_id, const Pipeline & p
}
appendLog(run_id, "Pipeline finished");
#endif
}
void PipelineRunner::appendLog(const PIString & run_id, const PIString & line) {