End-to-end: ``python -m orchestrator --target-pid <pid> --duration N`` now
writes a complete episode directory matching docs/data-model.md, with phase
labels, events, and a 10 Hz host /proc telemetry stream. No VM yet — pid is
arbitrary so we can validate the loop against e.g. ``sleep 5`` while the lab
side comes up.
collectors/proc_qemu.py — parses /proc/<pid>/{stat,io,status} (handles parens
in comm), single-shot collect_once(), and a stop-event-driven run_loop()
that ticks at a fixed cadence and exits when the pid disappears. Tagged
``available_in_deployment: false`` per the threat-model doc.
orchestrator/episode.py — EpisodeRunner: creates data/episodes/<ulid>/,
atomic meta.json, events.jsonl + labels.jsonl writers, drives the collector
in a thread for duration_s, writes done.marker last so the shipper never
sees a half-finished episode.
orchestrator/ulid.py — tiny 26-char Crockford-base32 ULID generator.
Time-sortable, no third-party dep.
orchestrator/__main__.py — CLI entry point.
Tests (15 new, 28 total green):
- proc_qemu: real-ish stat with parens-in-comm, missing /proc/<pid>/io,
missing pid, run_loop cadence, run_loop terminates when pid disappears.
- episode: full directory shape against os.getpid(), id override,
done.marker written after meta.json finalize.
- ulid: length+alphabet, 2000-burst uniqueness, time-sortability.
Smoke-tested against ``sleep 10``: 16 rows over 1.5s at 100ms cadence,
monotonic clock, RSS stable at ~3.5 MiB as expected for an idle sleep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
21 lines
592 B
Python
21 lines
592 B
Python
"""Tiny ULID generator. 26-char Crockford base32, time-sortable."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import time
|
|
|
|
|
|
_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
|
|
|
|
def new_ulid(now_ms: int | None = None) -> str:
|
|
"""Return a fresh ULID. ``now_ms`` is overridable for tests."""
|
|
ms = (now_ms if now_ms is not None else int(time.time() * 1000)) & ((1 << 48) - 1)
|
|
raw = ms.to_bytes(6, "big") + os.urandom(10)
|
|
n = int.from_bytes(raw, "big")
|
|
out = []
|
|
for _ in range(26):
|
|
out.append(_CROCKFORD[n & 31])
|
|
n >>= 5
|
|
return "".join(reversed(out))
|