reboot command beside poweroff, one polkit grant; forward yukart/gnss/+; mock uses current names
--allow-poweroff becomes --allow-power. deploy/50-yukart-gw-power.pkla adds logind's reboot actions and the deploy removes the old poweroff-only file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
# polkit (0.105, local authority): lets the svc-gw user power the host off through
|
||||
# logind, with no sudo. svc-gw runs `systemctl poweroff` for the display's
|
||||
# `poweroff` command (contracts BOUNDARY.md). Nothing else is granted.
|
||||
# polkit (0.105, local authority): lets the svc-gw user power the host off or reboot it
|
||||
# through logind, with no sudo. svc-gw runs `systemctl poweroff` / `systemctl reboot`
|
||||
# for the display's `poweroff` and `reboot` commands (contracts BOUNDARY.md). Nothing
|
||||
# else is granted.
|
||||
# Installed to /etc/polkit-1/localauthority/50-local.d/ by scripts/deploy.sh.
|
||||
# multiple-sessions: an ssh login is another session; ignore-inhibit: no inhibitor lock
|
||||
# (none are taken on this host today) should leave the button silently doing nothing.
|
||||
[yukart-gw: power off]
|
||||
[yukart-gw: power off and reboot]
|
||||
Identity=unix-user:mikkeli
|
||||
Action=org.freedesktop.login1.power-off;org.freedesktop.login1.power-off-multiple-sessions;org.freedesktop.login1.power-off-ignore-inhibit
|
||||
Action=org.freedesktop.login1.power-off;org.freedesktop.login1.power-off-multiple-sessions;org.freedesktop.login1.power-off-ignore-inhibit;org.freedesktop.login1.reboot;org.freedesktop.login1.reboot-multiple-sessions;org.freedesktop.login1.reboot-ignore-inhibit
|
||||
ResultAny=yes
|
||||
ResultInactive=yes
|
||||
ResultActive=yes
|
||||
@@ -8,8 +8,8 @@ StartLimitIntervalSec=0
|
||||
[Service]
|
||||
User=mikkeli
|
||||
# Add --mock only on a bench unit: every message then says so, and displays must show it.
|
||||
# --allow-poweroff: the display may shut this host down (contracts BOUNDARY.md, poweroff). Only on the Orin.
|
||||
ExecStart=/usr/local/bin/yukart-gw --listen 0.0.0.0:7700 --allow-poweroff
|
||||
# --allow-power: the display may power this host off or reboot it (contracts BOUNDARY.md). Only on the Orin.
|
||||
ExecStart=/usr/local/bin/yukart-gw --listen 0.0.0.0:7700 --allow-power
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ directly.
|
||||
0. **Boundary v0** — in place: WebSocket on `:7700`, hello, heartbeats both ways,
|
||||
`mark` forwarded to the recorder's control socket, mock state behind `--mock`.
|
||||
Protocol in contracts [`docs/BOUNDARY.md`](gitea:yukart/contracts).
|
||||
**Bus input** — in place (2026-10-12): subscribes to `yukart/vehicle/+` and
|
||||
`yukart/host/+`, keeps the newest envelope per signal, marks stale after 5 s
|
||||
**Bus input** — in place (2026-10-12): subscribes to `yukart/vehicle/+`,
|
||||
`yukart/host/+` and `yukart/gnss/+`, keeps the newest envelope per signal, marks stale after 5 s
|
||||
or when known only from a retained copy. Bus or mock, never both.
|
||||
1. Static file serving for the basemap — smallest useful thing, unblocks the map.
|
||||
2. Nav HTTP proxy to [svc-nav](gitea:yukart/svc-nav).
|
||||
|
||||
+7
-5
@@ -6,8 +6,8 @@
|
||||
# env: BUILD_HOST (default: mikkeli-orin-nano-2)
|
||||
#
|
||||
# Builds what is pushed, not the working tree: the build host pulls master of
|
||||
# svc-gw and contracts under ~/dev/yukart/. Native on purpose: the poweroff command
|
||||
# runs systemctl, and std::process::Command cross-compiled against the workstation's
|
||||
# svc-gw and contracts under ~/dev/yukart/. Native on purpose: the poweroff and reboot
|
||||
# commands run systemctl, and std::process::Command cross-compiled against the workstation's
|
||||
# newer glibc links GLIBC_2.39 symbols (pidfd_spawnp) the car cannot load.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -31,13 +31,15 @@ scripts/check-glibc.sh "$BIN"
|
||||
ssh "$HOST" 'mkdir -p ~/infra/svc-gw'
|
||||
scp -q "$BIN" "$HOST:infra/svc-gw/svc-gw.new"
|
||||
scp -q deploy/yukart-gw.service "$HOST:infra/svc-gw/yukart-gw.service"
|
||||
scp -q deploy/50-yukart-gw-poweroff.pkla "$HOST:infra/svc-gw/50-yukart-gw-poweroff.pkla"
|
||||
scp -q deploy/50-yukart-gw-power.pkla "$HOST:infra/svc-gw/50-yukart-gw-power.pkla"
|
||||
ssh "$HOST" 'set -e
|
||||
mv ~/infra/svc-gw/svc-gw.new ~/infra/svc-gw/svc-gw
|
||||
sudo ln -sfn "$HOME/infra/svc-gw/svc-gw" /usr/local/bin/yukart-gw
|
||||
sudo install -m 0644 ~/infra/svc-gw/yukart-gw.service /etc/systemd/system/yukart-gw.service
|
||||
# polkit, not sudo, lets the service power the host off (the poweroff command).
|
||||
sudo install -m 0644 ~/infra/svc-gw/50-yukart-gw-poweroff.pkla /etc/polkit-1/localauthority/50-local.d/50-yukart-gw-poweroff.pkla
|
||||
# polkit, not sudo, lets the service power the host off or reboot it (the poweroff and
|
||||
# reboot commands). The file was named 50-yukart-gw-poweroff.pkla before reboot joined.
|
||||
sudo install -m 0644 ~/infra/svc-gw/50-yukart-gw-power.pkla /etc/polkit-1/localauthority/50-local.d/50-yukart-gw-power.pkla
|
||||
sudo rm -f /etc/polkit-1/localauthority/50-local.d/50-yukart-gw-poweroff.pkla ~/infra/svc-gw/50-yukart-gw-poweroff.pkla
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable yukart-gw.service >/dev/null
|
||||
sudo systemctl restart yukart-gw.service
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ use std::time::{Duration, Instant};
|
||||
use rumqttc::{Client, Event, MqttOptions, Packet, QoS};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
const TOPICS: [&str; 2] = ["yukart/vehicle/+", "yukart/host/+"];
|
||||
const TOPICS: [&str; 3] = ["yukart/vehicle/+", "yukart/host/+", "yukart/gnss/+"];
|
||||
const STALE: Duration = Duration::from_secs(5);
|
||||
|
||||
struct Entry {
|
||||
|
||||
+19
-17
@@ -26,8 +26,8 @@ pub struct Ctx {
|
||||
pub hub: Hub,
|
||||
pub hello: Value,
|
||||
pub recorder_ctl: String,
|
||||
/// `--allow-poweroff`: off by default, so a gateway on a workstation can never shut it down.
|
||||
pub allow_poweroff: bool,
|
||||
/// `--allow-power`: off by default, so a gateway on a workstation can never shut it down or reboot it.
|
||||
pub allow_power: bool,
|
||||
/// Acks by command id, shared across connections so a retry after reconnect is answered, not re-run.
|
||||
pub acks: Mutex<VecDeque<(String, Value)>>,
|
||||
}
|
||||
@@ -130,39 +130,41 @@ fn run(ctx: &Ctx, msg: &Value) -> (bool, String) {
|
||||
Err(e) => (false, format!("recorder unreachable: {e}")),
|
||||
}
|
||||
}
|
||||
"poweroff" => poweroff(ctx, &msg["args"]),
|
||||
"poweroff" => power(ctx, &msg["args"], "poweroff"),
|
||||
"reboot" => power(ctx, &msg["args"], "reboot"),
|
||||
other => (false, format!("unknown command '{other}'")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The host's boot id: a poweroff command must carry the current one, so a resend
|
||||
/// The host's boot id: a poweroff or reboot command must carry the current one, so a resend
|
||||
/// from before a reboot cannot shut the next boot down.
|
||||
pub fn boot_id() -> String {
|
||||
std::fs::read_to_string("/proc/sys/kernel/random/boot_id").unwrap_or_default().trim().to_string()
|
||||
}
|
||||
|
||||
fn poweroff(ctx: &Ctx, args: &Value) -> (bool, String) {
|
||||
if !ctx.allow_poweroff {
|
||||
return (false, "poweroff not enabled on this gateway (--allow-poweroff)".into());
|
||||
/// `poweroff` or `reboot`: the same guards, the same grant, a different systemctl verb.
|
||||
fn power(ctx: &Ctx, args: &Value, verb: &'static str) -> (bool, String) {
|
||||
if !ctx.allow_power {
|
||||
return (false, format!("{verb} not enabled on this gateway (--allow-power)"));
|
||||
}
|
||||
if args["confirm"].as_str() != Some("poweroff") {
|
||||
return (false, "poweroff needs args.confirm = \"poweroff\"".into());
|
||||
if args["confirm"].as_str() != Some(verb) {
|
||||
return (false, format!("{verb} needs args.confirm = \"{verb}\""));
|
||||
}
|
||||
if args["boot_id"].as_str() != Some(boot_id().as_str()) {
|
||||
return (false, "poweroff refused: boot_id is not this boot's".into());
|
||||
return (false, format!("{verb} refused: boot_id is not this boot's"));
|
||||
}
|
||||
eprintln!("svc-gw: poweroff requested by a client");
|
||||
eprintln!("svc-gw: {verb} requested by a client");
|
||||
// Ack first: the reply has to reach the client before the services stop.
|
||||
thread::spawn(|| {
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
// Unprivileged: polkit grants this user logind's power-off (deploy/50-yukart-gw-poweroff.pkla).
|
||||
match Command::new("systemctl").args(["poweroff", "--no-ask-password"]).status() {
|
||||
// Unprivileged: polkit grants this user logind's power-off and reboot (deploy/50-yukart-gw-power.pkla).
|
||||
match Command::new("systemctl").args([verb, "--no-ask-password"]).status() {
|
||||
Ok(s) if s.success() => {}
|
||||
Ok(s) => eprintln!("svc-gw: systemctl poweroff exited {s}"),
|
||||
Err(e) => eprintln!("svc-gw: systemctl poweroff: {e}"),
|
||||
Ok(s) => eprintln!("svc-gw: systemctl {verb} exited {s}"),
|
||||
Err(e) => eprintln!("svc-gw: systemctl {verb}: {e}"),
|
||||
}
|
||||
});
|
||||
(true, "shutting down in 2 s".into())
|
||||
(true, if verb == "reboot" { "rebooting in 2 s" } else { "shutting down in 2 s" }.into())
|
||||
}
|
||||
|
||||
fn recorder(path: &str, line: &str) -> std::io::Result<String> {
|
||||
|
||||
+4
-4
@@ -25,7 +25,7 @@ usage: svc-gw [options]
|
||||
--mock synthesise vehicle state, flagged as mock in every message (no bus)
|
||||
--broker HOST:PORT MQTT broker; '' for none (127.0.0.1:1883)
|
||||
--recorder-ctl PATH recorder control socket, for 'mark' (/run/yukart-recorder/ctl.sock)
|
||||
--allow-poweroff accept the 'poweroff' command (the Orin's unit only; never on a workstation)";
|
||||
--allow-power accept the 'poweroff' and 'reboot' commands (the Orin's unit only; never on a workstation)";
|
||||
|
||||
pub fn now_us() -> i64 {
|
||||
let d = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
|
||||
@@ -54,13 +54,13 @@ fn main() -> ExitCode {
|
||||
let mut listen = "0.0.0.0:7700".to_string();
|
||||
let mut mock = false;
|
||||
let mut recorder_ctl = "/run/yukart-recorder/ctl.sock".to_string();
|
||||
let mut allow_poweroff = false;
|
||||
let mut allow_power = false;
|
||||
let mut broker = "127.0.0.1:1883".to_string();
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(a) = args.next() {
|
||||
match a.as_str() {
|
||||
"--mock" => mock = true,
|
||||
"--allow-poweroff" => allow_poweroff = true,
|
||||
"--allow-power" => allow_power = true,
|
||||
"--listen" => listen = args.next().unwrap_or_default(),
|
||||
"--recorder-ctl" => recorder_ctl = args.next().unwrap_or_default(),
|
||||
"--broker" => broker = args.next().unwrap_or_default(),
|
||||
@@ -99,7 +99,7 @@ fn main() -> ExitCode {
|
||||
"timeout_ms": client::TIMEOUT.as_millis() as u64,
|
||||
}),
|
||||
recorder_ctl,
|
||||
allow_poweroff,
|
||||
allow_power,
|
||||
acks: Mutex::new(Default::default()),
|
||||
});
|
||||
|
||||
|
||||
+7
-7
@@ -40,20 +40,20 @@ impl Mock {
|
||||
s.insert(name.to_string(), reg.envelope(name, v, q, true));
|
||||
};
|
||||
put("engine_rpm", json!(rpm.round()), None);
|
||||
put("vehicle_speed_kph", json!((speed * 10.0_f64).round() / 10.0), None);
|
||||
put("coolant_temp_c", json!(self.coolant.round()), None);
|
||||
// Throttle drops out for 5 s in every 40: a signal that disappears.
|
||||
put("gnss_speed_kph", json!((speed * 10.0_f64).round() / 10.0), None);
|
||||
put("obd_coolant_temp_c", json!(self.coolant.round()), None);
|
||||
// The pedal drops out for 5 s in every 40: a signal that disappears.
|
||||
if t % 40.0 >= 5.0 {
|
||||
put("throttle_pct", json!(throttle), None);
|
||||
put("accel_pedal_pct", json!(throttle), None);
|
||||
}
|
||||
// Intake temperature is stuck at its last value and marked stale for 10 s in every 30.
|
||||
if t % 30.0 < 10.0 {
|
||||
put("intake_air_temp_c", json!(31.0), Some("stale"));
|
||||
put("obd_intake_air_temp_c", json!(31.0), Some("stale"));
|
||||
} else {
|
||||
put("intake_air_temp_c", json!((31.0 + (t * 0.05).sin()).round()), None);
|
||||
put("obd_intake_air_temp_c", json!((31.0 + (t * 0.05).sin()).round()), None);
|
||||
}
|
||||
// Fuel level is known but has no value: the registry has it, the car never sent it.
|
||||
put("fuel_level_pct", Value::Null, None);
|
||||
put("obd_fuel_level_pct", Value::Null, None);
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user