Package workstation tools, reuse emulator sessions, and derive build versions
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
name = "dasungd"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2024"
|
||||
description = "DASUNG Paperlike USB keepalive, control socket and Linux display profile daemon"
|
||||
license = "MIT"
|
||||
|
||||
@@ -31,7 +31,7 @@ use std::{
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(version, about)]
|
||||
#[command(version = env!("FDS_BUILD_VERSION"), about)]
|
||||
struct Cli {
|
||||
#[arg(long, global = true, default_value = "/etc/dasungd.toml")]
|
||||
config: PathBuf,
|
||||
@@ -286,7 +286,7 @@ fn daemon(config: Config) -> Result<()> {
|
||||
let mut last_error = String::new();
|
||||
eprintln!(
|
||||
"dasungd {} ready; waiting for {}",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
env!("FDS_BUILD_VERSION"),
|
||||
config.monitor_serial
|
||||
);
|
||||
while running.load(Ordering::Relaxed) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
name = "fds-boottrace"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "Measured FDS boot events and regression reports"
|
||||
|
||||
@@ -80,7 +80,7 @@ fn adopt() -> Result<()> {
|
||||
}
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
version = env!("FDS_BUILD_VERSION"),
|
||||
about = "Record and compare measured boot events",
|
||||
after_help = "Measurements use Linux CLOCK_BOOTTIME, excluding firmware and power-on."
|
||||
)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
name = "fds-burn"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "Verified cartridge images and protected FDS media writes"
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{fs::OpenOptions, os::unix::fs::OpenOptionsExt, path::PathBuf, process:
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
version = env!("FDS_BUILD_VERSION"),
|
||||
about = "Create cartridge images and preview confirmed media writes",
|
||||
after_help = "Creation writes a new regular file. Writes require confirmation tied to the selected insertion and image hash."
|
||||
)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
name = "fds-cartridged"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "FDS event-driven cartridge and physical bay manager"
|
||||
|
||||
@@ -14,7 +14,7 @@ use std::{path::PathBuf, process::ExitCode};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
version = env!("FDS_BUILD_VERSION"),
|
||||
about = "Run the root cartridge service or inspect USB topology"
|
||||
)]
|
||||
struct Cli {
|
||||
|
||||
@@ -226,7 +226,8 @@ fn session() -> Result<()> {
|
||||
}
|
||||
#[derive(clap::Parser)]
|
||||
#[command(
|
||||
version,
|
||||
name = "fds-profile",
|
||||
version = env!("FDS_BUILD_VERSION"),
|
||||
about = "Control the desktop and network profile",
|
||||
after_help = "Examples: fds-profile activate windowmaker; fds-profile deactivate"
|
||||
)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
name = "fds-cli"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "FDS system and cartridge command interface"
|
||||
|
||||
@@ -12,10 +12,10 @@ pub struct Cli {
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Applet {
|
||||
#[command(version, about = "FDS/OS system and cartridge control")]
|
||||
#[command(version = env!("FDS_BUILD_VERSION"), about = "FDS/OS system and cartridge control")]
|
||||
Fds(FdsArgs),
|
||||
/// Inspect a bay, image, or cartridge manifest.
|
||||
#[command(version)]
|
||||
#[command(version = env!("FDS_BUILD_VERSION"))]
|
||||
FdsInspect {
|
||||
#[arg(long, global = true)]
|
||||
json: bool,
|
||||
@@ -23,7 +23,7 @@ pub enum Applet {
|
||||
args: InspectArgs,
|
||||
},
|
||||
/// Unmount a cartridge before removal.
|
||||
#[command(version)]
|
||||
#[command(version = env!("FDS_BUILD_VERSION"))]
|
||||
FdsEject {
|
||||
#[arg(long, global = true)]
|
||||
json: bool,
|
||||
@@ -31,7 +31,7 @@ pub enum Applet {
|
||||
args: BayArgs,
|
||||
},
|
||||
/// Prepare native shutdown or inspect its status.
|
||||
#[command(version)]
|
||||
#[command(version = env!("FDS_BUILD_VERSION"))]
|
||||
FdsPower {
|
||||
#[arg(long, global = true)]
|
||||
json: bool,
|
||||
|
||||
@@ -16,7 +16,7 @@ struct Invocation {
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "fds-program",
|
||||
version,
|
||||
version = env!("FDS_BUILD_VERSION"),
|
||||
disable_help_subcommand = true,
|
||||
about = "Run a published cartridge command in the foreground"
|
||||
)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
name = "fds-common"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "Shared bounded configuration and Linux discovery for FDS tools"
|
||||
|
||||
@@ -11,7 +11,7 @@ pub mod trace;
|
||||
|
||||
use std::{fmt, fs::File, io::Read, path::Path, str::FromStr};
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const VERSION: &str = env!("FDS_BUILD_VERSION");
|
||||
pub const MAX_CONFIG_BYTES: u64 = 64 * 1024;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
name = "fds-control"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "Native grayscale X11 cartridge control panel"
|
||||
|
||||
@@ -24,7 +24,7 @@ type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
version = env!("FDS_BUILD_VERSION"),
|
||||
about = "FDS cartridge control panel for X11",
|
||||
after_help = "Select a bay to inspect its cartridge. Run opens a terminal; Eject stops its programs and releases the cartridge. Remove media only after SAFE appears."
|
||||
)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
name = "fds-release"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "FDS release manifest signing and explicit-key verification"
|
||||
|
||||
@@ -63,7 +63,13 @@ pub fn filename(value: &str) -> Result<()> {
|
||||
impl Manifest {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.format != 1
|
||||
|| self.version != fds_common::VERSION
|
||||
|| self.version.is_empty()
|
||||
|| self.version.len() > 32
|
||||
|| !self.version.starts_with(|c: char| c.is_ascii_digit())
|
||||
|| !self
|
||||
.version
|
||||
.bytes()
|
||||
.all(|b| b.is_ascii_alphanumeric() || b == b'.')
|
||||
|| self.source_epoch == 0
|
||||
|| !hex_valid(&self.source_sha256, 64)
|
||||
|| !hex_valid(&self.void_commit, 40)
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::{path::PathBuf, process::ExitCode};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
version = env!("FDS_BUILD_VERSION"),
|
||||
about = "Sign and verify FDS release artifacts",
|
||||
after_help = "Keys and signatures are never overwritten. Verification checks every listed artifact.
|
||||
An independently trusted public key is required; a bundled key is not automatically trusted."
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[package]
|
||||
name = "fds-smoketest"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
name = "fds-software"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "Verified installed software trees and legacy archive reading"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
name = "fds-stage0"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "Static FDS early boot discovery and root handoff"
|
||||
|
||||
@@ -364,7 +364,10 @@ pub fn boot() -> Result<()> {
|
||||
let events = netlink()?; // Bind before scanning so insertion cannot fall into a gap.
|
||||
let signals = child_events()?;
|
||||
let mut monitor = start_monitor(options.debug)?;
|
||||
println!("FELIS DATA SYSTEMS\nPORTABLE COMPUTER FP-85\n\nFDS BOOT ROM 0.1");
|
||||
println!(
|
||||
"FELIS DATA SYSTEMS\nPORTABLE COMPUTER FP-85\n\nFDS BOOT ROM {}",
|
||||
fds_common::VERSION
|
||||
);
|
||||
println!("Commands while waiting: list, rescan, recovery, reboot, poweroff");
|
||||
let mut previous = String::new();
|
||||
let mut pending = ConsoleInput::default();
|
||||
@@ -397,7 +400,10 @@ pub fn boot() -> Result<()> {
|
||||
if options.debug {
|
||||
println!("FDS_STAGE0_ROOT: {}", options.root_label());
|
||||
}
|
||||
println!("MEMORY ........ READY\nSYSTEM ........ FDS/OS 0.1");
|
||||
println!(
|
||||
"MEMORY ........ READY\nSYSTEM ........ FDS/OS {}",
|
||||
fds_common::VERSION
|
||||
);
|
||||
return handoff(monitor, options.debug);
|
||||
}
|
||||
Err(error) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{path::PathBuf, process::ExitCode};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
version = env!("FDS_BUILD_VERSION"),
|
||||
about = "Early boot discovery and native s6 handoff",
|
||||
after_help = "With no arguments, root PID 1 boots FDS. Diagnostic options only read their supplied inputs."
|
||||
)]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[package]
|
||||
name = "fds-workstation"
|
||||
version = "0.1.0"
|
||||
version.workspace = true
|
||||
build = "../../tools/version-build.rs"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "Linux workstation software and cartridge tools"
|
||||
|
||||
@@ -4,12 +4,13 @@ use fds_workstation::emulator::Session;
|
||||
use std::{path::PathBuf, process::ExitCode};
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
name = "fds-emulator",
|
||||
version = env!("FDS_BUILD_VERSION"),
|
||||
about = "Boot FDS/OS and hotplug virtual USB cartridges on a Linux workstation",
|
||||
after_help = "QEMU virt tests FDS software, not Raspberry Pi firmware or physical hardware. DATA uses retained temporary overlays; source images remain unchanged."
|
||||
)]
|
||||
struct Cli {
|
||||
/// Private session directory. Choose a new directory for each boot.
|
||||
/// Private session directory. A stopped session can be started again.
|
||||
#[arg(long, global = true, default_value = "out/emulator")]
|
||||
session: PathBuf,
|
||||
#[command(subcommand)]
|
||||
|
||||
@@ -12,8 +12,11 @@ use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::Write,
|
||||
os::{
|
||||
fd::AsRawFd,
|
||||
unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt},
|
||||
fd::{AsRawFd, FromRawFd, OwnedFd},
|
||||
unix::{
|
||||
fs::{DirBuilderExt, MetadataExt, OpenOptionsExt},
|
||||
net::UnixStream,
|
||||
},
|
||||
},
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
@@ -90,6 +93,43 @@ fn lock(root: &Path) -> Result<File> {
|
||||
}
|
||||
Ok(file)
|
||||
}
|
||||
// A pidfd remains attached to the original process even if its PID is reused.
|
||||
// QMP EOF alone does not establish that QEMU finished releasing its files.
|
||||
fn process(root: &Path) -> Result<Option<OwnedFd>> {
|
||||
let text = match fs::read_to_string(root.join("qemu.pid")) {
|
||||
Ok(text) => text,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let pid: i32 = text
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| Error("Invalid QEMU pid file".into()))?;
|
||||
if pid <= 0 {
|
||||
return Err(Error("Invalid QEMU pid file".into()));
|
||||
}
|
||||
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) } as i32;
|
||||
if fd < 0 {
|
||||
let error = std::io::Error::last_os_error();
|
||||
if error.raw_os_error() == Some(libc::ESRCH) {
|
||||
return Ok(None);
|
||||
}
|
||||
return Err(error.into());
|
||||
}
|
||||
Ok(Some(unsafe { OwnedFd::from_raw_fd(fd) }))
|
||||
}
|
||||
fn exited(process: &OwnedFd, timeout_ms: i32) -> Result<bool> {
|
||||
let mut event = libc::pollfd {
|
||||
fd: process.as_raw_fd(),
|
||||
events: libc::POLLIN,
|
||||
revents: 0,
|
||||
};
|
||||
let result = unsafe { libc::poll(&mut event, 1, timeout_ms) };
|
||||
if result < 0 {
|
||||
return Err(std::io::Error::last_os_error().into());
|
||||
}
|
||||
Ok(result > 0 && event.revents & libc::POLLIN != 0)
|
||||
}
|
||||
impl Session {
|
||||
pub fn load(root: &Path) -> Result<Self> {
|
||||
ordinary()?;
|
||||
@@ -159,17 +199,70 @@ impl Session {
|
||||
"Use a shorter session path (under 90 bytes), without commas or line breaks".into(),
|
||||
));
|
||||
}
|
||||
fs::DirBuilder::new()
|
||||
.mode(0o700)
|
||||
.create(&root)
|
||||
.map_err(|e| {
|
||||
Error(format!(
|
||||
"Create a new session directory {}: {e}",
|
||||
root.display()
|
||||
))
|
||||
})?;
|
||||
let runner = runner.map(regular).transpose()?;
|
||||
let guard = match fs::DirBuilder::new().mode(0o700).create(&root) {
|
||||
Ok(()) => lock(&root)?,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
let previous = Self::load(&root)?;
|
||||
if process(&root)?
|
||||
.as_ref()
|
||||
.map(|pid| exited(pid, 0))
|
||||
.transpose()?
|
||||
== Some(false)
|
||||
{
|
||||
return Err(Error(
|
||||
"Session still has a live process; stop it before starting again".into(),
|
||||
));
|
||||
}
|
||||
for name in ["qmp.sock", "console.sock"] {
|
||||
match UnixStream::connect(root.join(name)) {
|
||||
Ok(_) => {
|
||||
return Err(Error(
|
||||
"Session still has an active socket; stop it before starting again"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
Err(error)
|
||||
if matches!(
|
||||
error.kind(),
|
||||
std::io::ErrorKind::NotFound
|
||||
| std::io::ErrorKind::ConnectionRefused
|
||||
) =>
|
||||
{
|
||||
()
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
}
|
||||
// Keep the previous cartridge-to-overlay mapping as well as the
|
||||
// overlay files. A fresh boot starts with empty cartridge bays.
|
||||
let archive = root.join(format!(
|
||||
"previous-{}.json",
|
||||
image::hex(&image::random_id()?)
|
||||
));
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.mode(0o600)
|
||||
.open(archive)?;
|
||||
file.write_all(
|
||||
&serde_json::to_vec_pretty(&previous.state)
|
||||
.map_err(|e| Error(e.to_string()))?,
|
||||
)?;
|
||||
file.sync_all()?;
|
||||
for name in ["qmp.sock", "console.sock", "qemu.pid"] {
|
||||
match fs::remove_file(root.join(name)) {
|
||||
Ok(()) => (),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => (),
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
}
|
||||
previous._lock
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let session = Self {
|
||||
_lock: lock(&root)?,
|
||||
_lock: guard,
|
||||
root,
|
||||
state: State {
|
||||
format: 1,
|
||||
@@ -177,7 +270,7 @@ impl Session {
|
||||
kernel,
|
||||
initramfs,
|
||||
system,
|
||||
qemu_runner: runner.map(regular).transpose()?,
|
||||
qemu_runner: runner,
|
||||
cartridges: BTreeMap::new(),
|
||||
},
|
||||
};
|
||||
@@ -193,7 +286,7 @@ impl Session {
|
||||
.arg("-initrd").arg(&session.state.initramfs)
|
||||
.args(["-append","console=ttyAMA0 rdinit=/init ro quiet loglevel=3 fds.emulator=1","-blockdev"]).arg(block.to_string())
|
||||
.args(["-device","virtio-blk-pci,drive=system","-device","qemu-xhci,id=xhci,addr=05.0,p2=12,p3=12"])
|
||||
.stdin(Stdio::null()).stdout(Stdio::null()).stderr(File::create(session.root.join("qemu.log"))?);
|
||||
.stdin(Stdio::null()).stdout(Stdio::null()).stderr(OpenOptions::new().create(true).append(true).mode(0o600).custom_flags(libc::O_NOFOLLOW).open(session.root.join("qemu.log"))?);
|
||||
crate::success(&mut command)
|
||||
.map_err(|e| Error(format!("{e}; inspect {}/qemu.log", session.root.display())))?;
|
||||
let boot = (|| -> Result<()> {
|
||||
@@ -344,16 +437,26 @@ impl Session {
|
||||
}
|
||||
pub fn stop(&self, force: bool) -> Result<Value> {
|
||||
let mut qmp = self.qmp()?;
|
||||
if force {
|
||||
qmp.execute("quit", json!({}))?;
|
||||
let process = process(&self.root)?.ok_or_else(|| {
|
||||
Error("QEMU process record is unavailable; cannot confirm shutdown".into())
|
||||
})?;
|
||||
let response = if force {
|
||||
// QEMU may reset QMP before acknowledging quit. The pidfd,
|
||||
// rather than transport EOF or a reply, establishes actual exit.
|
||||
qmp.execute("quit", json!({})).map(|_| ())
|
||||
} else {
|
||||
let output =
|
||||
Serial::connect(&self.root, 120)?.command(&["fds".into(), "poweroff".into()])?;
|
||||
if output.status != 0 {
|
||||
return Err(Error(format!("Guest refused shutdown: {}", output.output)));
|
||||
}
|
||||
qmp.closed()
|
||||
};
|
||||
if !exited(&process, 30_000)? {
|
||||
return Err(response
|
||||
.err()
|
||||
.unwrap_or_else(|| Error("QEMU has not exited; session remains in use".into())));
|
||||
}
|
||||
qmp.closed()?;
|
||||
Ok(json!({"stopped":self.root,"forced":force,"logs_and_data_overlays_retained":true}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use std::{path::PathBuf, process::ExitCode};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
name = "fds-cartridge",
|
||||
version = env!("FDS_BUILD_VERSION"),
|
||||
about = "Build Void source packages and ready-to-run cartridge images on Linux"
|
||||
)]
|
||||
struct Cli {
|
||||
|
||||
Reference in New Issue
Block a user