diff --git a/Cargo.lock b/Cargo.lock index f0172c8..a56ca8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,17 +165,6 @@ dependencies = [ "slab", ] -[[package]] -name = "async-fs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - [[package]] name = "async-io" version = "2.6.0" @@ -3885,6 +3874,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "softbuffer" version = "0.4.8" @@ -4209,11 +4208,14 @@ version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -5441,15 +5443,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" dependencies = [ "async-broadcast", - "async-executor", - "async-fs", - "async-io", - "async-lock", "async-process", "async-recursion", - "async-task", "async-trait", - "blocking", "enumflags2", "event-listener", "futures-core", @@ -5463,6 +5459,7 @@ dependencies = [ "serde_repr", "sha1", "static_assertions", + "tokio", "tracing", "uds_windows", "windows-sys 0.52.0", diff --git a/Cargo.toml b/Cargo.toml index 5405c15..12ad0f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,8 @@ rust-version = "1.96" [workspace.dependencies] tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "sync", "time"] } -zbus = "4" +# tokio-executor у zbus (а не async-io) — сервисы на #[tokio::main]; нужно для tokio::spawn в хендлерах (grace-таймер). +zbus = { version = "4", default-features = false, features = ["tokio"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" diff --git a/crates/core/shturman-power/src/fsm.rs b/crates/core/shturman-power/src/fsm.rs index afa6b16..d9b4a6b 100644 --- a/crates/core/shturman-power/src/fsm.rs +++ b/crates/core/shturman-power/src/fsm.rs @@ -102,7 +102,16 @@ impl PowerFsm { self.state = Accessory; vec![] } - (Accessory | Running, E::AccOff) => self.begin_shutdown(ShutdownReason::AccOff), + // ACC-off: линия ACC сменилась (AccChanged) + старт shutdown. + (Accessory | Running, E::AccOff) => { + self.state = ShuttingDown { phase: Abortable, reason: ShutdownReason::AccOff }; + vec![ + Action::AccChanged(false), + Action::ShutdownImminent(ShutdownReason::AccOff), + Action::StartGrace, + ] + } + // under-voltage/thermal: ACC не менялся → без AccChanged. (Accessory | Running, E::UnderVoltage) => self.begin_shutdown(ShutdownReason::UnderVoltage), (Accessory | Running, E::ThermalTrip) => self.begin_shutdown(ShutdownReason::Thermal), (ShuttingDown { phase: Abortable, .. }, E::AccOn) => { @@ -149,7 +158,11 @@ mod tests { let mut f = PowerFsm::new(); // Running assert_eq!( f.step(Event::AccOff), - vec![Action::ShutdownImminent(ShutdownReason::AccOff), Action::StartGrace] + vec![ + Action::AccChanged(false), + Action::ShutdownImminent(ShutdownReason::AccOff), + Action::StartGrace + ] ); assert_eq!(f.power_state(), PowerState::ShuttingDown); assert_eq!(f.source(), PowerSource::HoldupCap); diff --git a/crates/core/shturman-power/src/service.rs b/crates/core/shturman-power/src/service.rs index 15a639e..d89e753 100644 --- a/crates/core/shturman-power/src/service.rs +++ b/crates/core/shturman-power/src/service.rs @@ -1,39 +1,24 @@ -//! Server-стаб `ru.shturman.Power1` + (feature `dev-mocks`) `ru.shturman.dev.PowerMock1` (fake-ACC). -//! zbus 4: несколько интерфейсов на одном объекте — это РАЗНЫЕ типы на одном пути, разделяющие -//! состояние через `Arc>` (а не два `#[interface]` на одном типе). +//! Server `ru.shturman.Power1` + (feature `dev-mocks`) `ru.shturman.dev.PowerMock1` (fake-ACC). +//! v0.3: оборачивает чистый `PowerFsm` (спека §5–§7). dev-mock кормит входы FSM (не флипает состояние). +use crate::fsm::{Action, Event, PowerFsm}; use shturman_common::monotonic_secs; use shturman_ipc::types::{IgnitionState, PowerSource, PowerState}; use std::sync::{Arc, Mutex}; +use std::time::Duration; use zbus::interface; use zbus::object_server::SignalContext; -struct State { - power: PowerState, - ignition: IgnitionState, - source: PowerSource, -} +/// Grace-окно (сек): и поле сигнала `ShutdownImminent`, и длительность таймера. v0.3 — фикс. (конфиг — позже). +const GRACE_SECS: u32 = 2; -impl Default for State { - fn default() -> Self { - Self { - power: PowerState::Running, - ignition: IgnitionState::Running, - source: PowerSource::Vehicle12v, - } - } -} - -/// Стаб питания (`Power1`). В v0 стартует в `running`; запись/actuator отсутствуют (#2). pub struct PowerService { - state: Arc>, + fsm: Arc>, } impl Default for PowerService { fn default() -> Self { - Self { - state: Arc::new(Mutex::new(State::default())), - } + Self { fsm: Arc::new(Mutex::new(PowerFsm::new())) } } } @@ -41,25 +26,60 @@ impl PowerService { pub fn new() -> Self { Self::default() } - - // Inherent-аксессоры (тесты + источник для interface-методов). pub fn power_state(&self) -> PowerState { - self.state.lock().unwrap().power + self.fsm.lock().unwrap().power_state() } pub fn ignition(&self) -> IgnitionState { - self.state.lock().unwrap().ignition + self.fsm.lock().unwrap().ignition() } pub fn source(&self) -> PowerSource { - self.state.lock().unwrap().source + self.fsm.lock().unwrap().source() } - /// dev-mock «fake-ACC», разделяющий состояние (только в dev-сборке). #[cfg(feature = "dev-mocks")] pub fn mock(&self) -> PowerMock { - PowerMock { - state: Arc::clone(&self.state), + PowerMock { fsm: Arc::clone(&self.fsm) } + } +} + +/// Durable-write barrier (#5): сбросить грязные страницы `/data` ДО PONR (Settings уже синхронен). +fn durable_barrier() { + let _ = std::process::Command::new("sync").status(); + tracing::info!( + "power: commit (PONR) — durable barrier sync; load-shed: amp/backlight/modem (нет реальных нагрузок в v0)" + ); +} + +/// Шагнуть FSM и исполнить действия (эмит сигналов, grace-таймер, durable-barrier). +async fn apply_event( + fsm: &Arc>, + ev: Event, + ctx: &SignalContext<'_>, +) -> zbus::Result<()> { + let actions = fsm.lock().unwrap().step(ev); + for a in actions { + match a { + Action::ShutdownImminent(r) => { + PowerService::shutdown_imminent(ctx, GRACE_SECS, r.as_str()).await? + } + Action::ShutdownAborted => PowerService::shutdown_aborted(ctx).await?, + Action::AccChanged(on) => PowerService::acc_changed(ctx, on).await?, + Action::StartGrace => { + // Фоновый grace-таймер (монотоника tokio). По истечении — GraceExpired: + // commit (durable-barrier), если FSM ещё в abortable; если был re-power (abort) — no-op. + let fsm = Arc::clone(fsm); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(GRACE_SECS as u64)).await; + let acts = fsm.lock().unwrap().step(Event::GraceExpired); + if acts.contains(&Action::Commit) { + durable_barrier(); + } + }); + } + Action::Commit => durable_barrier(), } } + Ok(()) } #[interface(name = "ru.shturman.Power1")] @@ -68,19 +88,17 @@ impl PowerService { self.power_state().as_str().to_string() } - /// Внутренний; в v0-стабе — no-op (полная sleep/wake — v1/v2, B §7). + /// Внутренний; sleep/wake — v1/v2 (B §7). В v0.3 — no-op. async fn request_sleep(&self) {} #[zbus(property)] async fn ignition_state(&self) -> String { self.ignition().as_str().to_string() } - #[zbus(property)] async fn uptime(&self) -> u64 { monotonic_secs() } - #[zbus(property)] async fn power_source(&self) -> String { self.source().as_str().to_string() @@ -102,51 +120,46 @@ impl PowerService { async fn wake(ctx: &SignalContext<'_>) -> zbus::Result<()>; } -/// dev-mock «fake-ACC» — отдельный тип на том же пути. Прод (`--no-default-features`) его НЕ регистрирует. -/// Методы возвращают `()` (ошибку эмита сигнала игнорируем — мок не отвечает D-Bus-ошибкой). +/// dev-mock «fake-ACC/voltage/thermal» — кормит входы FSM. Прод (`--no-default-features`) не регистрирует. #[cfg(feature = "dev-mocks")] pub struct PowerMock { - state: Arc>, + fsm: Arc>, } #[cfg(feature = "dev-mocks")] #[interface(name = "ru.shturman.dev.PowerMock1")] impl PowerMock { async fn set_acc(&self, on: bool, #[zbus(signal_context)] ctx: SignalContext<'_>) { - { - let mut st = self.state.lock().unwrap(); - st.ignition = if on { - IgnitionState::Running - } else { - IgnitionState::Off - }; - st.power = if on { - PowerState::Running - } else { - PowerState::Off - }; - } - // Эмитим Power1-сигнал (тот же путь; имя интерфейса добавляет сама acc_changed). - let _ = PowerService::acc_changed(&ctx, on).await; + let ev = if on { Event::AccOn } else { Event::AccOff }; + let _ = apply_event(&self.fsm, ev, &ctx).await; } - async fn set_ignition(&self, state: String) { - if let Ok(ig) = state.parse::() { - self.state.lock().unwrap().ignition = ig; - } + async fn set_ignition(&self, state: String, #[zbus(signal_context)] ctx: SignalContext<'_>) { + // accessory↔running — через EngineOn/Off; off — AccOff. + let ev = match state.as_str() { + "running" => Event::EngineOn, + "accessory" => Event::EngineOff, + _ => Event::AccOff, + }; + let _ = apply_event(&self.fsm, ev, &ctx).await; } async fn trigger_shutdown( &self, - seconds: u32, + _seconds: u32, reason: String, #[zbus(signal_context)] ctx: SignalContext<'_>, ) { - let _ = PowerService::shutdown_imminent(&ctx, seconds, &reason).await; + let ev = match reason.as_str() { + "thermal" => Event::ThermalTrip, + "under_voltage" => Event::UnderVoltage, + _ => Event::AccOff, + }; + let _ = apply_event(&self.fsm, ev, &ctx).await; } async fn abort_shutdown(&self, #[zbus(signal_context)] ctx: SignalContext<'_>) { - let _ = PowerService::shutdown_aborted(&ctx).await; + let _ = apply_event(&self.fsm, Event::AccOn, &ctx).await; } } diff --git a/crates/core/shturman-power/tests/integration.rs b/crates/core/shturman-power/tests/integration.rs index 005c799..c7a3637 100644 --- a/crates/core/shturman-power/tests/integration.rs +++ b/crates/core/shturman-power/tests/integration.rs @@ -48,3 +48,48 @@ async fn power_state_and_fake_acc() { let sig = acc.next().await.unwrap(); assert!(!sig.args().unwrap().on()); } + +#[tokio::test] +#[ignore = "нужна session-шина: just test-integration"] +async fn shutdown_imminent_then_abort() { + let svc = PowerService::new(); + let mock = svc.mock(); + let server = zbus::Connection::session().await.unwrap(); + server.object_server().at(names::power::PATH, svc).await.unwrap(); + server.object_server().at(names::power::PATH, mock).await.unwrap(); + server.request_name(names::power::NAME).await.unwrap(); + + let client = zbus::Connection::session().await.unwrap(); + let power = PowerClient::new(&client).await.unwrap(); + let mut imminent = power.proxy().receive_shutdown_imminent().await.unwrap(); + let mut aborted = power.proxy().receive_shutdown_aborted().await.unwrap(); + + // ACC-off → ShutdownImminent(acc_off), состояние shutting_down + client + .call_method( + Some(names::power::NAME), + names::power::PATH, + Some(names::power::MOCK_IFACE), + "SetAcc", + &(false,), + ) + .await + .unwrap(); + let sig = imminent.next().await.unwrap(); + assert_eq!(sig.args().unwrap().reason(), "acc_off"); + assert_eq!(power.power_state().await.unwrap(), PowerState::ShuttingDown); + + // re-power до grace → ShutdownAborted + running + client + .call_method( + Some(names::power::NAME), + names::power::PATH, + Some(names::power::MOCK_IFACE), + "SetAcc", + &(true,), + ) + .await + .unwrap(); + aborted.next().await.unwrap(); + assert_eq!(power.power_state().await.unwrap(), PowerState::Running); +} diff --git a/justfile b/justfile index 048fc5d..8f3df61 100644 --- a/justfile +++ b/justfile @@ -22,9 +22,10 @@ lint: deny: cargo deny check -# интеграционные тесты на session-шине (нужен dbus: brew install dbus / в Lima) +# интеграционные тесты на session-шине (нужен dbus: brew install dbus / в Lima). +# --test-threads=1: тесты владеют одними well-known именами на общей шине → серийно (иначе кросс-talk/вис). test-integration: - dbus-run-session -- cargo test --workspace -- --ignored + dbus-run-session -- cargo test --workspace -- --ignored --test-threads=1 # полный локальный гейт ci: lint test deny