chore: monorepo inicial con arje + minga + yahweh absorbidos

Workspace en 4 ejes (core/modules/apps/shared):

- core/: 24 crates de arje (Init systemd-compatible: ente-card, ente-zero,
  ente-kernel, ente-bus, ente-cas, ente-soma, ente-wasm, ente-snapshot,
  ente-brain, ente-echo, ente-policy-provider, + 12 crates *-compat)
- modules/semantic_dht/: 5 crates de minga (minga-core con AST/CAS/MST,
  minga-p2p con libp2p Kad, minga-store, minga-vfs, minga-cli)
- modules/ui_engine/: 11 crates de yahweh (libs/{core,theme,bus,providers},
  widgets/{tree,splitter,tabs,tiled,container_core,text_input})
- apps/: 5 crates de yahweh (file_explorer, database_explorer, text_viewer,
  image_viewer, yahweh-shell)
- shared_wit/protocol.wit: handshake/lifecycle inicial

Cargo.toml unificado: thiserror bumped a 2 (transparente para arje), tokio
"full", paths intra-workspace de yahweh redirigidos a su nueva ubicación.

cargo check --workspace: 0 errores, 17 warnings (dead code preexistente).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 04:45:44 +00:00
commit 53dbdf0f1d
176 changed files with 34845 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "ente-timer-compat"
version = "0.0.1"
edition.workspace = true
license.workspace = true
publish.workspace = true
[[bin]]
name = "ente-timer-compat"
path = "src/main.rs"
[dependencies]
ente-card = { path = "../ente-card" }
ente-bus = { path = "../ente-bus" }
serde = { workspace = true }
serde_json = { workspace = true }
ulid = { workspace = true }
anyhow = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
+228
View File
@@ -0,0 +1,228 @@
//! ente-timer-compat: scheduler estilo cron + systemd .timer.
//!
//! Lee config en JSON desde `/etc/ente/timers.json` (override env
//! `ENTE_TIMERS_FILE`):
//!
//! ```json
//! [
//! {
//! "name": "daily-cleanup",
//! "schedule": "0 4 * * *",
//! "card": {
//! "id": "01KQ_TIMER_CLEANUP_0000000",
//! "label": "daily-cleanup-job",
//! "schema_version": 1,
//! "soma": {"namespaces": {}, "rlimits": {}, "cgroup": {"path": ""}},
//! "payload": {"Native": {"exec": "/usr/local/bin/cleanup", "argv": [], "envp": []}},
//! "supervision": "OneShot",
//! "provides": [], "requires": []
//! }
//! }
//! ]
//! ```
//!
//! Schedule: cron 5-fields `min hour dom mon dow` (DOM/DOW como en cron
//! tradicional). `*` y `*/N` soportados, listas no.
//!
//! Cuando un timer dispara, se envía un `BusRequest::Invoke` al bus interno
//! con la cap "TimerFire" + payload = serialized Card. Un Ente que provea
//! esa cap (futuro: ente-zero internamente) hace el spawn.
//!
//! Para el demo: log "FIRE" cada vez que el schedule matchea, sin spawn real
//! (requiere mover SpawnRequest al protocolo del bus, fuera de scope).
use ente_bus::{BusClient, BusRequest, BusResponse};
use ente_card::Capability;
use serde::Deserialize;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::signal::unix::{signal, SignalKind};
use tracing::{info, warn};
use tracing_subscriber::EnvFilter;
#[derive(Debug, Clone, Deserialize)]
struct TimerConfig {
name: String,
/// Cron 5-field: `min hour dom mon dow`. `*`, `N`, `*/N` soportados.
schedule: String,
/// Card a disparar. Por ahora se loguea — futuro: SpawnRequest via bus.
#[serde(default)]
card: Option<serde_json::Value>,
}
#[derive(Debug)]
struct Cron {
min: CronField,
hour: CronField,
dom: CronField,
mon: CronField,
dow: CronField,
}
#[derive(Debug)]
enum CronField {
Any,
Exact(u32),
Step(u32), // */N
}
impl CronField {
fn parse(s: &str) -> Option<Self> {
if s == "*" { return Some(CronField::Any); }
if let Some(n) = s.strip_prefix("*/") {
return n.parse().ok().map(CronField::Step);
}
s.parse().ok().map(CronField::Exact)
}
fn matches(&self, v: u32) -> bool {
match self {
CronField::Any => true,
CronField::Exact(n) => *n == v,
CronField::Step(n) if *n > 0 => v % n == 0,
_ => false,
}
}
}
impl Cron {
fn parse(s: &str) -> Option<Self> {
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() != 5 { return None; }
Some(Self {
min: CronField::parse(parts[0])?,
hour: CronField::parse(parts[1])?,
dom: CronField::parse(parts[2])?,
mon: CronField::parse(parts[3])?,
dow: CronField::parse(parts[4])?,
})
}
fn matches(&self, t: &TimeBits) -> bool {
self.min.matches(t.min)
&& self.hour.matches(t.hour)
&& self.dom.matches(t.dom)
&& self.mon.matches(t.mon)
&& self.dow.matches(t.dow)
}
}
#[derive(Debug)]
struct TimeBits {
min: u32, hour: u32, dom: u32, mon: u32, dow: u32,
}
/// Decompose epoch_secs en componentes UTC. Algoritmo simple (Howard Hinnant).
fn time_bits_utc(epoch_secs: i64) -> TimeBits {
let secs_per_day = 86400i64;
let days_since_epoch = epoch_secs.div_euclid(secs_per_day);
let secs_in_day = epoch_secs.rem_euclid(secs_per_day);
let hour = (secs_in_day / 3600) as u32;
let min = ((secs_in_day % 3600) / 60) as u32;
// dow: 1970-01-01 fue jueves (4); cron usa 0-6 con 0=domingo.
let dow = ((days_since_epoch + 4).rem_euclid(7)) as u32;
// Conversión a y/m/d (Howard Hinnant Civil from days).
let z = days_since_epoch + 719_468;
let era = z.div_euclid(146_097);
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
let _y = y + if m <= 2 { 1 } else { 0 };
TimeBits { min, hour, dom: d, mon: m, dow }
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
init_tracing();
info!("ente-timer-compat: arrancando");
announce_to_fractal().await;
let timers = load_timers();
info!(count = timers.len(), "timers cargados");
for t in &timers {
info!(name = %t.name, schedule = %t.schedule, "timer activo");
}
let parsed: Vec<(TimerConfig, Cron)> = timers.into_iter()
.filter_map(|t| {
let cron = Cron::parse(&t.schedule)?;
Some((t, cron))
})
.collect();
let mut term = signal(SignalKind::terminate())?;
let mut int_ = signal(SignalKind::interrupt())?;
let mut tick = tokio::time::interval(std::time::Duration::from_secs(60));
// Alinear al próximo minuto entero.
let now_ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
let to_next_min = 60_000 - (now_ms % 60_000);
tokio::time::sleep(std::time::Duration::from_millis(to_next_min)).await;
tick.tick().await; // descartar primer tick post-alignment
info!("scheduler activo (cron 5-field UTC)");
loop {
tokio::select! {
_ = tick.tick() => {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64;
let bits = time_bits_utc(now);
for (cfg, cron) in &parsed {
if cron.matches(&bits) {
fire(cfg).await;
}
}
}
_ = term.recv() => { info!("SIGTERM"); return Ok(()); }
_ = int_.recv() => { info!("SIGINT"); return Ok(()); }
}
}
}
async fn fire(cfg: &TimerConfig) {
info!(name = %cfg.name, "TIMER FIRE");
if cfg.card.is_none() {
return;
}
// En el futuro: forwardear via bus a un proveedor que haga SpawnRequest.
// Por ahora log estructurado.
info!(name = %cfg.name, "card spawn requested (no-op por ahora)");
}
fn load_timers() -> Vec<TimerConfig> {
let path = std::env::var("ENTE_TIMERS_FILE")
.unwrap_or_else(|_| "/etc/ente/timers.json".into());
match std::fs::read_to_string(&path) {
Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
warn!(?e, path, "timers.json inválido — sin timers");
vec![]
}),
Err(_) => {
info!(path, "timers.json ausente — scheduler inactivo");
vec![]
}
}
}
async fn announce_to_fractal() {
if let Ok(mut client) = BusClient::from_env().await {
let req = BusRequest::Announce {
capabilities: vec![Capability::Endpoint {
interface: ente_card::InterfaceId([0xa8; 16]),
version: 1,
}],
};
match client.call(req).await {
Ok(BusResponse::Ok) => info!("Announce → bus interno OK"),
Ok(other) => warn!(?other, "Announce respuesta inesperada"),
Err(e) => warn!(?e, "Announce falló"),
}
}
}
fn init_tracing() {
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("ente_timer_compat=info"));
tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
}