update docs

This commit is contained in:
2026-09-22 13:23:34 +08:00
parent 99bc3d15c5
commit 8a4788fca8
126 changed files with 7198 additions and 2425 deletions
+4
View File
@@ -9,6 +9,10 @@ description = "FDS system and cartridge command interface"
name = "fds"
path = "src/main.rs"
[[bin]]
name = "fds-program"
path = "src/program.rs"
[dependencies]
clap.workspace = true
fds-burn = { path = "../fds-burn" }
+6
View File
@@ -159,6 +159,7 @@ fn inspect_manifest(path: &Path, json: bool) -> Result<()> {
}
fn cartridge(request: Request, json: bool) -> Result<()> {
let debug = matches!(request, Request::Topology);
let details = matches!(request, Request::Bay { .. });
let response = control::request(&request)?;
if json {
println!(
@@ -237,6 +238,11 @@ fn cartridge(request: Request, json: bool) -> Result<()> {
if let Some(mount) = bay.mount {
println!(" MOUNT {mount}");
}
if details {
for command in bay.commands {
println!(" FOREGROUND {}", command.alias);
}
}
if bay.consumers > 0 {
println!(" MANAGED PROCESSES {}", bay.consumers);
}
+142
View File
@@ -0,0 +1,142 @@
//! Foreground cartridge commands preserve terminal I/O and managed process tracking.
mod program_io;
use clap::{Parser, Subcommand};
use fds_common::{Error, Result, launch};
use std::{ffi::OsString, path::PathBuf, process::ExitCode};
// Parse argv[0] as a typed positional: Clap multicall uses file_stem(), which
// removes dotted software IDs. All options/arguments still go through Clap.
#[derive(Parser)]
#[command(no_binary_name = true, disable_help_flag = true)]
struct Invocation {
executable: PathBuf,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
arguments: Vec<OsString>,
}
#[derive(Parser)]
#[command(
name = "fds-program",
version,
disable_help_subcommand = true,
about = "Run a published cartridge command in the foreground"
)]
struct Explicit {
#[command(subcommand)]
command: Published,
}
#[derive(Subcommand)]
enum Published {
#[command(external_subcommand)]
Command(Vec<OsString>),
}
impl Invocation {
fn command(self) -> std::result::Result<(String, Vec<OsString>), clap::Error> {
let name = self
.executable
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| {
clap::Error::raw(
clap::error::ErrorKind::InvalidUtf8,
"Invalid cartridge command name",
)
})?
.to_owned();
if name == "fds-program" {
let parsed = Explicit::try_parse_from(
std::iter::once(self.executable.into_os_string()).chain(self.arguments),
)?;
let Published::Command(mut arguments) = parsed.command;
let command = arguments.remove(0).into_string().map_err(|_| {
clap::Error::raw(
clap::error::ErrorKind::InvalidUtf8,
"Invalid cartridge command name",
)
})?;
Ok((command, arguments))
} else {
Ok((name, self.arguments))
}
}
}
fn run() -> Result<i32> {
let (name, arguments) = Invocation::parse()
.command()
.unwrap_or_else(|error| error.exit());
if unsafe { libc::geteuid() } == 0 {
if unsafe { libc::setgroups(0, std::ptr::null()) } < 0
|| unsafe { libc::setgid(1000) } < 0
|| unsafe { libc::setuid(1000) } < 0
{
return Err(std::io::Error::last_os_error().into());
}
}
let io = program_io::Io::new()?;
let arguments = arguments
.into_iter()
.map(|value| {
value
.into_string()
.map_err(|_| Error("Program arguments must be UTF-8".into()))
})
.collect::<Result<Vec<_>>>()?;
let (socket, child) = launch::start(
&name,
arguments,
io.terminal(),
std::env::current_dir()?.display().to_string(),
std::env::var("TERM").unwrap_or_else(|_| "linux".into()),
&io.descriptors(),
)?;
io.wait(socket, child)
}
fn main() -> ExitCode {
match run() {
Ok(status) => ExitCode::from(if libc::WIFEXITED(status) {
libc::WEXITSTATUS(status) as u8
} else {
(128 + libc::WTERMSIG(status)) as u8
}),
Err(error) => {
eprintln!("fds-program: {error}");
ExitCode::from(126)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn clap_preserves_dotted_aliases_and_application_options() {
<Invocation as CommandFactory>::command().debug_assert();
Explicit::command().debug_assert();
for name in ["hello", "hello.world", "b01:demo.report:shell"] {
let (command, arguments) = Invocation::try_parse_from([
format!("/run/fds/bin/{name}"),
"--help".into(),
"two words".into(),
])
.unwrap()
.command()
.unwrap();
assert_eq!(command, name);
assert_eq!(arguments, ["--help", "two words"]);
}
let (command, arguments) =
Invocation::try_parse_from(["fds-program", "hello", "--version"])
.unwrap()
.command()
.unwrap();
assert_eq!(command, "hello");
assert_eq!(arguments, ["--version"]);
assert_eq!(
Invocation::try_parse_from(["fds-program", "--help"])
.unwrap()
.command()
.unwrap_err()
.kind(),
clap::error::ErrorKind::DisplayHelp
);
}
}
+345
View File
@@ -0,0 +1,345 @@
//! Terminal proxy for foreground programs whose privileged parent is the service.
use fds_common::{
Error, Result,
control::{LIMIT, Response},
};
use std::{
io::{Read, Write},
os::{
fd::{AsRawFd, FromRawFd, OwnedFd},
unix::net::UnixStream,
},
};
pub struct Io {
master: Option<OwnedFd>,
slave: Option<OwnedFd>,
terminal: Option<libc::termios>,
signals: OwnedFd,
old_mask: libc::sigset_t,
pending: std::collections::VecDeque<u8>,
}
fn checked(result: i32) -> Result<i32> {
if result < 0 {
Err(std::io::Error::last_os_error().into())
} else {
Ok(result)
}
}
impl Io {
pub fn new() -> Result<Self> {
let mut mask = unsafe { std::mem::zeroed::<libc::sigset_t>() };
let mut old_mask = unsafe { std::mem::zeroed::<libc::sigset_t>() };
unsafe {
libc::sigemptyset(&mut mask);
for signal in [
libc::SIGINT,
libc::SIGTERM,
libc::SIGHUP,
libc::SIGQUIT,
libc::SIGWINCH,
libc::SIGTSTP,
libc::SIGCONT,
] {
libc::sigaddset(&mut mask, signal);
}
checked(libc::sigprocmask(libc::SIG_BLOCK, &mask, &mut old_mask))?;
}
let fd = unsafe { libc::signalfd(-1, &mask, libc::SFD_CLOEXEC | libc::SFD_NONBLOCK) };
if fd < 0 {
let error = std::io::Error::last_os_error();
unsafe {
libc::sigprocmask(libc::SIG_SETMASK, &old_mask, std::ptr::null_mut());
}
return Err(error.into());
}
let signals = unsafe { OwnedFd::from_raw_fd(fd) };
let mut io = Self {
master: None,
slave: None,
terminal: None,
signals,
old_mask,
pending: Default::default(),
};
if unsafe { libc::isatty(0) == 1 && libc::isatty(1) == 1 } {
let mut term = unsafe { std::mem::zeroed::<libc::termios>() };
let mut size = unsafe { std::mem::zeroed::<libc::winsize>() };
checked(unsafe { libc::tcgetattr(0, &mut term) })?;
checked(unsafe { libc::ioctl(0, libc::TIOCGWINSZ, &mut size) })?;
let (mut master, mut slave) = (-1, -1);
checked(unsafe {
libc::openpty(&mut master, &mut slave, std::ptr::null_mut(), &term, &size)
})?;
io.master = Some(unsafe { OwnedFd::from_raw_fd(master) });
io.slave = Some(unsafe { OwnedFd::from_raw_fd(slave) });
for fd in [master, slave] {
checked(unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) })?;
}
checked(unsafe { libc::fcntl(master, libc::F_SETFL, libc::O_NONBLOCK) })?;
io.terminal = Some(term);
io.raw()?;
}
Ok(io)
}
pub fn terminal(&self) -> bool {
self.terminal.is_some()
}
pub fn descriptors(&self) -> [i32; 3] {
if let Some(slave) = &self.slave {
[
slave.as_raw_fd(),
slave.as_raw_fd(),
if unsafe { libc::isatty(2) } == 1 {
slave.as_raw_fd()
} else {
2
},
]
} else {
[0, 1, 2]
}
}
fn raw(&self) -> Result<()> {
if let Some(term) = self.terminal {
let mut raw = term;
unsafe {
libc::cfmakeraw(&mut raw);
}
raw.c_lflag |= libc::ISIG;
checked(unsafe { libc::tcsetattr(0, libc::TCSANOW, &raw) })?;
}
Ok(())
}
fn restore(&self) {
if let Some(term) = self.terminal {
unsafe {
libc::tcsetattr(0, libc::TCSANOW, &term);
}
}
}
fn write_terminal(&mut self, bytes: &[u8]) -> Result<()> {
if self.pending.len() + bytes.len() > 65536 {
return Err(Error("Terminal input queue exceeded its limit".into()));
}
self.pending.extend(bytes);
Ok(())
}
fn flush_input(&mut self) -> Result<()> {
let Some(master) = &self.master else {
return Ok(());
};
let bytes = self.pending.as_slices().0;
if bytes.is_empty() {
return Ok(());
}
let count = unsafe { libc::write(master.as_raw_fd(), bytes.as_ptr().cast(), bytes.len()) };
if count > 0 {
self.pending.drain(..count as usize);
} else if count < 0 {
let error = std::io::Error::last_os_error();
if !matches!(
error.kind(),
std::io::ErrorKind::Interrupted | std::io::ErrorKind::WouldBlock
) {
return Err(error.into());
}
}
Ok(())
}
fn output(&self) -> Result<bool> {
let Some(master) = &self.master else {
return Ok(false);
};
loop {
let mut buffer = [0u8; 8192];
let count =
unsafe { libc::read(master.as_raw_fd(), buffer.as_mut_ptr().cast(), buffer.len()) };
if count > 0 {
std::io::stdout().write_all(&buffer[..count as usize])?;
continue;
}
if count == 0 {
return Ok(false);
}
let error = std::io::Error::last_os_error();
if error.kind() == std::io::ErrorKind::Interrupted {
continue;
}
if error.raw_os_error() == Some(libc::EIO) {
return Ok(false);
}
if error.kind() == std::io::ErrorKind::WouldBlock {
return Ok(true);
}
return Err(error.into());
}
}
pub fn wait(mut self, mut socket: UnixStream, child: OwnedFd) -> Result<i32> {
self.slave.take();
let mut bytes = Vec::new();
let mut input = self.master.is_some();
let mut output = self.master.is_some();
loop {
let mut fds = [
libc::pollfd {
fd: socket.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: self.signals.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: if input && self.pending.len() < 32768 {
0
} else {
-1
},
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: if output {
self.master.as_ref().unwrap().as_raw_fd()
} else {
-1
},
events: libc::POLLIN
| if self.pending.is_empty() {
0
} else {
libc::POLLOUT
},
revents: 0,
},
];
let result = unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as _, -1) };
if result < 0
&& std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted
{
continue;
}
checked(result)?;
if fds[3].revents & (libc::POLLIN | libc::POLLHUP | libc::POLLERR) != 0 {
output = self.output()?;
}
if output && fds[3].revents & libc::POLLOUT != 0 {
self.flush_input()?;
}
if fds[2].revents != 0 {
let mut buffer = [0u8; 4096];
let count = unsafe { libc::read(0, buffer.as_mut_ptr().cast(), buffer.len()) };
if count > 0 {
self.write_terminal(&buffer[..count as usize])?;
} else if count == 0 {
input = false;
} else if std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted
{
return Err(std::io::Error::last_os_error().into());
}
}
if fds[1].revents != 0 {
let mut info = unsafe { std::mem::zeroed::<libc::signalfd_siginfo>() };
if unsafe {
libc::read(
self.signals.as_raw_fd(),
(&mut info as *mut libc::signalfd_siginfo).cast(),
std::mem::size_of_val(&info),
)
} > 0
{
let signal = info.ssi_signo as i32;
if signal == libc::SIGWINCH {
if let Some(master) = &self.master {
let mut size = unsafe { std::mem::zeroed::<libc::winsize>() };
if unsafe { libc::ioctl(0, libc::TIOCGWINSZ, &mut size) } == 0 {
unsafe {
libc::ioctl(master.as_raw_fd(), libc::TIOCSWINSZ, &size);
}
}
}
} else if signal == libc::SIGTSTP {
self.restore();
if let Some(master) = &self.master {
let group = unsafe { libc::tcgetpgrp(master.as_raw_fd()) };
if group > 0 {
unsafe {
libc::kill(-group, libc::SIGSTOP);
}
}
}
unsafe {
libc::kill(libc::getpid(), libc::SIGSTOP);
}
self.raw()?;
if let Some(master) = &self.master {
let group = unsafe { libc::tcgetpgrp(master.as_raw_fd()) };
if group > 0 {
unsafe {
libc::kill(-group, libc::SIGCONT);
}
}
}
} else if self.master.is_some()
&& matches!(signal, libc::SIGINT | libc::SIGQUIT)
{
let index = if signal == libc::SIGINT {
libc::VINTR
} else {
libc::VQUIT
};
self.write_terminal(&[self.terminal.as_ref().unwrap().c_cc[index]])?;
} else if signal != libc::SIGCONT {
unsafe {
libc::syscall(
libc::SYS_pidfd_send_signal,
child.as_raw_fd(),
signal,
std::ptr::null::<libc::siginfo_t>(),
0,
);
}
}
}
}
if fds[0].revents != 0 {
let mut chunk = [0u8; 4096];
loop {
match socket.read(&mut chunk) {
Ok(0) => {
let _ = self.output()?;
let reply: Response = serde_json::from_slice(&bytes)
.map_err(|e| Error(format!("Invalid program completion: {e}")))?;
if let Some(error) = reply.error {
return Err(Error(error));
}
return reply
.exit_status
.ok_or_else(|| Error("Missing program exit status".into()));
}
Ok(n) => {
bytes.extend_from_slice(&chunk[..n]);
if bytes.len() > LIMIT {
return Err(Error("Program response is too large".into()));
}
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
}
}
}
}
}
}
impl Drop for Io {
fn drop(&mut self) {
self.restore();
unsafe {
libc::sigprocmask(libc::SIG_SETMASK, &self.old_mask, std::ptr::null_mut());
}
}
}