update docs
This commit is contained in:
@@ -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" }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user