Skip to content

Commit

Permalink
motd: maintain a /etc/motd file with interesting info about the TAC
Browse files Browse the repository at this point in the history
Not anyone looks at the web interface or the display of the TAC.
This gives us a third vector to convey information to the user,
by showing messages on ssh login.

Use bind-mounts to put the highly volatile file itself to /var/run
and point to it from /etc.

Signed-off-by: Stefan Kerkmann <[email protected]>
Co-authored-by: Leonard Göhrs <[email protected]>
  • Loading branch information
hnez authored and KarlK90 committed Aug 7, 2024
1 parent a5faf76 commit 9ec1977
Show file tree
Hide file tree
Showing 3 changed files with 295 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/target
/.cargo
/demo_files/home/root/.ssh/authorized_keys
/demo_files/var/run/tacd/motd
/web/npm-shrinkwrap.json
/web/oe-logs
/web/oe-workdir
14 changes: 14 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ mod iobus;
mod journal;
mod led;
mod measurement;
mod motd;
mod regulators;
mod setup_mode;
mod system;
Expand Down Expand Up @@ -133,6 +134,19 @@ async fn init(screenshooter: ScreenShooter) -> Result<(Ui, WatchedTasksBuilder)>
// in the web interface.
journal::serve(&mut http_server.server);

// Maintain a /etc/motd with useful information about the TAC.
if let Err(err) = motd::run(
&mut wtb,
&dut_pwr,
&iobus,
&rauc,
&setup_mode,
&temperatures,
&usb_hub,
) {
error!("failed to start motd update service with {err}");
}

// Set up the user interface for the hardware display on the TAC.
// The different screens receive updates via the topics provided in
// the UiResources struct.
Expand Down
280 changes: 280 additions & 0 deletions src/motd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
use std::fmt::{self, Display, Formatter};
use std::fs::{create_dir_all, File};
use std::io::{Seek, Write};
use std::path::Path;

use anyhow::Result;
use futures::FutureExt;
use nix::mount::MsFlags;

use crate::dut_power::OutputState;
use crate::temperatures::Warning;
use crate::usb_hub::OverloadedPort;
use crate::WatchedTasksBuilder;

#[cfg(feature = "demo_mode")]
mod setup {
pub(super) const VAR_RUN_TACD: &str = "demo_files/var/run/tacd";
pub(super) const ETC: &str = "demo_files/etc";

/// mount stub for demo_mode that works without root permissions
///
/// (by doing nothing).
pub(super) fn mount(
_source: Option<&std::path::Path>,
_target: &std::path::Path,
_fstype: Option<&str>,
_flags: nix::mount::MsFlags,
_data: Option<&str>,
) -> nix::Result<()> {
Ok(())
}
}

#[cfg(not(feature = "demo_mode"))]
mod setup {
pub(super) use nix::mount::mount;
pub(super) const VAR_RUN_TACD: &str = "/var/run/tacd";
pub(super) const ETC: &str = "/etc";
}

use setup::*;

struct Motd {
dut_pwr_state: OutputState,
iobus_fault: bool,
rauc_should_reboot: bool,
rauc_update_urls: Vec<String>,
setup_mode_active: bool,
temperature_warning: bool,
usb_overload: Option<OverloadedPort>,
handle: File,
}

const COLOR_RED: &str = "\x1b[31m";
const COLOR_GREEN: &str = "\x1b[32m";
const COLOR_YELLOW: &str = "\x1b[33m";
const COLOR_RESET: &str = "\x1b[0m";

impl Display for Motd {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(f, "Welcome to you TAC!")?;
writeln!(f)?;

if self.temperature_warning {
writeln!(
f,
"- {COLOR_RED}WARNING{COLOR_RESET}: Your TAC is overheating, please provide proper airflow and let",
)?;
writeln!(f, " it cool down.")?;
}

if self.setup_mode_active {
writeln!(
f,
"- {COLOR_GREEN}GREAT!{COLOR_RESET} You have logged in successfully!",
)?;
writeln!(
f,
" Now you should continue the setup process in the web interface"
)?;
writeln!(f, " to leave the setup mode.")?;
}

if self.rauc_should_reboot {
writeln!(
f,
"- {COLOR_YELLOW}INFO{COLOR_RESET}: A software update was installed. Please reboot to start using it.",
)?;
}

if !self.rauc_update_urls.is_empty() {
writeln!(
f,
"- {COLOR_YELLOW}INFO{COLOR_RESET}: A software update is available. To install it run:",
)?;
writeln!(f)?;

for url in &self.rauc_update_urls {
writeln!(f, " rauc install \"{url}\"")?;
writeln!(f)?;
}
}

match self.dut_pwr_state {
OutputState::On => {
writeln!(
f,
"- {COLOR_GREEN}NOTE{COLOR_RESET}: The device under test is currently powered on.",
)?;
}
OutputState::Off | OutputState::OffFloating | OutputState::Changing => {}
OutputState::InvertedPolarity => {
writeln!(
f,
"- {COLOR_RED}WARNING{COLOR_RESET}: The device under test was powered off due to inverted polarity.",
)?;
}
OutputState::OverCurrent => {
writeln!(
f,
"- {COLOR_RED}WARNING{COLOR_RESET}: The device under test was powered off due to overcurrent.",
)?;
}
OutputState::OverVoltage => {
writeln!(
f,
"- {COLOR_RED}WARNING{COLOR_RESET}: The device under test was powered off due to overvoltage.",
)?;
}
OutputState::RealtimeViolation => {
writeln!(
f,
"- {COLOR_RED}WARNING{COLOR_RESET}: The device under test was powered because the TAC could not hold",
)?;

writeln!(f, " its realtime guarantees.",)?;
}
}

if let Some(port) = &self.usb_overload {
let port = match port {
OverloadedPort::Total => " ",
OverloadedPort::Port1 => " 1 ",
OverloadedPort::Port2 => " 2 ",
OverloadedPort::Port3 => " 3 ",
};

writeln!(
f,
"- {COLOR_RED}WARNING{COLOR_RESET}: The USB port{port}power supply is overloaded.",
)?;
}

if self.iobus_fault {
writeln!(
f,
"- {COLOR_RED}WARNING{COLOR_RESET}: The LXA IOBus power supply is overloaded.",
)?;
}

Ok(())
}
}

pub fn run(
wtb: &mut WatchedTasksBuilder,
dut_pwr: &crate::dut_power::DutPwrThread,
iobus: &crate::iobus::IoBus,
rauc: &crate::dbus::Rauc,
setup_mode: &crate::setup_mode::SetupMode,
temperatures: &crate::temperatures::Temperatures,
usb_hub: &crate::usb_hub::UsbHub,
) -> Result<()> {
let mut motd = Motd::new()?;

// Write default MOTD once on startup
motd.update()?;

// Spawn a task that accepts motd updates and dumps them into the file in /var/run.
let (state_events, _) = dut_pwr.state.clone().subscribe_unbounded();
let (fault_events, _) = iobus.supply_fault.clone().subscribe_unbounded();
let (should_reboot_events, _) = rauc.should_reboot.clone().subscribe_unbounded();
let (channels_events, _) = rauc.channels.clone().subscribe_unbounded();
let (setup_mode_events, _) = setup_mode.setup_mode.clone().subscribe_unbounded();
let (temperature_events, _) = temperatures.warning.clone().subscribe_unbounded();
let (usb_events, _) = usb_hub.overload.clone().subscribe_unbounded();

wtb.spawn_task("motd-file-service", async move {
loop {
futures::select! {
update = state_events.recv().fuse() => {
motd.dut_pwr_state = update?;
},
update = fault_events.recv().fuse() => {
motd.iobus_fault = update?;
},
update = should_reboot_events.recv().fuse() => {
motd.rauc_should_reboot = update?;
},
update = channels_events.recv().fuse() => {
motd.rauc_update_urls = update?
.into_iter()
.filter_map(|ch| {
ch.bundle
.as_ref()
.map_or(false, |b| b.newer_than_installed)
.then_some(ch.url)
})
.collect();
},
update = setup_mode_events.recv().fuse() => {
motd.setup_mode_active = update?;
},
update = temperature_events.recv().fuse() => {
motd.temperature_warning = match update? {
Warning::Okay => false,
Warning::SocHigh | Warning::SocCritical => true,
};
},
update = usb_events.recv().fuse() => {
motd.usb_overload = update?;
},
};

motd.update()?;
}
})?;

Ok(())
}

impl Motd {
/// Create a motd in a tmpfs so we can write it without harming the eMMC
fn new() -> Result<Self> {
// Create /var/run/tacd (or an equivalent in demo mode).
create_dir_all(VAR_RUN_TACD)?;

// "/var/run/tacd/motd" or "demo_files/var/run/tacd/motd"
// "/etc/motd" or "demo_files/etc/motd"
let path_runtime_motd = Path::new(VAR_RUN_TACD).join("motd");
let path_etc_motd = Path::new(ETC).join("motd");

// Create the motd file in /var/run/tacd.
let runtime_motd = File::create(&path_runtime_motd)?;

// Bind (re)mount /var/run/tacd/motd to /etc/motd.
// The benefit over writing to /etc/motd directly is that we do not
// hammer the eMMC as much.
// The benefit over a symlink is that the bind-mount does not persist
// across rebots, leaving the /etc/motd point to a non-existing file.
// The drawback of using a bind-mount is that it clutters up the output
// of `mount` and that it requires special permissions that we do not
// have in demo_mode.
mount(
Some(&path_runtime_motd),
&path_etc_motd,
None::<&str>,
MsFlags::MS_BIND | MsFlags::MS_REMOUNT,
None::<&str>,
)?;

Ok(Self {
dut_pwr_state: OutputState::Off,
iobus_fault: false,
rauc_should_reboot: false,
rauc_update_urls: Vec::new(),
setup_mode_active: false,
temperature_warning: false,
usb_overload: None,
handle: runtime_motd,
})
}

fn update(&mut self) -> Result<()> {
self.handle.rewind()?;
self.handle.set_len(0)?;
write!(&self.handle, "{self}")?;
Ok(())
}
}

0 comments on commit 9ec1977

Please sign in to comment.