Compare commits

...
2 Commits
Author SHA1 Message Date
proitlab cb9be7a402 Update Session 2026-08-12 16:15:41 +07:00
proitlab 9ea6820a37 Preserve STATE_FILE after file-input recount; delete on start
File sources now keep STATE_FILE on graceful shutdown (EOF or SIGTERM):
the final batch snapshot is written instead of deleted, so the dashboard's
/api/current-batch still shows the recount result after the process exits.
On startup, STATE_FILE is cleared unless a live instance is detected via a
/proc cmdline scan (rtsp/http source in argv or config). Live sources keep
the old resume-at-start / delete-on-finish behavior.

SPEC: V19, T21, B2. AGENTS: STATE_FILE lifecycle section.
2026-08-12 15:05:48 +07:00
7 changed files with 5802 additions and 7 deletions

No files matched your search

+5707 -5
View File
File diff suppressed because it is too large. Load diff
+15
View File
@@ -45,6 +45,21 @@ Place a `.reset` file in `SHM_DIR` (default `/dev/shm/bytetrack-counter`) to tri
touch /dev/shm/bytetrack-counter/.reset
```
## STATE_FILE lifecycle (file sources)
For local file input, STATE_FILE persists the recount result so the dashboard's
`/api/current-batch` still shows the final batch after the process exits:
- **Start**: STATE_FILE is deleted unless a live instance is running — detected by
a `/proc/*/cmdline` scan for another `bytetrack-counter` whose source is
`rtsp://`/`http://` (argv or `SOURCE=` in its config). If live is active, the
file is kept and a `[INFO] Live instance active — keeping STATE_FILE` line is
logged. Stop the live service before recounting so stale recount state is cleared.
- **Finish**: on graceful shutdown (`end_batch("shutdown")` — EOF or SIGTERM) the
final batch snapshot is written to STATE_FILE (in both the persist and the
discard-below-minimum paths) instead of being deleted.
- Live sources are unchanged: resume at start, delete on finish.
## Shared memory
`SHM_DIR` (`/dev/shm/bytetrack-counter` by default) stores:
+3
View File
@@ -55,6 +55,7 @@ V15: RECORD_CRF (lower = better, -1 unset) & RECORD_PRESET → OPENCV_FFMPEG_WRI
V16: pause_on_first_frame (env/--pause-on-first-frame, file source only; live ignored w/ warn): process frame 0 → idle-loop, re-write annotated frame to LIVE_STREAM_FRAME_PATH, wait for SHM_DIR/.continue → delete → resume
V17: file sources (non-live) record from frame 0: SPECULATING never discarded, any active recording saved on batch close and shutdown → output first frame = source frame 0 (no RECORD_DELAY offset)
V18: file sources: cap opened w/ CAP_PROP_POS_FRAMES=0 (frame-0 seek); CAP_PROP_BUFFERSIZE=1 live-only; ∀ live|file → first decoded frame == source frame 0 (no build-buffer offset into file)
V19: file sources: STATE_FILE deleted @ startup IF no live instance active (bytetrack-counter w/ rtsp|http src in argv|config → /proc scan); preserved @ graceful shutdown (end_batch "shutdown" → final snapshot written, ⊥ deleted). Live: resume @ start, delete @ finish (unchanged)
```
## §T — Tasks
@@ -81,6 +82,7 @@ T17|~?|Python DEBUG_TRACKING equivalent (per-frame score/track dump)|-
T18|x|CLI --source/-s/--source= override of SOURCE (rtsp|http|local mp4; file = single pass)|I.cmd
T19|x|Pause-on-first-frame for file source: process frame 0, hold annotated frame as live view, wait for SHM_DIR/.continue, resume. Env PAUSE_ON_FIRST_FRAME + --pause-on-first-frame; stale .continue cleared @ startup; live source ignored w/ warning.|V16,V18
T20|x|File-source capture: POS_FRAMES=0 seek @ open (frame 0 !), BUFFERSIZE live-only; log pos_frames/pos_msec/frame_count/fps on first decoded frame|V18
T21|x|File-source STATE_FILE lifecycle: startup delete (guard: live-instance /proc scan → skip delete if active), preserve final snapshot @ graceful shutdown (end_batch "shutdown")|V19
```
## §B — Bugs
@@ -88,4 +90,5 @@ T20|x|File-source capture: POS_FRAMES=0 seek @ open (frame 0 !), BUFFERSIZE live
```
id|date|cause|fix
B1|2026-08-10|file source CAP_PROP_BUFFERSIZE=1 @ open → OpenCV FFMPEG build buffer offset → first decoded frame ≠ source frame 0 (paused view showed ~11s-later clock 21:07:39 vs mp4 frame0 21:07:28)|V18
B2|2026-08-12|file input graceful finish deleted STATE_FILE (save_state w/ null current_state_) → recount result invisible to /api/current-batch after exit; stale file resumed next run|V19
```
Binary file not shown.
+12 -2
View File
@@ -332,7 +332,12 @@ bool BatchStore::end_batch_locked(const std::string& closed_by) {
if (count < min_object_per_batch_ || duration_sec < min_duration_per_batch_) {
current_state_ = nullptr;
save_state();
if (preserve_state_file_ && closed_by == "shutdown") {
std::ofstream f(state_file_);
f << state.dump(2);
} else {
save_state();
}
batch_timer_running_ = false;
if (batch_closed_cb_) batch_closed_cb_(batch_number, counting_date);
return false;
@@ -347,7 +352,12 @@ bool BatchStore::end_batch_locked(const std::string& closed_by) {
}
current_state_ = nullptr;
save_state();
if (preserve_state_file_ && closed_by == "shutdown") {
std::ofstream f(state_file_);
f << state.dump(2);
} else {
save_state();
}
batch_timer_running_ = false;
if (batch_closed_cb_) batch_closed_cb_(batch_number, counting_date);
return true;
+2
View File
@@ -32,6 +32,7 @@ public:
int display_total();
void set_batch_closed_callback(std::function<void(int, const std::string&)> cb);
void set_preserve_state_file(bool v) { preserve_state_file_ = v; }
void reset();
bool check_cutoff_reset();
@@ -85,4 +86,5 @@ private:
json current_state_;
json previous_state_;
bool preserve_state_file_ = false;
};
+63
View File
@@ -14,11 +14,16 @@
#include <iostream>
#include <chrono>
#include <thread>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <deque>
#include <ctime>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <dirent.h>
static std::atomic<bool> shutdown_requested{false};
@@ -35,6 +40,53 @@ static std::string now_str() {
return ss.str();
}
static bool live_instance_running() {
pid_t self_pid = getpid();
DIR* dir = opendir("/proc");
if (!dir) return false;
struct dirent* ent;
while ((ent = readdir(dir)) != nullptr) {
if (ent->d_name[0] < '0' || ent->d_name[0] > '9') continue;
if (std::atoi(ent->d_name) == static_cast<int>(self_pid)) continue;
std::string cmdline_path = std::string("/proc/") + ent->d_name + "/cmdline";
std::ifstream f(cmdline_path);
if (!f) continue;
std::vector<std::string> args;
std::string tok;
while (std::getline(f, tok, '\0')) {
if (!tok.empty()) args.push_back(tok);
}
if (args.empty()) continue;
std::string base = args[0];
auto slash = base.find_last_of("/\\");
if (slash != std::string::npos) base = base.substr(slash + 1);
if (base != "bytetrack-counter") continue;
for (const auto& a : args) {
if (a.rfind("rtsp://", 0) == 0 || a.rfind("http://", 0) == 0) {
closedir(dir);
return true;
}
}
std::string cfg = args.size() > 1 ? args[1] : "";
if (!cfg.empty() && cfg[0] != '-' && cfg.find("rtsp://") == std::string::npos &&
cfg.find("http://") == std::string::npos) {
std::ifstream cf(cfg);
std::string line;
while (std::getline(cf, line)) {
if (line.rfind("SOURCE=", 0) == 0) {
std::string v = line.substr(7);
if (v.rfind("rtsp://", 0) == 0 || v.rfind("http://", 0) == 0) {
closedir(dir);
return true;
}
}
}
}
}
closedir(dir);
return false;
}
static cv::VideoCapture open_capture(const std::string& source) {
bool is_live = source.substr(0, 7) == "rtsp://" || source.substr(0, 7) == "http://";
if (is_live) {
@@ -206,6 +258,16 @@ int main(int argc, char* argv[]) {
std::filesystem::remove(g_config.shm_dir + "/.continue", ce);
}
if (!is_live && !live_instance_running()) {
std::error_code ec;
std::filesystem::remove(g_config.state_file, ec);
std::cout << "[INFO] File source — cleared STATE_FILE "
<< g_config.state_file << std::endl;
} else if (!is_live) {
std::cout << "[INFO] Live instance active — keeping STATE_FILE "
<< g_config.state_file << std::endl;
}
int class_ayam = g_config.ayam_class_id;
int class_talenan = g_config.talenan_class_id;
@@ -221,6 +283,7 @@ int main(int argc, char* argv[]) {
g_config.min_object_per_batch,
g_config.min_duration_per_batch,
logger);
store.set_preserve_state_file(!is_live);
store.start_cutoff_watcher();
if (startup_reset) store.reset();