feat: config opencode path and timeout (opencode_path, opencode_timeout)

This commit is contained in:
2026-07-20 21:23:43 +03:00
parent d1e96d35aa
commit 99e9f880ee
9 changed files with 71 additions and 19 deletions
+3 -1
View File
@@ -2,5 +2,7 @@
"port": 8000, "port": 8000,
"pipelines_dir": "./storage/pipelines/", "pipelines_dir": "./storage/pipelines/",
"logs_dir": "./storage/logs/", "logs_dir": "./storage/logs/",
"web_root": "../../frontend/build/web/" "web_root": "../../frontend/build/web/",
"opencode_path": "opencode",
"opencode_timeout": 300
} }
+18
View File
@@ -7,12 +7,16 @@
static const int DefaultPort = 8000; static const int DefaultPort = 8000;
static const PIString DefaultPipelinesDir = "./storage/pipelines/"; static const PIString DefaultPipelinesDir = "./storage/pipelines/";
static const PIString DefaultLogsDir = "./storage/logs/"; static const PIString DefaultLogsDir = "./storage/logs/";
static const PIString DefaultOpencodePath = "opencode";
static const int DefaultOpencodeTimeout = 300;
ServerConfig::ServerConfig() ServerConfig::ServerConfig()
: port_(DefaultPort) : port_(DefaultPort)
, pipelines_dir_(DefaultPipelinesDir) , pipelines_dir_(DefaultPipelinesDir)
, logs_dir_(DefaultLogsDir) , logs_dir_(DefaultLogsDir)
, web_root_() , web_root_()
, opencode_path_(DefaultOpencodePath)
, opencode_timeout_(DefaultOpencodeTimeout)
, valid_(true) {} , valid_(true) {}
bool ServerConfig::loadFromFile(const PIString & path) { bool ServerConfig::loadFromFile(const PIString & path) {
@@ -45,6 +49,12 @@ bool ServerConfig::loadFromFile(const PIString & path) {
if (config.contains("web_root")) { if (config.contains("web_root")) {
web_root_ = config["web_root"].toString(); web_root_ = config["web_root"].toString();
} }
if (config.contains("opencode_path")) {
opencode_path_ = config["opencode_path"].toString();
}
if (config.contains("opencode_timeout")) {
opencode_timeout_ = config["opencode_timeout"].toInt();
}
// Normalize directory paths // Normalize directory paths
pipelines_dir_ = normalizeDir(pipelines_dir_); pipelines_dir_ = normalizeDir(pipelines_dir_);
@@ -88,6 +98,14 @@ PIString ServerConfig::webRoot() const {
return web_root_; return web_root_;
} }
PIString ServerConfig::opencodePath() const {
return opencode_path_;
}
int ServerConfig::opencodeTimeout() const {
return opencode_timeout_;
}
bool ServerConfig::isValid() const { bool ServerConfig::isValid() const {
return valid_; return valid_;
} }
+4
View File
@@ -13,6 +13,8 @@ public:
PIString pipelinesDir() const; PIString pipelinesDir() const;
PIString logsDir() const; PIString logsDir() const;
PIString webRoot() const; PIString webRoot() const;
PIString opencodePath() const;
int opencodeTimeout() const;
bool isValid() const; bool isValid() const;
@@ -23,6 +25,8 @@ private:
PIString pipelines_dir_; PIString pipelines_dir_;
PIString logs_dir_; PIString logs_dir_;
PIString web_root_; PIString web_root_;
PIString opencode_path_;
int opencode_timeout_;
bool valid_; bool valid_;
}; };
+8 -1
View File
@@ -27,6 +27,8 @@ int main(int argc, char * argv[]) {
piCout << "Pipelines dir: " << config.pipelinesDir(); piCout << "Pipelines dir: " << config.pipelinesDir();
piCout << "Logs dir: " << config.logsDir(); piCout << "Logs dir: " << config.logsDir();
piCout << "Web root: " << (config.webRoot().isEmpty() ? "(not set)" : config.webRoot()); piCout << "Web root: " << (config.webRoot().isEmpty() ? "(not set)" : config.webRoot());
piCout << "Opencode path: " << config.opencodePath();
piCout << "Opencode timeout: " << config.opencodeTimeout() << "s";
// Handle signals // Handle signals
PISignals::setSlot([](PISignals::Signal s) { PISignals::setSlot([](PISignals::Signal s) {
@@ -37,7 +39,12 @@ int main(int argc, char * argv[]) {
PISignals::grabSignals(PISignals::Interrupt); PISignals::grabSignals(PISignals::Interrupt);
// Start server // Start server
Server server(static_cast<ushort>(config.port()), config.pipelinesDir(), config.logsDir(), config.webRoot()); Server server(static_cast<ushort>(config.port()),
config.pipelinesDir(),
config.logsDir(),
config.webRoot(),
config.opencodePath(),
config.opencodeTimeout());
server.start(); server.start();
// Wait for exit // Wait for exit
+8 -3
View File
@@ -86,9 +86,14 @@ RunState PipelineRunner::runStateFromJSON(const PIJSON & j) {
return state; return state;
} }
PipelineRunner::PipelineRunner(const PIString & pipelines_dir, const PIString & logs_dir) PipelineRunner::PipelineRunner(const PIString & pipelines_dir,
const PIString & logs_dir,
const PIString & opencode_path,
int opencode_timeout)
: pipelines_dir_(pipelines_dir) : pipelines_dir_(pipelines_dir)
, logs_dir_(logs_dir) {} , logs_dir_(logs_dir)
, opencode_path_(opencode_path)
, opencode_timeout_(opencode_timeout) {}
PipelineRunner::~PipelineRunner() { PipelineRunner::~PipelineRunner() {
for (int i = 0; i < threads_.size(); ++i) { for (int i = 0; i < threads_.size(); ++i) {
@@ -212,7 +217,7 @@ void PipelineRunner::executePipeline(const PIString & run_id, const Pipeline & p
PIStringList args; PIStringList args;
args << "run" << prompt.text << "--title" << prompt.title; args << "run" << prompt.text << "--title" << prompt.title;
ProcessResult result = ProcessExecutor::run("opencode", args, 300, pipeline.working_dir); ProcessResult result = ProcessExecutor::run(opencode_path_, args, opencode_timeout_, pipeline.working_dir);
int rc = result.exitCode; int rc = result.exitCode;
PIString output = result.output; PIString output = result.output;
PIString error = result.error; PIString error = result.error;
+3 -1
View File
@@ -48,7 +48,7 @@ class PipelineRunner: public PIObject {
PIOBJECT(PipelineRunner) PIOBJECT(PipelineRunner)
public: public:
PipelineRunner(const PIString & pipelines_dir, const PIString & logs_dir); PipelineRunner(const PIString & pipelines_dir, const PIString & logs_dir, const PIString & opencode_path, int opencode_timeout);
~PipelineRunner(); ~PipelineRunner();
// Start a pipeline run, returns run_id or empty string on error // Start a pipeline run, returns run_id or empty string on error
@@ -76,6 +76,8 @@ private:
const PIString pipelines_dir_; const PIString pipelines_dir_;
const PIString logs_dir_; const PIString logs_dir_;
const PIString opencode_path_;
const int opencode_timeout_;
PIMutex mutex_; PIMutex mutex_;
PIMap<PIString, RunState> runs_; PIMap<PIString, RunState> runs_;
+9 -2
View File
@@ -31,12 +31,19 @@ const PIMap<PIString, PIString> StaticContentTypes{
}; };
} // namespace } // namespace
Server::Server(ushort port, const PIString & pipelines_dir, const PIString & logs_dir, const PIString & web_root) Server::Server(ushort port,
const PIString & pipelines_dir,
const PIString & logs_dir,
const PIString & web_root,
const PIString & opencode_path,
int opencode_timeout)
: port_(port) : port_(port)
, pipelines_dir_(pipelines_dir) , pipelines_dir_(pipelines_dir)
, logs_dir_(logs_dir) , logs_dir_(logs_dir)
, web_root_(web_root) , web_root_(web_root)
, runner_(pipelines_dir, logs_dir) { , opencode_path_(opencode_path)
, opencode_timeout_(opencode_timeout)
, runner_(pipelines_dir, logs_dir, opencode_path, opencode_timeout) {
httpserver_ = new PIHTTPServer(); httpserver_ = new PIHTTPServer();
// CORS headers // CORS headers
+8 -1
View File
@@ -13,7 +13,12 @@ class Server: public PIObject {
PIOBJECT(Server) PIOBJECT(Server)
public: public:
Server(ushort port, const PIString & pipelines_dir, const PIString & logs_dir, const PIString & web_root = PIString()); Server(ushort port,
const PIString & pipelines_dir,
const PIString & logs_dir,
const PIString & web_root,
const PIString & opencode_path,
int opencode_timeout);
~Server(); ~Server();
bool start(); bool start();
@@ -34,6 +39,8 @@ private:
PIString pipelines_dir_; PIString pipelines_dir_;
PIString logs_dir_; PIString logs_dir_;
PIString web_root_; PIString web_root_;
PIString opencode_path_;
int opencode_timeout_;
PIHTTPServer * httpserver_; PIHTTPServer * httpserver_;
PipelineRunner runner_; PipelineRunner runner_;
}; };
+10 -10
View File
@@ -62,7 +62,7 @@ static Pipeline makeEchoPipeline() {
TEST(RunnerTest, StartRunNotFound) { TEST(RunnerTest, StartRunNotFound) {
setupDirs(); setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir); PipelineRunner runner(testPipelinesDir, testLogsDir, "opencode", 300);
PIString runId = runner.startRun("nonexistent"); PIString runId = runner.startRun("nonexistent");
EXPECT_TRUE(runId.isEmpty()); EXPECT_TRUE(runId.isEmpty());
@@ -72,7 +72,7 @@ TEST(RunnerTest, StartRunNotFound) {
TEST(RunnerTest, StartRunAndGetState) { TEST(RunnerTest, StartRunAndGetState) {
setupDirs(); setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir); PipelineRunner runner(testPipelinesDir, testLogsDir, "opencode", 300);
// Create a pipeline that runs 'echo hello' (uses shell command, not opencode) // Create a pipeline that runs 'echo hello' (uses shell command, not opencode)
Pipeline pl; Pipeline pl;
@@ -108,7 +108,7 @@ TEST(RunnerTest, StartRunAndGetState) {
TEST(RunnerTest, GetStateNotFound) { TEST(RunnerTest, GetStateNotFound) {
setupDirs(); setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir); PipelineRunner runner(testPipelinesDir, testLogsDir, "opencode", 300);
RunState state = runner.getRunState("nonexistent"); RunState state = runner.getRunState("nonexistent");
EXPECT_TRUE(state.run_id.isEmpty()); EXPECT_TRUE(state.run_id.isEmpty());
@@ -118,7 +118,7 @@ TEST(RunnerTest, GetStateNotFound) {
TEST(RunnerTest, IsRunActive) { TEST(RunnerTest, IsRunActive) {
setupDirs(); setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir); PipelineRunner runner(testPipelinesDir, testLogsDir, "opencode", 300);
Pipeline pl; Pipeline pl;
pl.id = "active-test"; pl.id = "active-test";
@@ -154,7 +154,7 @@ TEST(RunnerTest, RunStatusToString) {
TEST(PersistenceTest, SaveLoadRoundTrip) { TEST(PersistenceTest, SaveLoadRoundTrip) {
setupDirs(); setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir); PipelineRunner runner(testPipelinesDir, testLogsDir, "opencode", 300);
// Test loadRunState on non-existent file // Test loadRunState on non-existent file
RunState empty = runner.loadRunState("nonexistent"); RunState empty = runner.loadRunState("nonexistent");
@@ -243,7 +243,7 @@ TEST(PersistenceTest, RecoverRuns) {
PIFile::writeAll(completedStatePath, j2.toJSON(PIJSON::Tree).toUTF8()); PIFile::writeAll(completedStatePath, j2.toJSON(PIJSON::Tree).toUTF8());
// New runner should recover only the running state // New runner should recover only the running state
PipelineRunner runner(testPipelinesDir, testLogsDir); PipelineRunner runner(testPipelinesDir, testLogsDir, "opencode", 300);
runner.recoverRuns(); runner.recoverRuns();
RunState recovered = runner.getRunState(runId); RunState recovered = runner.getRunState(runId);
@@ -260,7 +260,7 @@ TEST(PersistenceTest, RecoverRuns) {
TEST(PersistenceTest, CompletedRunStatePersisted) { TEST(PersistenceTest, CompletedRunStatePersisted) {
setupDirs(); setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir); PipelineRunner runner(testPipelinesDir, testLogsDir, "opencode", 300);
Pipeline pl; Pipeline pl;
pl.id = "persist-test"; pl.id = "persist-test";
@@ -317,7 +317,7 @@ static Pipeline makeFakePipeline(const PIString & id, const PIString & name, int
TEST(FakeRunnerTest, FullPipelineCompletes) { TEST(FakeRunnerTest, FullPipelineCompletes) {
setupDirs(); setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir); PipelineRunner runner(testPipelinesDir, testLogsDir, "opencode", 300);
Pipeline pl = makeFakePipeline("fake-full", "Fake Full Pipeline", 3); Pipeline pl = makeFakePipeline("fake-full", "Fake Full Pipeline", 3);
savePipeline(testPipelinesDir, pl); savePipeline(testPipelinesDir, pl);
@@ -357,7 +357,7 @@ TEST(FakeRunnerTest, FullPipelineCompletes) {
TEST(FakeRunnerTest, RunStatusTransitions) { TEST(FakeRunnerTest, RunStatusTransitions) {
setupDirs(); setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir); PipelineRunner runner(testPipelinesDir, testLogsDir, "opencode", 300);
Pipeline pl = makeFakePipeline("fake-transition", "Fake Transition Pipeline", 2); Pipeline pl = makeFakePipeline("fake-transition", "Fake Transition Pipeline", 2);
savePipeline(testPipelinesDir, pl); savePipeline(testPipelinesDir, pl);
@@ -397,7 +397,7 @@ TEST(FakeRunnerTest, RunStatusTransitions) {
TEST(FakeRunnerTest, StepOutputContainsFakeMarker) { TEST(FakeRunnerTest, StepOutputContainsFakeMarker) {
setupDirs(); setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir); PipelineRunner runner(testPipelinesDir, testLogsDir, "opencode", 300);
Pipeline pl = makeFakePipeline("fake-output", "Fake Output Pipeline", 2); Pipeline pl = makeFakePipeline("fake-output", "Fake Output Pipeline", 2);
savePipeline(testPipelinesDir, pl); savePipeline(testPipelinesDir, pl);