From 7e51bee2211030ec15c9644655db3609ba14181e Mon Sep 17 00:00:00 2001 From: Sofiia Syuy Date: Tue, 31 Mar 2026 13:20:09 +0300 Subject: [PATCH] HW4 Beagle: annagorbatsevich-o1eg0-sonyasyuy --- .../gorbatyuk_gorbatsevich_syuy/Dockerfile | 35 +++ 09-fuzz/gorbatyuk_gorbatsevich_syuy/README.md | 135 +++++++++ .../ci-app/daemon.js | 280 ++++++++++++++++++ .../ci-app/index.html | 133 +++++++++ 09-fuzz/gorbatyuk_gorbatsevich_syuy/start.sh | 50 ++++ 5 files changed, 633 insertions(+) create mode 100644 09-fuzz/gorbatyuk_gorbatsevich_syuy/Dockerfile create mode 100644 09-fuzz/gorbatyuk_gorbatsevich_syuy/README.md create mode 100644 09-fuzz/gorbatyuk_gorbatsevich_syuy/ci-app/daemon.js create mode 100644 09-fuzz/gorbatyuk_gorbatsevich_syuy/ci-app/index.html create mode 100755 09-fuzz/gorbatyuk_gorbatsevich_syuy/start.sh diff --git a/09-fuzz/gorbatyuk_gorbatsevich_syuy/Dockerfile b/09-fuzz/gorbatyuk_gorbatsevich_syuy/Dockerfile new file mode 100644 index 00000000..7cc73d58 --- /dev/null +++ b/09-fuzz/gorbatyuk_gorbatsevich_syuy/Dockerfile @@ -0,0 +1,35 @@ +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y \ + gcc-14 \ + cmake \ + make \ + pkg-config \ + libsodium-dev \ + librocksdb-dev \ + liblz4-dev \ + libcurl4-gnutls-dev \ + libjavascriptcoregtk-4.1-dev \ + libgtest-dev \ + libbenchmark-dev \ + curl \ + nodejs \ + git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src +RUN git clone https://github.com/gritzko/librdx.git . + +RUN sed -i '/POLLtest/d' abc/test/CMakeLists.txt + +RUN mkdir build && cd build \ + && CC=gcc-14 cmake -DCMAKE_BUILD_TYPE=Debug -DWITH_INET=ON .. \ + && make -j$(nproc) be-srv be-cli + +COPY ci-app/ /src/ci-app/ +COPY start.sh /src/start.sh +RUN chmod +x /src/start.sh + +EXPOSE 8800 8801 + +CMD ["/src/start.sh"] diff --git a/09-fuzz/gorbatyuk_gorbatsevich_syuy/README.md b/09-fuzz/gorbatyuk_gorbatsevich_syuy/README.md new file mode 100644 index 00000000..bf48e882 --- /dev/null +++ b/09-fuzz/gorbatyuk_gorbatsevich_syuy/README.md @@ -0,0 +1,135 @@ +# Beagle CI + +Realtime build/CI daemon for [Beagle](https://github.com/gritzko/librdx) — a decentralized source code management system. + +## What it does + +Beagle CI watches a Beagle project for code changes and automatically runs build and test commands. Results are displayed in a web dashboard. + +### How it works + +Two processes run inside a Docker container: + +- **be-srv** (port 8800) — Beagle's HTTP server. Reads files from RocksDB and serves them over HTTP. Also accepts POST requests to write data back. The dashboard (`index.html`) and build results (`builds.json`) are stored here. + +- **daemon.js** — the CI daemon. Every 3 seconds it: + 1. Fetches the file listing from be-srv + 2. Downloads each file and computes a content hash + 3. Compares hashes with the previous poll + 4. If anything changed — runs the build and test commands from `ci.json` + 5. POSTs build results back to be-srv (stored in Beagle) + 6. Also keeps a local fallback on port 8801 + +The dashboard and all data live inside Beagle and are served by be-srv. + +### Developer workflow + +```mermaid +sequenceDiagram + participant Dev as Developer + participant BE as Beagle (RocksDB) + participant CI as CI daemon + participant BR as Browser + + Dev->>BE: be post main.c + loop every 3s + CI->>BE: GET / (file list) + BE-->>CI: content changed + end + CI->>CI: gcc && test + CI->>BE: POST /builds.json + BR->>BE: GET /index.html + BE-->>BR: html + js + BR->>BE: GET /builds.json + BE-->>BR: build results +``` + +## Setup + +### Build and run + +```bash +docker build -t beagle . +docker run -p 8800:8800 -p 8801:8801 beagle +``` + +This will: +1. Build librdx (be-srv, be-cli) from source +2. Create a demo C project with `ci.json` +3. Import it into Beagle (including `index.html`) +4. Start be-srv on port 8800 +5. Start the CI daemon + +Open **http://localhost:8800/index.html** in a browser — the dashboard is served directly from Beagle. + +For manual setup inside the container (`docker run -it ... beagle bash`): + +```bash +/src/start.sh +``` + +## Configuration + +Place a `ci.json` in the project root: + +```json +{ + "name": "myproject", + "build": "gcc-14 -o main main.c", + "test": "./main && echo ok" +} +``` + +| Field | Description | +|---------|------------------------------------| +| `name` | Project name shown in the dashboard | +| `build` | Shell command to build the project | +| `test` | Shell command to run tests (runs only if build passes) | + +### Environment variables + +| Variable | Default | Description | +|-------------|---------|--------------------------| +| `CI_POLL_MS` | 3000 | Poll interval in ms | +| `CI_PORT` | 8801 | Dashboard HTTP port | +| `BE_BIN` | /src/build/be/be | Path to `be` binary | + +### CLI arguments + +``` +node daemon.js [be-srv-url] [project-dir] +``` + +- `be-srv-url` — default `http://127.0.0.1:8800` +- `project-dir` — worktree with `.be` file, default `/tmp/testproject` + +## Making changes + +To push code changes into Beagle, stop be-srv first (RocksDB allows only one writer at file-system level): + +```bash +pkill be-srv +echo 'int main() { return 1; }' > main.c +/src/build/be/be post main.c +/src/build/be/be-srv 8800 & +``` + +The CI daemon will detect the changed content on its next poll and trigger a new build. Results are automatically POSTed back to be-srv. + +## Known limitations + +- **RocksDB single-writer lock**: `be post` CLI and `be-srv` cannot run simultaneously. Stop the server before using `be post`. +- **Polling-based**: changes are detected by content hashing every 3 seconds, not by push notifications. +- **MIME types**: be-srv may serve HTML as `text/plain` — if the browser shows raw HTML, use the fallback dashboard on port 8801. + +## Project structure + +``` +beagle/ +├── Dockerfile # Ubuntu 24.04 + gcc-14 + deps +├── README.md +├── start.sh # Entrypoint: init + be-srv + daemon +└── ci-app/ + ├── daemon.js # CI daemon (Node.js) + └── index.html # Web dashboard (stored in Beagle) +``` diff --git a/09-fuzz/gorbatyuk_gorbatsevich_syuy/ci-app/daemon.js b/09-fuzz/gorbatyuk_gorbatsevich_syuy/ci-app/daemon.js new file mode 100644 index 00000000..6bdfc6ec --- /dev/null +++ b/09-fuzz/gorbatyuk_gorbatsevich_syuy/ci-app/daemon.js @@ -0,0 +1,280 @@ +#!/usr/bin/env node +// +// Beagle CI Daemon +// +// Polls be-srv for file changes, runs build/test commands, +// serves results via its own HTTP on CI_PORT. +// +// Usage: node daemon.js [be-srv-url] [project-dir] +// + +const http = require("http"); +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const BE_URL = process.argv[2] || "http://127.0.0.1:8800"; +const PROJECT = process.argv[3] || "/tmp/testproject"; +const POLL_INTERVAL = parseInt(process.env.CI_POLL_MS || "3000", 10); +const CI_PORT = parseInt(process.env.CI_PORT || "8801", 10); +const CI_DIR = path.join(PROJECT, "ci"); + +let prevFiles = {}; +let builds = []; +let buildNo = 0; + +// HTTP helpers + +function httpGet(urlStr) { + return new Promise((resolve, reject) => { + http.get(urlStr, (res) => { + let data = ""; + res.on("data", (c) => (data += c)); + res.on("end", () => resolve({ status: res.statusCode, body: data })); + }).on("error", reject); + }); +} + +function httpPost(urlStr, body) { + return new Promise((resolve, reject) => { + const url = new URL(urlStr); + const buf = Buffer.from(body, "utf8"); + const opts = { + hostname: url.hostname, + port: url.port, + path: url.pathname, + method: "POST", + headers: { "Content-Length": buf.length }, + }; + const req = http.request(opts, (res) => { + let data = ""; + res.on("data", (c) => (data += c)); + res.on("end", () => resolve({ status: res.statusCode, body: data })); + }); + req.on("error", reject); + req.write(buf); + req.end(); + }); +} + +// Simple string hash (djb2) + +function hash(str) { + let h = 5381; + for (let i = 0; i < str.length; i++) { + h = ((h << 5) + h + str.charCodeAt(i)) & 0xffffffff; + } + return h.toString(16); +} + +// Fetch file listing from be-srv + +async function fetchFileList() { + const res = await httpGet(BE_URL + "/"); + if (res.status !== 200) return []; + return res.body + .split("\n") + .map((l) => l.trim()) + .filter((l) => l && !l.startsWith("ci/") && l !== ".be" + && l !== "ci.json" && l !== "index.html" && l !== "builds.json"); +} + +// Fetch file content + +async function fetchFile(name) { + const res = await httpGet(BE_URL + "/" + encodeURIComponent(name)); + if (res.status !== 200) return null; + return res.body; +} + +// Fetch CI config from be-srv + +async function fetchConfig() { + const defaults = { + name: "default", + build: "echo 'no ci.json found'", + test: "echo 'no tests'", + }; + try { + const res = await httpGet(BE_URL + "/ci.json"); + if (res.status === 200) { + return { ...defaults, ...JSON.parse(res.body) }; + } + } catch {} + return defaults; +} + +// Detect changes + +async function detectChanges() { + const files = await fetchFileList(); + const currFiles = {}; + const changed = []; + + for (const f of files) { + const content = await fetchFile(f); + if (content === null) continue; + const h = hash(content); + currFiles[f] = h; + if (prevFiles[f] !== h) { + changed.push(f); + } + } + + for (const f of Object.keys(prevFiles)) { + if (!(f in currFiles)) changed.push(f); + } + + prevFiles = currFiles; + return changed; +} + +// Run a shell command + +function runCommand(cmd, cwd) { + const start = Date.now(); + try { + const out = execSync(cmd, { + cwd, + timeout: 120000, + encoding: "utf8", + stdio: ["pipe", "pipe", "pipe"], + }); + return { ok: true, ms: Date.now() - start, log: out }; + } catch (e) { + return { + ok: false, + ms: Date.now() - start, + log: (e.stdout || "") + "\n" + (e.stderr || ""), + code: e.status, + }; + } +} + +// Run build + +async function runBuild(changedFiles) { + const config = await fetchConfig(); + buildNo++; + const ts = new Date().toISOString(); + + console.log(`\n=== Build #${buildNo} at ${ts} ===`); + console.log(` changed: ${changedFiles.join(", ")}`); + + // build step + console.log(` build: ${config.build}`); + const buildResult = runCommand(config.build, PROJECT); + console.log(` build ${buildResult.ok ? "OK" : "FAIL"} (${buildResult.ms}ms)`); + + // test step (only if build passed) + let testResult = { ok: false, ms: 0, log: "skipped (build failed)" }; + if (buildResult.ok) { + console.log(` test: ${config.test}`); + testResult = runCommand(config.test, PROJECT); + console.log(` test ${testResult.ok ? "OK" : "FAIL"} (${testResult.ms}ms)`); + } + + const result = { + id: buildNo, + ts, + files: changedFiles, + project: config.name, + build: { + cmd: config.build, + ok: buildResult.ok, + ms: buildResult.ms, + log: buildResult.log.slice(-4096), + }, + test: { + cmd: config.test, + ok: testResult.ok, + ms: testResult.ms, + log: testResult.log.slice(-4096), + }, + status: buildResult.ok && testResult.ok ? "pass" : "fail", + }; + + builds.push(result); + if (builds.length > 50) builds = builds.slice(-50); + + const json = JSON.stringify(builds, null, 2); + + // persist to disk (fallback) + fs.mkdirSync(CI_DIR, { recursive: true }); + fs.writeFileSync(path.join(CI_DIR, "builds.json"), json); + + // persist to beagle via HTTP POST + try { + const res = await httpPost(BE_URL + "/builds.json", json); + if (res.status === 200) { + console.log(" posted results to beagle"); + } else { + console.error(" beagle POST returned", res.status); + } + } catch (e) { + console.error(" beagle POST failed:", e.message); + } + + return result; +} + +// Poll loop + +async function poll() { + try { + const changed = await detectChanges(); + if (changed.length > 0) { + await runBuild(changed); + } + } catch (e) { + console.error("poll error:", e.message); + } + setTimeout(poll, POLL_INTERVAL); +} + +// CI HTTP server (serves index.html + builds.json) + +const INDEX_HTML = fs.readFileSync( + path.join(__dirname, "index.html"), + "utf8" +); + +const ciServer = http.createServer((req, res) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + + if (req.url === "/" || req.url === "/index.html") { + res.writeHead(200, { "Content-Type": "text/html" }); + res.end(INDEX_HTML); + return; + } + + if (req.url === "/builds.json") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(builds, null, 2)); + return; + } + + res.writeHead(404); + res.end("not found"); +}); + +// Start + +console.log("Beagle CI Daemon"); +console.log(` be-srv: ${BE_URL}`); +console.log(` project: ${PROJECT}`); +console.log(` poll: ${POLL_INTERVAL}ms`); +console.log(` ci web: http://0.0.0.0:${CI_PORT}`); + +// load previous builds +const bfile = path.join(CI_DIR, "builds.json"); +try { + builds = JSON.parse(fs.readFileSync(bfile, "utf8")); + buildNo = builds.length > 0 ? builds[builds.length - 1].id : 0; + console.log(` loaded ${builds.length} previous builds`); +} catch {} + +ciServer.listen(CI_PORT, "0.0.0.0", () => { + console.log("waiting for changes...\n"); + poll(); +}); diff --git a/09-fuzz/gorbatyuk_gorbatsevich_syuy/ci-app/index.html b/09-fuzz/gorbatyuk_gorbatsevich_syuy/ci-app/index.html new file mode 100644 index 00000000..6f98cb41 --- /dev/null +++ b/09-fuzz/gorbatyuk_gorbatsevich_syuy/ci-app/index.html @@ -0,0 +1,133 @@ + + + + + +Beagle CI + + + + +

Beagle CI

+

Polling for updates...

+ +
+
0
Builds
+
0
Passed
+
0
Failed
+
-
Last build
+
+ +
+
No builds yet. Waiting for changes...
+
+ + + + diff --git a/09-fuzz/gorbatyuk_gorbatsevich_syuy/start.sh b/09-fuzz/gorbatyuk_gorbatsevich_syuy/start.sh new file mode 100755 index 00000000..c898175f --- /dev/null +++ b/09-fuzz/gorbatyuk_gorbatsevich_syuy/start.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# +# Starts beagle CI: init project, be-srv, daemon +# + +set -e + +BE=/src/build/be/be +BE_SRV=/src/build/be/be-srv +PROJECT=${1:-/tmp/testproject} + +# create demo project + +mkdir -p "$PROJECT" +cd "$PROJECT" + +if [ ! -f main.c ]; then + cat > main.c << 'SRC' +#include +int main() { + printf("hello beagle\n"); + return 0; +} +SRC +fi + +if [ ! -f ci.json ]; then + cat > ci.json << 'SRC' +{"name": "testproject", "build": "gcc-14 -o main main.c", "test": "./main && echo ok"} +SRC +fi + +# copy dashboard into the project so it gets stored in beagle +cp /src/ci-app/index.html "$PROJECT/index.html" + +# import everything into beagle + +$BE post //testrepo/testproject +echo "project imported into beagle" + +# start be-srv + +$BE_SRV 8800 & +sleep 1 +echo "be-srv on :8800" + +# start CI daemon + +echo "starting CI daemon on :8801" +exec node /src/ci-app/daemon.js http://127.0.0.1:8800 "$PROJECT"