From 4225806394ddef0ad264ab10256210e1c3cbfe19 Mon Sep 17 00:00:00 2001 From: Mark Poliakov Date: Thu, 27 Jul 2023 16:25:26 +0300 Subject: [PATCH] init: add some initialization stuff --- Cargo.lock | 121 +++++++++++++++++++++ Cargo.toml | 10 +- abi | 1 + build.sh | 24 ++++- etc/inittab | 3 + etc/rc.d/00-mount | 1 + etc/rc.d/99-motd | 1 + init/Cargo.toml | 9 ++ init/src/main.rs | 189 +++++++++++++++++++++++++------- init/src/rc.rs | 103 ++++++++++++++++++ rt | 1 + rust-toolchain.toml | 1 - shell/Cargo.toml | 9 ++ shell/src/expr.rs | 204 +++++++++++++++++++++++++++++++++++ shell/src/main.rs | 245 ++++++++++++++++++++++++++++++++++++++++++ shell/src/parse.rs | 241 +++++++++++++++++++++++++++++++++++++++++ sysutils/Cargo.toml | 18 ++++ sysutils/src/login.rs | 96 +++++++++++++++++ sysutils/src/mount.rs | 44 ++++++++ 19 files changed, 1280 insertions(+), 41 deletions(-) create mode 120000 abi create mode 100644 etc/inittab create mode 100644 etc/rc.d/00-mount create mode 100644 etc/rc.d/99-motd create mode 100644 init/src/rc.rs create mode 120000 rt create mode 100644 shell/Cargo.toml create mode 100644 shell/src/expr.rs create mode 100644 shell/src/main.rs create mode 100644 shell/src/parse.rs create mode 100644 sysutils/Cargo.toml create mode 100644 sysutils/src/login.rs create mode 100644 sysutils/src/mount.rs diff --git a/Cargo.lock b/Cargo.lock index 8cddee5b..6d8d26d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,127 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "clap" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_derive" +version = "4.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "init" version = "0.1.0" +dependencies = [ + "yggdrasil-rt", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "shell" +version = "0.1.0" +dependencies = [ + "yggdrasil-rt", +] + +[[package]] +name = "syn" +version = "2.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sysutils" +version = "0.1.0" +dependencies = [ + "clap", + "yggdrasil-rt", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "yggdrasil-abi" +version = "0.1.0" + +[[package]] +name = "yggdrasil-rt" +version = "0.1.0" +dependencies = [ + "yggdrasil-abi", +] diff --git a/Cargo.toml b/Cargo.toml index af52998f..2b8de832 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,13 @@ [workspace] resolver = "1" members = [ - "init" + "init", + "shell", + "sysutils" ] + +[patch.'https://git.alnyan.me/yggdrasil/yggdrasil-abi.git'] +yggdrasil-abi = { path = "abi" } + +[patch.'https://git.alnyan.me/yggdrasil/yggdrasil-rt.git'] +yggdrasil-rt = { path = "rt" } diff --git a/abi b/abi new file mode 120000 index 00000000..56d6bf5f --- /dev/null +++ b/abi @@ -0,0 +1 @@ +../abi \ No newline at end of file diff --git a/build.sh b/build.sh index ffdcd317..23dba45a 100755 --- a/build.sh +++ b/build.sh @@ -34,16 +34,32 @@ check_toolchain() { } pack_initrd() { + local workspace_dir=$(pwd) local build_dir=target/${USER_TARGET}/${PROFILE} local root_dir=${build_dir}/rootfs - mkdir -p "${root_dir}" + mkdir -p "${root_dir}" + mkdir -p "${root_dir}/sbin" + mkdir -p "${root_dir}/bin" + + # init cp ${build_dir}/init ${root_dir} + cp ${build_dir}/rc ${root_dir}/sbin/ + + # shell + cp ${build_dir}/shell ${root_dir}/bin/sh + + # sysutils + cp ${build_dir}/mount ${root_dir}/sbin/ + cp ${build_dir}/login ${root_dir}/sbin/ + + cp -r ${workspace_dir}/etc ${root_dir}/ cd "${root_dir}" mkdir -p dev + touch dev/.do_not_remove - tar cf ../initrd.tar `find . -printf "%P\n"` + tar cf ../initrd.tar `find . -type f -printf "%P\n"` cd - } @@ -51,6 +67,10 @@ check_toolchain case "$1" in build|"") + if [ "${CLEAN_BUILD}" = 1 ]; then + cargo clean + fi + pstatus "Building userspace programs" cargo +ygg-stage1 build ${USER_CARGO_OPTS} diff --git a/etc/inittab b/etc/inittab new file mode 100644 index 00000000..970e4c55 --- /dev/null +++ b/etc/inittab @@ -0,0 +1,3 @@ +init:1:wait:/sbin/rc default + +user:1:once:/sbin/login /dev/ttyS0 diff --git a/etc/rc.d/00-mount b/etc/rc.d/00-mount new file mode 100644 index 00000000..7e756543 --- /dev/null +++ b/etc/rc.d/00-mount @@ -0,0 +1 @@ +!(~/sbin/mount nil "/dev" "devfs") diff --git a/etc/rc.d/99-motd b/etc/rc.d/99-motd new file mode 100644 index 00000000..31772369 --- /dev/null +++ b/etc/rc.d/99-motd @@ -0,0 +1 @@ +(echo "TODO: message of the day") diff --git a/init/Cargo.toml b/init/Cargo.toml index f922052a..86e8e56e 100644 --- a/init/Cargo.toml +++ b/init/Cargo.toml @@ -5,4 +5,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "init" +path = "src/main.rs" + +[[bin]] +name = "rc" +path = "src/rc.rs" + [dependencies] +yggdrasil-rt = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-rt.git" } diff --git a/init/src/main.rs b/init/src/main.rs index 36519af5..0a02ea66 100644 --- a/init/src/main.rs +++ b/init/src/main.rs @@ -1,46 +1,161 @@ -#![feature(rustc_private)] +use std::{ + fmt, + fs::File, + io::{BufRead, BufReader}, + path::Path, + process::{Command, ExitCode}, + str::FromStr, +}; -use std::os::yggdrasil::{mount, open, OpenOptions, FileMode, MountOptions}; -use std::borrow::Cow; -use std::path::{Path, PathBuf}; +use yggdrasil_rt::debug_trace; -fn ls_tree>(path: P, depth: usize) -> std::io::Result<()> { - for entry in std::fs::read_dir(path)? { - let entry = entry?; - let os_filename = entry.file_name(); - let filename = os_filename.to_str().unwrap(); - let path = entry.path(); +const INITTAB_PATH: &str = "/etc/inittab"; - if filename.starts_with('.') { +pub enum InitError { + IoError(std::io::Error), + OsError(yggdrasil_rt::Error), + CustomError(String), +} + +impl From for InitError { + fn from(value: std::io::Error) -> Self { + Self::IoError(value) + } +} + +impl From for InitError { + fn from(value: yggdrasil_rt::Error) -> Self { + Self::OsError(value) + } +} + +impl fmt::Debug for InitError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::IoError(e) => fmt::Debug::fmt(e, f), + Self::OsError(e) => fmt::Debug::fmt(e, f), + Self::CustomError(e) => fmt::Debug::fmt(e, f), + } + } +} + +#[derive(Debug, PartialEq)] +pub enum RuleAction { + Wait, + Once, + Boot, +} + +#[derive(Debug)] +pub struct Rule { + #[allow(unused)] + id: String, + // runlevels ignored here + action: RuleAction, + program: String, + arguments: Vec, +} + +impl Rule { + pub fn run(&self) -> Result<(), InitError> { + let arguments: Vec<_> = self.arguments.iter().map(String::as_str).collect(); + let mut child = match self.action { + RuleAction::Wait | RuleAction::Once => { + Command::new(&self.program).args(&arguments).spawn()? + } + RuleAction::Boot => todo!(), + }; + + if self.action == RuleAction::Wait { + child.wait()?; + } + + Ok(()) + } +} + +impl FromStr for RuleAction { + type Err = InitError; + + fn from_str(s: &str) -> Result { + match s { + "wait" => Ok(Self::Wait), + "once" => Ok(Self::Once), + "boot" => Ok(Self::Boot), + _ => Err(InitError::CustomError(format!( + "Unrecognized rule action: {:?}", + s + ))), + } + } +} + +fn parse_rule(line: &str) -> Result { + let terms: Vec<_> = line.split(':').collect(); + if terms.len() != 4 { + return Err(InitError::CustomError(format!( + "Expected 4 terms in init rule, got {}", + terms.len() + ))); + } + + let id = terms[0].to_owned(); + // Runlevel (term 1) ignored + let action = RuleAction::from_str(&terms[2])?; + + let command_terms: Vec<_> = terms[3].split(' ').collect(); + let program = command_terms[0].to_owned(); + let arguments = command_terms + .into_iter() + .skip(1) + .map(str::to_owned) + .collect(); + + Ok(Rule { + id, + action, + program, + arguments, + }) +} + +fn load_rules>(path: P) -> Result, InitError> { + let file = BufReader::new(File::open(path)?); + let mut rules = vec![]; + + for line in file.lines() { + let line = line?; + let line = line.trim(); + let (line, _) = line.split_once('#').unwrap_or((line, "")); + + if line.is_empty() { continue; } - for _ in 0..depth { - print!(" "); - } - println!("{:?}", filename); - - if entry.file_type().unwrap().is_dir() { - ls_tree(path, depth + 1); - } - } - Ok(()) -} - -fn main() { - unsafe { - mount(MountOptions { - source: None, - filesystem: Some("devfs"), - target: "/dev", - }) - .unwrap(); - - // Open stdin/stdout/stderr - open("/dev/ttyS0", OpenOptions::READ, FileMode::empty()).unwrap(); - open("/dev/ttyS0", OpenOptions::WRITE, FileMode::empty()).unwrap(); - open("/dev/ttyS0", OpenOptions::WRITE, FileMode::empty()).unwrap(); + rules.push(parse_rule(&line)?); } - ls_tree("/", 0).ok(); + Ok(rules) +} + +fn main() -> ExitCode { + debug_trace!("Userspace init starting"); + + let rules = match load_rules(INITTAB_PATH) { + Ok(s) => s, + Err(e) => { + debug_trace!("init: failed to load rules: {:?}", e); + return ExitCode::FAILURE; + } + }; + + debug_trace!("Rules loaded"); + + for rule in rules { + if let Err(err) = rule.run() { + debug_trace!("rc: failed to execute rule {:?}: {:?}", rule, err); + } + } + + loop {} } diff --git a/init/src/rc.rs b/init/src/rc.rs new file mode 100644 index 00000000..9111a0ca --- /dev/null +++ b/init/src/rc.rs @@ -0,0 +1,103 @@ +use std::{ + env, + fs::read_dir, + path::{Path, PathBuf}, + process::{Command, ExitCode}, + str::FromStr, +}; + +const RC_DIR: &str = "/etc/rc.d"; + +#[derive(Debug)] +enum Error { + IncorrectUsage(String), + IoError(std::io::Error), +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Self::IoError(value) + } +} + +#[derive(Debug)] +enum Mode { + Default, +} + +impl FromStr for Mode { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "default" => Ok(Self::Default), + _ => Err(Error::IncorrectUsage(format!("Incorrect mode: {:?}", s))), + } + } +} + +fn exec_script>(path: P, arg: &str) -> Result<(), Error> { + let path = path.as_ref(); + yggdrasil_rt::debug_trace!("rc: {:?} {}", path, arg); + + // TODO run those in parallel, if allowed + // TODO binfmt guessing + let mut process = Command::new("/bin/sh").arg(path).arg(arg).spawn()?; + + if !process.wait()?.success() { + eprintln!("{:?}: Failed", path); + } + + Ok(()) +} + +fn get_scripts>(dir: P) -> Result, Error> { + let mut items = read_dir(dir)? + .map(|item| item.map(|item| item.file_name().to_str().unwrap().to_owned())) + .collect::, _>>()?; + items.sort(); + Ok(items) +} + +fn _main(mode: &str) -> Result<(), Error> { + // TODO only 1 mode supported, so ignore it + let _mode = Mode::from_str(mode)?; + + let path = PathBuf::from(RC_DIR); + let scripts = get_scripts(&path)?; + + // Execute scripts in order + for script in scripts { + if let Err(err) = exec_script(path.join(&script), "start") { + eprintln!("{}: {:?}", script, err); + } + } + + Ok(()) +} + +fn usage(_cmd: &str) { + eprintln!("Usage: ..."); +} + +fn main() -> ExitCode { + let args: Vec<_> = env::args().collect(); + if args.len() != 2 { + eprintln!("Incorrect rc usage, expected 2 args, got {}", args.len()); + usage(&args[0]); + return ExitCode::FAILURE; + } + + match _main(&args[1]) { + Ok(_) => ExitCode::SUCCESS, + Err(Error::IncorrectUsage(e)) => { + eprintln!("rc: incorrect usage: {:?}", e); + usage(&args[0]); + ExitCode::FAILURE + } + Err(e) => { + eprintln!("rc: {:?}", e); + ExitCode::FAILURE + } + } +} diff --git a/rt b/rt new file mode 120000 index 00000000..5061b3e0 --- /dev/null +++ b/rt @@ -0,0 +1 @@ +../../sandbox/yggdrasil-rust/yggdrasil-rt \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 124521f2..90ff040f 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,2 @@ [toolchain] channel = "ygg-stage1" -targets = ["aarch64-unknown-yggdrasil"] diff --git a/shell/Cargo.toml b/shell/Cargo.toml new file mode 100644 index 00000000..8433eaea --- /dev/null +++ b/shell/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "shell" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +yggdrasil-rt = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-rt.git" } diff --git a/shell/src/expr.rs b/shell/src/expr.rs new file mode 100644 index 00000000..27cba652 --- /dev/null +++ b/shell/src/expr.rs @@ -0,0 +1,204 @@ +use std::{collections::LinkedList, fmt, rc::Rc}; + +#[derive(Default, Debug, Clone, Copy)] +pub struct Position { + pub line: u64, + pub column: u64, +} + +#[derive(Debug)] +pub enum ExprValue { + List(LinkedList>), + Nil, + Ident(String), + StringLiteral(String), + IntLiteral(i64), + Quote(Rc), + Meta(char, Rc), + Error(String), +} + +#[derive(Debug)] +pub struct Expr { + pub value: ExprValue, + pub position: Option, +} + +pub struct ExprPrinter<'a> { + expr: &'a Expr, + ellipsisize_lists: bool, + pretty: bool, +} + +impl Expr { + pub fn nil() -> Rc { + Rc::new(Self { + value: ExprValue::Nil, + position: None, + }) + } + + pub fn ident>(s: S) -> Rc { + Rc::new(Self { + value: ExprValue::Ident(s.into()), + position: None, + }) + } + + pub fn str>(s: S) -> Rc { + Rc::new(Self { + value: ExprValue::StringLiteral(s.into()), + position: None, + }) + } + + pub fn t() -> Rc { + Rc::new(Self { + value: ExprValue::IntLiteral(1), + position: None, + }) + } + + pub fn quote(expr: Rc) -> Rc { + Rc::new(Self { + value: ExprValue::Quote(expr), + position: None, + }) + } + + pub fn meta(m: char, expr: Rc) -> Rc { + Rc::new(Self { + value: ExprValue::Meta(m, expr), + position: None, + }) + } + + pub fn list(list: LinkedList>) -> Rc { + Rc::new(Self { + value: ExprValue::List(list), + position: None, + }) + } + + pub fn error>(s: S) -> Rc { + Rc::new(Self { + value: ExprValue::Error(s.into()), + position: None, + }) + } + + pub fn is_nil(&self) -> bool { + match &self.value { + ExprValue::Nil => true, + ExprValue::List(e) if e.is_empty() => true, + _ => false, + } + } + + pub fn is_list(&self) -> bool { + match &self.value { + ExprValue::Nil => true, + ExprValue::List(_) => true, + _ => false, + } + } + + pub fn as_string(&self) -> String { + match &self.value { + ExprValue::Nil => "".to_owned(), + ExprValue::List(items) => items.iter().fold(String::new(), |acc, i| { + if acc.is_empty() { + i.as_string() + } else { + acc + " " + &i.as_string() + } + }), + ExprValue::Meta(_, s) => s.as_string(), + ExprValue::IntLiteral(s) => format!("{}", s), + ExprValue::StringLiteral(s) | ExprValue::Ident(s) => s.clone(), + ExprValue::Error(e) => format!("(error {:?})", e), + ExprValue::Quote(_) => todo!(), + } + } + + pub fn as_error(&self) -> Option<&str> { + match &self.value { + ExprValue::Error(e) => Some(e), + _ => None, + } + } +} + +impl<'a> ExprPrinter<'a> { + pub fn new(expr: &'a Expr) -> Self { + Self { + expr, + ellipsisize_lists: false, + pretty: false, + } + } + + pub fn ellipsisize_lists(&mut self) -> &mut Self { + self.ellipsisize_lists = true; + self + } + + pub fn pretty(&mut self) -> &mut Self { + self.pretty = true; + self + } + + fn print_node(&self, f: &mut fmt::Formatter<'_>, node: &Expr, depth: usize) -> fmt::Result { + if node.is_nil() { + f.write_str("nil") + } else if node.is_list() { + f.write_str("(")?; + + if depth >= 2 && self.ellipsisize_lists { + f.write_str("...")?; + } else { + // Non-empty list + if let ExprValue::List(items) = &node.value { + for (i, item) in items.iter().enumerate() { + if i != 0 { + f.write_str(" ")?; + } + self.print_node(f, item, depth + 1)?; + } + } + } + + f.write_str(")") + } else { + match &node.value { + ExprValue::Ident(name) => f.write_str(&name), + ExprValue::Meta(q, expr) => { + write!(f, "{}", q)?; + self.print_node(f, expr, depth) + } + ExprValue::IntLiteral(value) => write!(f, "{}", value), + ExprValue::StringLiteral(value) => { + if self.pretty { + write!(f, "{}", value) + } else { + write!(f, "{:?}", value) + } + }, + ExprValue::Quote(expr) => { + f.write_str("'")?; + self.print_node(f, expr, depth) + } + ExprValue::Error(s) => { + write!(f, "(error {:?})", s) + } + _ => todo!(), + } + } + } +} + +impl<'a> fmt::Display for ExprPrinter<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.print_node(f, self.expr, 0) + } +} diff --git a/shell/src/main.rs b/shell/src/main.rs new file mode 100644 index 00000000..5fb8741b --- /dev/null +++ b/shell/src/main.rs @@ -0,0 +1,245 @@ +#![feature(linked_list_cursors)] +use std::{ + collections::{linked_list::Cursor, HashMap, LinkedList}, + env, + fs::File, + io::BufReader, + process::{Command, ExitCode}, + rc::Rc, +}; + +use expr::{Expr, ExprPrinter, ExprValue}; +use parse::{Input, InputError, Parser}; + +use yggdrasil_rt::{ + io::{DeviceRequest, RawFd}, + sys, +}; + +pub mod expr; +pub mod parse; + +#[derive(Debug)] +pub enum EvalError { + UndefinedIdent(String), + MetaError(Rc, String), +} + +#[derive(Debug)] +pub enum ScriptError { + Input(InputError), + Eval(EvalError), +} + +impl From for ScriptError { + fn from(e: InputError) -> Self { + Self::Input(e) + } +} + +impl From for ScriptError { + fn from(e: EvalError) -> Self { + Self::Eval(e) + } +} + +#[derive(Clone)] +pub enum Function { + Script(LinkedList, Rc), + Native(Rc>) -> Result, EvalError>>), +} + +pub struct Shell { + parser: Parser, + // TODO variable substitution + #[allow(unused)] + variables: HashMap, + functions: HashMap, +} + +impl Shell { + pub fn new, I: IntoIterator>(input: R, args: I) -> Self { + let parser = Parser::new(input.into()); + let variables = Self::init_variables(args); + let functions = Self::init_functions(); + Self { + parser, + variables, + functions, + } + } + + fn init_variables>(args: I) -> HashMap { + let mut vars = HashMap::new(); + for (i, arg) in args.into_iter().enumerate() { + vars.insert(i.to_string(), arg); + } + vars + } + + fn init_functions() -> HashMap { + // Add builtins + let mut funcs = HashMap::new(); + funcs.insert("echo".to_string(), Function::Native(Rc::new(builtin_echo))); + funcs + } + + fn eval_command( + &mut self, + program: &str, + mut args: Cursor<'_, Rc>, + _capture_output: bool, + ) -> Result, EvalError> { + let mut str_args = vec![]; + + while let Some(arg) = args.current() { + let arg = self.eval(arg)?; + str_args.push(arg.as_string()); + args.move_next(); + } + + let mut process = match Command::new(program).args(str_args).spawn() { + Ok(p) => p, + Err(e) => { + return Ok(Expr::error(format!("{}: {}", program, e.to_string()))); + } + }; + + let status = process.wait().unwrap(); + + if status.success() { + Ok(Expr::t()) + } else { + Ok(Expr::nil()) + } + } + + fn eval_call(&mut self, list: &LinkedList>) -> Result, EvalError> { + let Some(head) = list.front() else { + todo!(); + }; + + let ExprValue::Ident(head) = &head.as_ref().value else { + todo!(); + }; + + let mut cursor = list.cursor_front(); + cursor.move_next(); + + if head.starts_with('~') { + // Execute external command + let head = head.trim_start_matches('~'); + self.eval_command(head, cursor, false) + } else { + let Some(func) = self.functions.get(head).cloned() else { + return Err(EvalError::UndefinedIdent(head.clone())); + }; + // func.eval(self, cursor) + match func { + Function::Native(f) => f(self, cursor), + _ => todo!(), + } + } + } + + fn eval(&mut self, expr: &Rc) -> Result, EvalError> { + match &expr.as_ref().value { + ExprValue::List(list) => self.eval_call(list), + ExprValue::Ident(name) => todo!("Ident: {:?}", name), + ExprValue::StringLiteral(_) | ExprValue::IntLiteral(_) | ExprValue::Nil => { + Ok(expr.clone()) + } + ExprValue::Quote(q) => Ok(q.clone()), + ExprValue::Error(_) => todo!(), + ExprValue::Meta('!', q) => { + let expr = self.eval(q)?; + if expr.is_nil() { + Err(EvalError::MetaError(q.clone(), "unexpected nil".to_owned())) + } else if let Some(err) = expr.as_error() { + Err(EvalError::MetaError(q.clone(), err.to_owned())) + } else { + Ok(expr) + } + } + ExprValue::Meta(_, _) => todo!(), + } + } + + pub fn run(mut self) -> Result { + loop { + let Some(expr) = self.parser.parse()? else { + break; + }; + + self.eval(&expr)?; + } + + Ok(ExitCode::SUCCESS) + } +} + +fn builtin_echo(shell: &mut Shell, mut args: Cursor<'_, Rc>) -> Result, EvalError> { + while let Some(arg) = args.current() { + let arg = shell.eval(arg)?; + print!("{}", ExprPrinter::new(&arg).pretty()); + + if args.peek_next().is_some() { + print!(" "); + } + args.move_next(); + } + println!(); + Ok(Expr::nil()) +} + +fn print_eval_error(e: &EvalError) { + match e { + EvalError::MetaError(e, msg) => { + let pos_string = if let Some(pos) = e.position { + format!("{},{}", pos.line + 1, pos.column) + } else { + "[...]".to_owned() + }; + eprintln!("{}: expression failed in:", pos_string); + let mut p = ExprPrinter::new(e); + p.ellipsisize_lists(); + eprintln!(" {}", p); + eprintln!(": {}", msg); + } + _ => todo!(), + } +} + +fn main() -> ExitCode { + let script_name = env::args().skip(1).next(); + + let input = if let Some(script_name) = script_name { + let file = File::open(&script_name).unwrap(); + Input::from(BufReader::new(file)) + } else { + // Interactive + // Gain control of the terminal signals + let pid = std::process::id(); + unsafe { + sys::set_process_group_id(pid, pid).unwrap(); + let mut req = DeviceRequest::SetTerminalGroup(pid); + sys::device_request(RawFd::STDIN, &mut req).unwrap(); + } + + Input::from(std::io::stdin()) + }; + + match Shell::new(input, env::args()).run() { + Ok(res) => res, + Err(ScriptError::Eval(e)) => { + eprintln!("Script error:"); + print_eval_error(&e); + ExitCode::FAILURE + } + Err(ScriptError::Input(e)) => { + eprintln!("Could not load the script:"); + eprintln!("{:?}", e); + ExitCode::FAILURE + } + } +} diff --git a/shell/src/parse.rs b/shell/src/parse.rs new file mode 100644 index 00000000..9867fbd9 --- /dev/null +++ b/shell/src/parse.rs @@ -0,0 +1,241 @@ +use std::{fs::File, io::{Read, BufReader, Stdin}, rc::Rc, collections::LinkedList}; + +use crate::expr::{Position, Expr, ExprValue}; + +trait CharExt { + fn is_ident0(self) -> bool; + fn is_ident1(self) -> bool; +} + +const IDENT_CHARS: &str = ":+-*/%$=!_-~"; + +impl CharExt for char { + fn is_ident0(self) -> bool { + self.is_ascii_alphabetic() || IDENT_CHARS.contains(self) + } + + fn is_ident1(self) -> bool { + self.is_ascii_alphanumeric() || IDENT_CHARS.contains(self) + } +} + +#[derive(Debug)] +pub enum InputError { + IoError(std::io::Error), + Eof, + ParseError(String) +} + +enum InputInner { + File(BufReader), + Stdin(Stdin) +} + +pub struct Input { + position: Position, + inner: InputInner, + buffer: Option, +} + +pub struct Parser { + input: Input +} + +impl Parser { + pub fn new(input: Input) -> Self { + Self { input } + } + + fn skip_whitespace(&mut self) -> Result<(), InputError> { + while let Some(c) = self.input.peek()? { + if c.is_ascii_whitespace() { + self.input.next()?; + } else { + break; + } + } + Ok(()) + } + + fn parse_list(&mut self, p: Position) -> Result, InputError> { + let mut buf = LinkedList::new(); + + loop { + self.skip_whitespace()?; + + let Some(c) = self.input.peek()? else { + return Err(InputError::Eof); + }; + + if c == ')' { + self.input.next()?.unwrap(); + break; + } + + let Some(item) = self.parse()? else { + return Err(InputError::Eof); + }; + + buf.push_back(item); + } + + Ok(Rc::new(Expr { + value: ExprValue::List(buf), + position: Some(p), + })) + } + + fn parse_ident(&mut self, p: Position) -> Result, InputError> { + let mut buf = String::new(); + + while let Some(c) = self.input.peek()? { + if !c.is_ident1() { + break; + } + + self.input.next()?.unwrap(); + buf.push(c); + } + + Ok(Rc::new(Expr{ + value: if buf == "nil" { + ExprValue::Nil + } else { + ExprValue::Ident(buf) + }, + position: Some(p) + })) + } + + fn parse_string(&mut self, p: Position) -> Result, InputError> { + let mut buf = String::new(); + + loop { + let Some(c) = self.input.peek()? else { + return Err(InputError::Eof); + }; + + self.input.next()?.unwrap(); + + if c == '"' { + break; + } + + buf.push(c); + } + + Ok(Rc::new(Expr { + value: ExprValue::StringLiteral(buf), + position: Some(p) + })) + } + + pub fn parse(&mut self) -> Result>, InputError> { + self.skip_whitespace()?; + let Some(c) = self.input.peek()? else { + return Ok(None); + }; + + let p = self.input.position; + + // TODO omit '(' for toplevels + match c { + '(' => { + self.input.next()?.unwrap(); + self.parse_list(p) + }, + '"' => { + self.input.next()?.unwrap(); + self.parse_string(p) + }, + '\'' => { + self.input.next()?.unwrap(); + let Some(expr) = self.parse()? else { + todo!(); + }; + Ok(Rc::new(Expr { + value: ExprValue::Quote(expr), + position: Some(p) + })) + } + '!' => { + self.input.next()?.unwrap(); + let Some(expr) = self.parse()? else { + todo!(); + }; + Ok(Rc::new(Expr { + value: ExprValue::Meta('!', expr), + position: Some(p) + })) + } + _ if c.is_ident0() => self.parse_ident(p), + _ => todo!("Undefined char: {:?}", c), + }.map(Some) + } +} + +impl Input { + pub fn peek(&mut self) -> Result, InputError> { + if self.buffer.is_none() { + self.next()?; + } + Ok(self.buffer) + } + + pub fn next(&mut self) -> Result, InputError> { + if let Some(old) = self.buffer { + self.buffer = self.getc()?; + Ok(Some(old)) + } else { + self.buffer = self.getc()?; + Ok(self.buffer) + } + } + + fn getc(&mut self) -> Result, InputError> { + let mut buf = [0u8; 1]; + let count = match &mut self.inner { + InputInner::File(f) => f.read(&mut buf), + InputInner::Stdin(f) => f.read(&mut buf) + }?; + + Ok(if count == 1 { + self.position.column += 1; + if buf[0] == b'\n' { + self.position.line += 1; + self.position.column = 0; + } + + Some(buf[0] as char) + } else { + None + }) + } +} + +impl From for Input { + fn from(value: Stdin) -> Self { + Self { + position: Position::default(), + inner: InputInner::Stdin(value), + buffer: None + } + } +} + +impl From> for Input { + fn from(value: BufReader) -> Self { + Self { + position: Position::default(), + inner: InputInner::File(value), + buffer: None, + } + } +} + + +impl From for InputError { + fn from(e: std::io::Error) -> Self { + Self::IoError(e) + } +} diff --git a/sysutils/Cargo.toml b/sysutils/Cargo.toml new file mode 100644 index 00000000..38d05246 --- /dev/null +++ b/sysutils/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sysutils" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.3.19", features = ["std", "derive"], default-features = false } +yggdrasil-rt = { git = "https://git.alnyan.me/yggdrasil/yggdrasil-rt.git" } + +[[bin]] +name = "mount" +path = "src/mount.rs" + +[[bin]] +name = "login" +path = "src/login.rs" diff --git a/sysutils/src/login.rs b/sysutils/src/login.rs new file mode 100644 index 00000000..2476484d --- /dev/null +++ b/sysutils/src/login.rs @@ -0,0 +1,96 @@ +#![feature(yggdrasil_os, yggdrasil_raw_fd, rustc_private)] + +use std::{ + env, + io::{stdin, stdout, BufRead, Write}, + os::{ + fd::AsRawFd, + yggdrasil::signal::{set_signal_handler, Signal, SignalHandler}, + }, + process::{ExitCode, Command}, +}; + +use yggdrasil_rt::{ + debug_trace, + io::{DeviceRequest, FileMode, OpenOptions, RawFd}, + sys, +}; + +fn handler(_signal: Signal) {} + +fn login_readline( + reader: &mut R, + buf: &mut String, + _secret: bool, +) -> Result { + reader.read_line(buf) +} + +fn login_as(_username: &str, _password: &str) -> Result<(), std::io::Error> { + let mut shell = Command::new("/bin/sh").spawn()?; + shell.wait()?; + Ok(()) +} + +fn login_attempt(erase: bool) -> Result<(), std::io::Error> { + let mut stdin = stdin().lock(); + let mut stdout = stdout(); + + if erase { + print!("\x1b[0;0H\x1b[2J"); + stdout.flush().ok(); + } + + let mut username = String::new(); + + print!("Username: "); + stdout.flush().ok(); + if login_readline(&mut stdin, &mut username, false)? == 0 { + return Ok(()); + } + + login_as(username.trim(), "") +} + +fn main() -> ExitCode { + let args: Vec<_> = env::args().skip(1).collect(); + if args.len() != 1 { + eprintln!("Usage: /sbin/login TTY"); + return ExitCode::FAILURE; + } + let terminal = args[0].as_str(); + + // TODO check that `terminal` is a terminal + + // Start a new session + if let Err(err) = unsafe { sys::start_session() } { + eprintln!("setsid(): {:?}", err); + return ExitCode::FAILURE; + } + + debug_trace!("Opening terminal: {}", terminal); + + // Open the target terminal + unsafe { + // File descriptors 0, 1, 2 are now free, need to reopen them with a new terminal + sys::open(None, terminal, OpenOptions::READ, FileMode::empty()).unwrap(); + sys::open(None, terminal, OpenOptions::WRITE, FileMode::empty()).unwrap(); + sys::open(None, terminal, OpenOptions::WRITE, FileMode::empty()).unwrap(); + } + + set_signal_handler(Signal::Interrupted, SignalHandler::Function(handler)); + + let mut attempt_number = 0; + loop { + // "Attach" the terminal + unsafe { + let mut req = DeviceRequest::SetTerminalGroup(sys::get_pid()); + sys::device_request(RawFd::STDIN, &mut req).unwrap(); + } + + if let Err(err) = login_attempt(attempt_number % 3 == 0) { + eprintln!("login: {}", err.to_string()); + } + attempt_number += 1; + } +} diff --git a/sysutils/src/mount.rs b/sysutils/src/mount.rs new file mode 100644 index 00000000..6ee92fcd --- /dev/null +++ b/sysutils/src/mount.rs @@ -0,0 +1,44 @@ +use std::process::ExitCode; + +use clap::Parser; +use yggdrasil_rt::{io::MountOptions, sys::mount}; + +#[derive(Parser, Debug)] +struct Args { + source: String, + target: String, + filesystem: Option, +} + +fn main() -> ExitCode { + let args = Args::parse(); + + let source = if args.source.is_empty() { + None + } else { + Some(args.source.as_str()) + }; + let filesystem = args.filesystem.as_deref(); + let target = args.target.as_str(); + + println!("Mount {:?}, {:?}, {:?}", source, target, filesystem); + + // Permissions are not yet implemented, lol + let result = unsafe { + let options = MountOptions { + source, + filesystem, + target + }; + + mount(&options) + }; + + match result { + Ok(()) => ExitCode::SUCCESS, + Err(err) => { + eprintln!("mount: {:?}", err); + ExitCode::FAILURE + } + } +}