├── .github └── workflows │ └── check.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── book ├── .gitignore ├── book.toml └── src │ ├── SUMMARY.md │ ├── fstab.md │ ├── intro.md │ ├── modules.md │ ├── services │ └── index.md │ └── start.md ├── rustfmt.toml ├── src ├── fstab.rs ├── main.rs ├── module.rs ├── service.rs ├── tty.rs ├── uname.rs └── util.rs └── tests └── fstab ├── comments_only ├── empty ├── several └── single /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | on: push 3 | jobs: 4 | Format: 5 | runs-on: [self-hosted, linux] 6 | steps: 7 | - uses: actions/checkout@v3 8 | - run: "cargo +nightly fmt --check" 9 | Build: 10 | runs-on: [self-hosted, linux] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - run: "cargo build" 14 | - run: "cargo build --release" 15 | Test: 16 | runs-on: [self-hosted, linux] 17 | needs: Build 18 | steps: 19 | - uses: actions/checkout@v3 20 | - run: "cargo test" 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.swp 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "cc" 7 | version = "1.0.83" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 10 | dependencies = [ 11 | "libc", 12 | ] 13 | 14 | [[package]] 15 | name = "equivalent" 16 | version = "1.0.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 19 | 20 | [[package]] 21 | name = "hashbrown" 22 | version = "0.14.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 25 | 26 | [[package]] 27 | name = "indexmap" 28 | version = "2.1.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" 31 | dependencies = [ 32 | "equivalent", 33 | "hashbrown", 34 | ] 35 | 36 | [[package]] 37 | name = "libc" 38 | version = "0.2.150" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 41 | 42 | [[package]] 43 | name = "memchr" 44 | version = "2.6.4" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 47 | 48 | [[package]] 49 | name = "proc-macro2" 50 | version = "1.0.70" 51 | source = "registry+https://github.com/rust-lang/crates.io-index" 52 | checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" 53 | dependencies = [ 54 | "unicode-ident", 55 | ] 56 | 57 | [[package]] 58 | name = "quote" 59 | version = "1.0.33" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 62 | dependencies = [ 63 | "proc-macro2", 64 | ] 65 | 66 | [[package]] 67 | name = "serde" 68 | version = "1.0.193" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 71 | dependencies = [ 72 | "serde_derive", 73 | ] 74 | 75 | [[package]] 76 | name = "serde_derive" 77 | version = "1.0.193" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 80 | dependencies = [ 81 | "proc-macro2", 82 | "quote", 83 | "syn", 84 | ] 85 | 86 | [[package]] 87 | name = "serde_spanned" 88 | version = "0.6.4" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" 91 | dependencies = [ 92 | "serde", 93 | ] 94 | 95 | [[package]] 96 | name = "solfege" 97 | version = "0.1.0" 98 | dependencies = [ 99 | "cc", 100 | "libc", 101 | "serde", 102 | "toml", 103 | ] 104 | 105 | [[package]] 106 | name = "syn" 107 | version = "2.0.39" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 110 | dependencies = [ 111 | "proc-macro2", 112 | "quote", 113 | "unicode-ident", 114 | ] 115 | 116 | [[package]] 117 | name = "toml" 118 | version = "0.8.8" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" 121 | dependencies = [ 122 | "serde", 123 | "serde_spanned", 124 | "toml_datetime", 125 | "toml_edit", 126 | ] 127 | 128 | [[package]] 129 | name = "toml_datetime" 130 | version = "0.6.5" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 133 | dependencies = [ 134 | "serde", 135 | ] 136 | 137 | [[package]] 138 | name = "toml_edit" 139 | version = "0.21.0" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" 142 | dependencies = [ 143 | "indexmap", 144 | "serde", 145 | "serde_spanned", 146 | "toml_datetime", 147 | "winnow", 148 | ] 149 | 150 | [[package]] 151 | name = "unicode-ident" 152 | version = "1.0.12" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 155 | 156 | [[package]] 157 | name = "winnow" 158 | version = "0.5.19" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" 161 | dependencies = [ 162 | "memchr", 163 | ] 164 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solfege" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | libc = "*" 8 | serde = { version = "1.0.159", features = ["derive"] } 9 | toml = "0.8.8" 10 | 11 | [build-dependencies] 12 | cc = "1.0" 13 | 14 | [profile.release] 15 | panic = "abort" 16 | strip = true 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Luc Lenôtre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | logo 5 | 6 |

7 | 8 | [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge&logo=book)](./LICENSE) 9 | ![Version](https://img.shields.io/badge/dynamic/toml?url=https%3A%2F%2Fraw.githubusercontent.com%2Fllenotre%2Fsolfege%2Fmaster%2FCargo.toml&query=%24.package.version&style=for-the-badge&label=version) 10 | ![Continuous integration](https://img.shields.io/github/actions/workflow/status/llenotre/solfege/check.yml?style=for-the-badge&logo=github) 11 | 12 | 13 | 14 | # About 15 | 16 | Solfège ("Music Theory" in French) is the Maestro operating system's default booting system. 17 | 18 | 19 | 20 | ## Build 21 | 22 | To build, simply use the command: 23 | 24 | ``` 25 | cargo build --release 26 | ``` 27 | 28 | For cross compilation, use the `--target` flag with the appropriate target triplet. 29 | 30 | 31 | 32 | ## Documentation 33 | 34 | Documentation can be found in the book, which can be built using the command: 35 | 36 | ``` 37 | mdbook build book/ 38 | ``` 39 | 40 | The book is then accessible at the path `book/book/index.html`. 41 | -------------------------------------------------------------------------------- /book/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["llenotre"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Solfege" 7 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./intro.md) 4 | - [fstab](./fstab.md) 5 | - [Kernel modules](./modules.md) 6 | - [Services](./services/index.md) 7 | - [Start program](./start.md) 8 | -------------------------------------------------------------------------------- /book/src/fstab.md: -------------------------------------------------------------------------------- 1 | # fstab 2 | 3 | The `/etc/fstab` file stores the list of filesystems to mount at boot. 4 | 5 | Each line of the file is a filesystem to mount, with the following syntax (this is an example): 6 | 7 | ``` 8 | UUID=b66bad31-f4a4-4d83-9b87-3ddd84b79fc2 / ext4 rw,relatime 0 1 9 | ``` 10 | 11 | Each column has the following meaning: 12 | - `file system`: The filesystem to mount. This can either be an UUID (example: `UUID=b66bad31-f4a4-4d83-9b87-3ddd84b79fc2`), a label (example: `LABEL=hello`) or a file (example: `/dev/sda`). 13 | - `dir`: The directory on which the filesystem will be mounted. 14 | - `type`: The filesystem type. 15 | - `options`: Mount options, comma-separated. TODO: document each option 16 | - `dump`: TODO 17 | - `pass`: TODO 18 | 19 | Comments can be added. They must start with the `#` character. 20 | -------------------------------------------------------------------------------- /book/src/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Solfège ("Music Theory" in French) is a booting system for the Maestro kernel. 4 | 5 | 6 | 7 | ## Scope 8 | 9 | This book documents configuration and usage of Solfège. 10 | -------------------------------------------------------------------------------- /book/src/modules.md: -------------------------------------------------------------------------------- 1 | # Kernel modules 2 | 3 | At boot, Solfège loads all the default kernel modules located in `/lib/modules/-/default`. 4 | 5 | - `` is the name of the kernel (`uname -s`) 6 | - `` is the kernel's release (`uname -r`) 7 | 8 | If a symbolic link is present, it is followed. 9 | -------------------------------------------------------------------------------- /book/src/services/index.md: -------------------------------------------------------------------------------- 1 | # Services 2 | 3 | A service is a daemon which is launched at system startup if enabled. 4 | 5 | 6 | 7 | ## Service descriptor 8 | 9 | Each service is described with a file, located in the directory `/usr/lib/solfege/services`. 10 | 11 | Each service descriptor is in TOML format. Example: 12 | 13 | ```toml 14 | name = "example" 15 | desc = "Just an example service" 16 | enabled = true 17 | restart_delay = 10 # optional 18 | user = "root" 19 | group = "root" 20 | program_path = "/path/to/program" 21 | ``` 22 | 23 | - `name` is the name of the service. 24 | - `desc` is the description of the service. 25 | - `enabled` tells whether the service is enabled. 26 | - `restart_delay` is the delay in milliseconds to wait before restarting the service after it exited. If not specified, the service is never restarted. 27 | - `user` is the name of the user owning the process. 28 | - `group` is the name of the group owning the process. 29 | - `program_path` is the path of the program to run. 30 | -------------------------------------------------------------------------------- /book/src/start.md: -------------------------------------------------------------------------------- 1 | # Start program 2 | 3 | After initialization, Solfège starts the program at the path specified in the file `/etc/solfege/startup`. 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | blank_lines_upper_bound = 1 2 | comment_width = 99 3 | condense_wildcard_suffixes = true 4 | hard_tabs = true 5 | hex_literal_case = "Lower" 6 | newline_style = "Unix" 7 | reorder_impl_items = true 8 | struct_lit_single_line = false 9 | use_field_init_shorthand = true 10 | wrap_comments = true 11 | -------------------------------------------------------------------------------- /src/fstab.rs: -------------------------------------------------------------------------------- 1 | //! This module handles the fstab file, which contains the list of filesystems to mount at boot. 2 | 3 | use std::convert::identity; 4 | use std::error::Error; 5 | use std::ffi::CString; 6 | use std::fmt::{Display, Formatter}; 7 | use std::fs::File; 8 | use std::io::BufRead; 9 | use std::io::BufReader; 10 | use std::iter::Peekable; 11 | use std::path::Path; 12 | use std::ptr::null; 13 | use std::str::Chars; 14 | use std::str::FromStr; 15 | use std::{fmt, io}; 16 | 17 | /// The path to the fstab file. 18 | const FSTAB_PATH: &str = "/etc/fstab"; 19 | 20 | /// Enumeration of possible filesystem sources types. 21 | #[derive(Debug, Eq, PartialEq)] 22 | pub enum FSSpec { 23 | /// Mounting from a file. 24 | File(String), 25 | /// Mounting from the given label. 26 | Label(String), 27 | /// Mounting from a partition UUID. 28 | Uuid(String), 29 | } 30 | 31 | impl FromStr for FSSpec { 32 | type Err = (); 33 | 34 | fn from_str(s: &str) -> Result { 35 | if let Some(label) = s.strip_prefix("LABEL=") { 36 | Ok(Self::Label(String::from(label))) 37 | } else if let Some(uuid) = s.strip_prefix("UUID=") { 38 | Ok(Self::Uuid(String::from(uuid))) 39 | } else { 40 | Ok(Self::File(s.to_string())) 41 | } 42 | } 43 | } 44 | 45 | impl Display for FSSpec { 46 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 47 | match self { 48 | Self::File(s) => write!(f, "{s}"), 49 | Self::Label(s) => write!(f, "LABEL={s}"), 50 | Self::Uuid(s) => write!(f, "UUID={s}"), 51 | } 52 | } 53 | } 54 | 55 | /// Structure representing an entry in the fstab file. 56 | pub struct FSTabEntry { 57 | /// The source of the filesystem. 58 | pub fs_spec: FSSpec, 59 | /// The file on which the filesystem will be mounted. 60 | pub fs_file: String, 61 | /// The filesystem type. 62 | pub fs_vfstype: String, 63 | /// The mount options associated with the filesystem. 64 | pub fs_mntops: Vec, 65 | /// Tells whether the filesystem has to be dumped. 66 | pub fs_freq: bool, 67 | /// Tells the order in which fsck checks the filesystems. 68 | pub fs_passno: u32, 69 | } 70 | 71 | impl FSTabEntry { 72 | /// Mounts the given entry. 73 | pub fn mount(&self) -> Result<(), Box> { 74 | let spec = self.fs_spec.to_string(); 75 | let result = unsafe { 76 | libc::mount( 77 | CString::new(spec.clone())?.as_ptr(), 78 | CString::new(self.fs_file.clone())?.as_ptr(), 79 | CString::new(self.fs_vfstype.clone())?.as_ptr(), 80 | 0, // TODO 81 | null(), // TODO 82 | ) 83 | }; 84 | if result == 0 { 85 | Ok(()) 86 | } else { 87 | Err(format!( 88 | "Failed to mount `{}` into `{}`: {}", 89 | spec, 90 | self.fs_file, 91 | io::Error::last_os_error() 92 | ) 93 | .into()) 94 | } 95 | } 96 | } 97 | 98 | /// Skips whitespace on the given iterator. 99 | fn skip_whitespaces(chars: &mut Peekable) { 100 | while let Some(c) = chars.peek() { 101 | if !c.is_whitespace() { 102 | break; 103 | } 104 | chars.next(); 105 | } 106 | } 107 | 108 | /// Consumes a token from the given chars iterator. 109 | /// If the token is invalid, the function returns None. 110 | fn consume_token(chars: &mut Peekable) -> Option { 111 | // The token 112 | let mut tok = String::new(); 113 | // Tells whether a quote is open 114 | let mut quote = false; 115 | 116 | while let Some(c) = chars.peek() { 117 | if !quote && (c.is_whitespace() || *c == '#') { 118 | break; 119 | } 120 | match c { 121 | '"' => { 122 | quote = !quote; 123 | chars.next(); 124 | } 125 | '\\' => { 126 | chars.next(); 127 | if let Some(c) = chars.next() { 128 | tok.push(c); 129 | continue; 130 | } else { 131 | return None; 132 | } 133 | } 134 | _ => { 135 | tok.push(*c); 136 | chars.next(); 137 | } 138 | } 139 | } 140 | (!quote).then_some(tok) 141 | } 142 | 143 | /// Parses the given line. 144 | /// 145 | /// If no entry is present on the line or if the entry is invalid, the function returns `None`. 146 | fn parse_line(line: &str) -> Option { 147 | if line.is_empty() { 148 | return None; 149 | } 150 | 151 | let mut fs_spec = None; 152 | let mut fs_file = None; 153 | let mut fs_vfstype = None; 154 | let mut fs_mntops = None; 155 | let mut fs_freq = None; 156 | let mut fs_passno = None; 157 | 158 | // The current index in the entry 159 | let mut i = 0; 160 | let mut chars = line.chars().peekable(); 161 | while chars.peek().is_some() { 162 | skip_whitespaces(&mut chars); 163 | if let Some(c) = chars.peek() { 164 | // On comment, stop parsing the line 165 | if *c == '#' { 166 | break; 167 | } 168 | } else { 169 | break; 170 | } 171 | // Get the next token 172 | let tok = consume_token(&mut chars)?; 173 | match i { 174 | // fs_spec 175 | 0 => fs_spec = FSSpec::from_str(&tok).ok(), 176 | // fs_file 177 | 1 => fs_file = Some(tok), 178 | // fs_vfstype 179 | 2 => fs_vfstype = Some(tok), 180 | // fs_mntops 181 | 3 => fs_mntops = Some(tok.split(',').map(str::to_owned).collect()), 182 | // fs_freq 183 | 4 => fs_freq = Some(tok != "0"), 184 | // fs_passno 185 | 5 => fs_passno = Some(tok.parse::().ok()?), 186 | // If the line has too many entries, ignore it 187 | _ => return None, 188 | } 189 | i += 1; 190 | } 191 | 192 | Some(FSTabEntry { 193 | fs_spec: fs_spec?, 194 | fs_file: fs_file?, 195 | fs_vfstype: fs_vfstype?, 196 | fs_mntops: fs_mntops?, 197 | fs_freq: fs_freq?, 198 | fs_passno: fs_passno?, 199 | }) 200 | } 201 | 202 | /// Parses the fstab file and returns the list of entries. 203 | /// 204 | /// `path` is the path to the fstab file. If `None`, the function takes the default path. 205 | /// 206 | /// Invalid entries are ignored. 207 | pub fn parse(path: Option<&Path>) -> io::Result> { 208 | let path = path.map(Path::new).unwrap_or(Path::new(FSTAB_PATH)); 209 | let file = File::open(path)?; 210 | let reader = BufReader::new(file); 211 | reader 212 | .lines() 213 | .map(|l| Ok(parse_line(&l?))) 214 | .map(Result::transpose) 215 | .filter_map(identity) 216 | .collect() 217 | } 218 | 219 | #[cfg(test)] 220 | mod test { 221 | use super::*; 222 | 223 | #[test] 224 | fn fstab_empty() { 225 | let entries = parse(Some(Path::new("tests/fstab/empty"))).unwrap(); 226 | assert!(entries.is_empty()); 227 | } 228 | 229 | #[test] 230 | fn fstab_comments_only() { 231 | let entries = parse(Some(Path::new("tests/fstab/comments_only"))).unwrap(); 232 | assert!(entries.is_empty()); 233 | } 234 | 235 | #[test] 236 | fn fstab_single() { 237 | let entries = parse(Some(Path::new("tests/fstab/single"))).unwrap(); 238 | assert_eq!(entries.len(), 1); 239 | 240 | assert_eq!(entries[0].fs_spec, FSSpec::File("/dev/sda1".to_string())); 241 | assert_eq!(entries[0].fs_file, "/"); 242 | assert_eq!(entries[0].fs_vfstype, "ext4"); 243 | assert_eq!(entries[0].fs_mntops[0], "rw"); 244 | assert_eq!(entries[0].fs_freq, false); 245 | assert_eq!(entries[0].fs_passno, 1); 246 | } 247 | 248 | #[test] 249 | fn fstab_several() { 250 | let entries = parse(Some(Path::new("tests/fstab/several"))).unwrap(); 251 | assert_eq!(entries.len(), 3); 252 | 253 | assert_eq!(entries[0].fs_spec, FSSpec::File("/dev/sda1".to_string())); 254 | assert_eq!(entries[0].fs_file, "/"); 255 | assert_eq!(entries[0].fs_vfstype, "ext4"); 256 | assert_eq!(entries[0].fs_mntops[0], "rw"); 257 | assert_eq!(entries[0].fs_freq, false); 258 | assert_eq!(entries[0].fs_passno, 1); 259 | 260 | assert_eq!(entries[1].fs_spec, FSSpec::Label("UEFI".to_string())); 261 | assert_eq!(entries[1].fs_file, "/"); 262 | assert_eq!(entries[1].fs_vfstype, "ext4"); 263 | assert_eq!(entries[1].fs_mntops[0], "defaults"); 264 | assert_eq!(entries[1].fs_mntops[1], "rw"); 265 | assert_eq!(entries[1].fs_freq, false); 266 | assert_eq!(entries[1].fs_passno, 1); 267 | 268 | assert_eq!( 269 | entries[2].fs_spec, 270 | FSSpec::Uuid("5fcd5a6e-a326-43fd-8b39-f6e1238bc54f".to_string()) 271 | ); 272 | assert_eq!(entries[2].fs_file, "/"); 273 | assert_eq!(entries[2].fs_vfstype, "ext4"); 274 | assert_eq!(entries[2].fs_mntops[0], "suid"); 275 | assert_eq!(entries[2].fs_mntops[1], "rw"); 276 | assert_eq!(entries[2].fs_freq, false); 277 | assert_eq!(entries[2].fs_passno, 1); 278 | } 279 | 280 | // TODO Test with quotes 281 | // TODO Test with invalid entries 282 | } 283 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Solfège is the default booting system for the Maestro operating system. 2 | 3 | //#![deny(warnings)] 4 | 5 | mod fstab; 6 | mod module; 7 | mod service; 8 | mod tty; 9 | mod uname; 10 | mod util; 11 | 12 | use std::fs; 13 | use std::process::exit; 14 | use std::process::Command; 15 | 16 | /// The path to the file containing the startup program. 17 | const STARTUP_PROG_PATH: &str = "/etc/solfege/startup"; 18 | 19 | /// Runs the startup command. 20 | fn startup() { 21 | let path = fs::read_to_string(STARTUP_PROG_PATH); 22 | let path = path.as_deref().map(str::trim).unwrap_or_else(|e| { 23 | eprintln!("Failed to open startup program configuration file: {e}"); 24 | exit(1); 25 | }); 26 | Command::new(path).spawn().unwrap_or_else(|e| { 27 | eprintln!("Cannot run startup program: {e}"); 28 | exit(1); 29 | }); 30 | } 31 | 32 | fn main() { 33 | println!("Hello world!"); 34 | uname::set_hostname().unwrap_or_else(|e| { 35 | eprintln!("Cannot set system's hostname: {e}"); 36 | }); 37 | let uname = uname::UnameInfo::get().unwrap_or_else(|e| { 38 | eprintln!("Cannot retrieve system informations with uname: {e}"); 39 | exit(1); 40 | }); 41 | println!( 42 | "Booting system with {} kernel, release {}", 43 | uname.sysname, uname.release 44 | ); 45 | 46 | // Initialize TTY 47 | println!("Initializing current TTY..."); 48 | tty::init().unwrap_or_else(|e| { 49 | eprintln!("Failed to setup TTY: {e}"); 50 | exit(1); 51 | }); 52 | 53 | // Mounting default filesystems 54 | println!("Mounting fstab filesystems..."); 55 | let fstab_entries = fstab::parse(None).unwrap_or_else(|e| { 56 | eprintln!("Failed to read the fstab file: {e}"); 57 | exit(1); 58 | }); 59 | for entry in fstab_entries { 60 | println!("Mounting `{}`...", entry.fs_file); 61 | entry.mount().unwrap_or_else(|e| { 62 | eprintln!("Failed to mount `{}`: {e}", entry.fs_file); 63 | exit(1); 64 | }); 65 | } 66 | 67 | // Loading default modules 68 | println!("Loading default modules..."); 69 | module::load_default(&uname).unwrap_or_else(|e| { 70 | eprintln!("Failed to load default modules: {e}"); 71 | exit(1); 72 | }); 73 | 74 | println!("Launching services..."); 75 | let mut services_manager = service::Manager::new().unwrap_or_else(|e| { 76 | eprintln!("Failed to launch the services manager: {e}"); 77 | exit(1); 78 | }); 79 | 80 | // Running the startup program 81 | startup(); 82 | 83 | loop { 84 | services_manager.tick(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/module.rs: -------------------------------------------------------------------------------- 1 | //! This module handles kernel modules management. 2 | 3 | use crate::uname::UnameInfo; 4 | use std::ffi::c_int; 5 | use std::fs; 6 | use std::fs::File; 7 | use std::io; 8 | use std::os::fd::AsRawFd; 9 | use std::path::Path; 10 | use std::path::PathBuf; 11 | use std::ptr::null; 12 | 13 | /// `finit_module` system call. 14 | unsafe fn finit_module(fd: c_int) -> io::Result<()> { 15 | let res = libc::syscall(libc::SYS_finit_module, fd, null::<()>(), 0); 16 | if res == 0 { 17 | Ok(()) 18 | } else { 19 | Err(io::Error::last_os_error()) 20 | } 21 | } 22 | 23 | /// Loads the module at the given path. 24 | /// 25 | /// On fail, the function returns an error. 26 | pub fn load(path: &Path) -> io::Result<()> { 27 | println!("Loading module `{}`...", path.display()); 28 | let file = File::open(path)?; 29 | unsafe { finit_module(file.as_raw_fd()) } 30 | } 31 | 32 | /// Unloads the module with the given name. 33 | /// 34 | /// On fail, the function returns an error. 35 | pub fn unload(_name: &str) -> Result<(), String> { 36 | // TODO 37 | todo!(); 38 | } 39 | 40 | /// Loads every modules recursively in the given directory. 41 | /// 42 | /// On success, the function returns the number of modules loaded. 43 | /// 44 | /// If the directory doesn't exist, the function returns an error. 45 | pub fn load_all(path: &Path) -> io::Result<()> { 46 | for entry in fs::read_dir(path)? { 47 | let e = entry?; 48 | let p = e.path(); 49 | 50 | let file_type = e.file_type()?; 51 | if file_type.is_dir() { 52 | load_all(&p)?; 53 | } else if file_type.is_file() { 54 | load(&p)?; 55 | } 56 | } 57 | Ok(()) 58 | } 59 | 60 | /// Loads default modules. 61 | pub fn load_default(uname: &UnameInfo) -> io::Result<()> { 62 | let default_modules_path: PathBuf = 63 | format!("/lib/modules/{}-{}/default/", uname.sysname, uname.release).into(); 64 | load_all(&default_modules_path) 65 | } 66 | -------------------------------------------------------------------------------- /src/service.rs: -------------------------------------------------------------------------------- 1 | //! This modules handles services. 2 | 3 | use serde::Deserialize; 4 | use std::convert::identity; 5 | use std::fs; 6 | use std::io; 7 | use std::io::ErrorKind; 8 | use std::path::PathBuf; 9 | use std::process::Child; 10 | use std::process::Command; 11 | use std::ptr::null_mut; 12 | 13 | /// The path to the services directory. 14 | const SERVICES_PATH: &str = "/usr/lib/solfege/services"; 15 | 16 | /// The service's state. 17 | #[derive(Clone, Copy, Eq, PartialEq)] 18 | pub enum ServiceState { 19 | /// The service is running. 20 | Running, 21 | /// The service is stopped. 22 | Stopped, 23 | /// The service has crashed. 24 | Crashed, 25 | } 26 | 27 | /// Structure representing a service as a file. 28 | #[derive(Deserialize)] 29 | pub struct ServiceDescriptor { 30 | /// The service's name. 31 | name: String, 32 | /// The service's description. 33 | desc: String, 34 | 35 | /// Tells whether the service is enabled. 36 | enabled: bool, 37 | 38 | /// The delay in milliseconds before restarting the service after it crashed. 39 | /// 40 | /// If `None`, the service won't be restarted automaticaly. 41 | restart_delay: Option, 42 | 43 | /// The user used to run the service. 44 | user: String, 45 | /// The group used to run the service. 46 | group: String, 47 | 48 | /// The path to the program of the service. 49 | program_path: PathBuf, 50 | } 51 | 52 | /// Structure representing a service. 53 | pub struct Service { 54 | /// The service as represented in its file. 55 | desc: ServiceDescriptor, 56 | 57 | /// The current state of the service. 58 | state: ServiceState, 59 | 60 | /// The service's current process. 61 | process: Option, 62 | /// The timestamp of the last crash. 63 | crash_timestamp: u64, 64 | } 65 | 66 | impl From for Service { 67 | fn from(desc: ServiceDescriptor) -> Self { 68 | Self { 69 | desc, 70 | 71 | state: ServiceState::Stopped, 72 | 73 | process: None, 74 | crash_timestamp: 0, 75 | } 76 | } 77 | } 78 | 79 | impl Service { 80 | /// Tells whether the service is enabled. 81 | pub fn is_enabled(&self) -> bool { 82 | self.desc.enabled 83 | } 84 | 85 | /// Returns current state of the service. 86 | pub fn get_state(&self) -> ServiceState { 87 | self.state 88 | } 89 | 90 | /// Starts the service. If the service is already started, the function does nothing. 91 | pub fn start(&mut self) -> io::Result<()> { 92 | if self.state != ServiceState::Running { 93 | println!("Starting service `{}`...", self.desc.name); 94 | 95 | // TODO Use uid and gid 96 | let process = Command::new(&self.desc.program_path).spawn()?; 97 | 98 | self.process = Some(process); 99 | self.state = ServiceState::Running; 100 | } 101 | Ok(()) 102 | } 103 | 104 | /// Reloads the service. 105 | pub fn reload(&mut self) -> io::Result<()> { 106 | if self.state == ServiceState::Running { 107 | self.stop()?; 108 | } 109 | self.start() 110 | } 111 | 112 | /// Stops the service. If the service is already stopped, the function does nothing. 113 | pub fn stop(&mut self) -> io::Result<()> { 114 | if let Some(ref mut process) = self.process { 115 | process.kill()?; 116 | self.process = None; 117 | } 118 | self.state = ServiceState::Stopped; 119 | Ok(()) 120 | } 121 | 122 | /// Restarts the service. 123 | pub fn restart(&mut self) -> io::Result<()> { 124 | self.stop()?; 125 | self.start()?; 126 | Ok(()) 127 | } 128 | } 129 | 130 | /// Structure representing the services manager. 131 | pub struct Manager { 132 | /// The list of services. 133 | services: Vec, 134 | } 135 | 136 | impl Manager { 137 | /// Reads the list of services. 138 | fn list() -> io::Result> { 139 | let entries = match fs::read_dir(SERVICES_PATH) { 140 | Ok(entries) => entries, 141 | Err(e) if e.kind() == ErrorKind::NotFound => return Ok(vec![]), 142 | Err(e) => return Err(e), 143 | }; 144 | entries 145 | .map(|entry| { 146 | let entry = entry?; 147 | let p = entry.path(); 148 | let file_type = entry.file_type()?; 149 | if !file_type.is_file() { 150 | return Ok(None); 151 | } 152 | let content = fs::read_to_string(p)?; 153 | match toml::from_str::(&content) { 154 | Ok(desc) => Ok(Some(Service::from(desc))), 155 | Err(_e) => { 156 | // TODO 157 | todo!(); 158 | } 159 | } 160 | }) 161 | .map(Result::transpose) 162 | .filter_map(identity) 163 | .collect() 164 | } 165 | 166 | /// Creates a new instance. 167 | pub fn new() -> io::Result { 168 | let mut services = Self::list()?; 169 | 170 | for s in &mut services { 171 | if s.is_enabled() { 172 | s.start()?; 173 | } 174 | } 175 | 176 | Ok(Self { 177 | services, 178 | }) 179 | } 180 | 181 | /// Returns an immutable reference to the list of services. 182 | pub fn get_services(&self) -> &Vec { 183 | &self.services 184 | } 185 | 186 | /// Returns the service with the given PID. 187 | pub fn get_service(&mut self, pid: u32) -> Option<&mut Service> { 188 | self.services 189 | .iter_mut() 190 | .find(|s| s.process.as_ref().map(|c| c.id()) == Some(pid)) 191 | } 192 | 193 | /// Ticks the manager. 194 | /// 195 | /// This function is used to restart services that died. 196 | pub fn tick(&mut self) { 197 | let pid = unsafe { libc::waitpid(-1, null_mut::(), 0) }; 198 | if pid < 0 { 199 | // TODO sleep? 200 | return; 201 | } 202 | 203 | let Some(_service) = self.get_service(pid as u32) else { 204 | // An orphan process has been assigned to the current process, then died 205 | return; 206 | }; 207 | 208 | // TODO update process state 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/tty.rs: -------------------------------------------------------------------------------- 1 | //! This module implements TTY-related features. 2 | 3 | use std::io; 4 | 5 | /// Initializes the current TTY. 6 | pub fn init() -> io::Result<()> { 7 | // Get current PGID 8 | let pgid = unsafe { libc::getpgid(0) }; 9 | if pgid < 0 { 10 | return Err(io::Error::last_os_error()); 11 | } 12 | 13 | // Set the TTY's PGRP 14 | let ret = unsafe { libc::tcsetpgrp(libc::STDIN_FILENO, pgid) }; 15 | if ret == 0 { 16 | Ok(()) 17 | } else { 18 | Err(io::Error::last_os_error()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/uname.rs: -------------------------------------------------------------------------------- 1 | //! This module implements a call to uname. Allowing to retrieve various informations. 2 | 3 | use libc::c_char; 4 | use std::fs; 5 | use std::io; 6 | use std::mem; 7 | 8 | /// The path to the hostname file. 9 | const HOSTNAME_FILE: &str = "/etc/hostname"; 10 | 11 | /// Structure containing the final informations to be returned outside of this module. 12 | #[derive(Debug)] 13 | pub struct UnameInfo { 14 | /// Operating system name 15 | pub sysname: String, 16 | /// Name within "some implementation-defined network" 17 | pub nodename: String, 18 | /// Operating system release 19 | pub release: String, 20 | /// Operating system version 21 | pub version: String, 22 | /// Hardware identifier 23 | pub machine: String, 24 | } 25 | 26 | /// Turns the given buffer into a `CStr`. 27 | fn array_to_string(buf: &[c_char]) -> String { 28 | buf.into_iter() 29 | .take_while(|b| **b != 0) 30 | .map(|b| (*b) as u8 as char) 31 | .collect() 32 | } 33 | 34 | impl UnameInfo { 35 | /// Returns the uname informations. 36 | /// 37 | /// If the uname informations cannot be retrieved, the function returns an error. 38 | pub fn get() -> io::Result { 39 | let mut uname_info = unsafe { mem::zeroed() }; 40 | let result = unsafe { libc::uname(&mut uname_info) }; 41 | if result == 0 { 42 | Ok(UnameInfo { 43 | sysname: array_to_string(&uname_info.sysname[..]), 44 | nodename: array_to_string(&uname_info.nodename[..]), 45 | release: array_to_string(&uname_info.release[..]), 46 | version: array_to_string(&uname_info.version[..]), 47 | machine: array_to_string(&uname_info.machine[..]), 48 | }) 49 | } else { 50 | Err(io::Error::last_os_error()) 51 | } 52 | } 53 | } 54 | 55 | /// Sets the system's hostname according to the hostname file. 56 | /// 57 | /// If the file is not present, the function doesn't do anything. 58 | pub fn set_hostname() -> io::Result<()> { 59 | // Read hostname file 60 | let hostname = match fs::read(HOSTNAME_FILE) { 61 | Ok(h) => h, 62 | Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()), 63 | Err(e) => return Err(e), 64 | }; 65 | let result = unsafe { libc::sethostname(hostname.as_ptr() as _, hostname.len()) }; 66 | if result == 0 { 67 | Ok(()) 68 | } else { 69 | Err(io::Error::last_os_error()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! This module implements utilities functions. 2 | 3 | use std::time::SystemTime; 4 | use std::time::UNIX_EPOCH; 5 | 6 | /// Returns the current timestamp in milliseconds from the Unix epoch. 7 | pub fn get_timestamp() -> u64 { 8 | SystemTime::now() 9 | .duration_since(UNIX_EPOCH) 10 | .expect("System clock panic!") 11 | .as_millis() as _ 12 | } 13 | -------------------------------------------------------------------------------- /tests/fstab/comments_only: -------------------------------------------------------------------------------- 1 | # Hello world! 2 | 3 | # Those are random comments :) 4 | 5 | # Bye :) 6 | -------------------------------------------------------------------------------- /tests/fstab/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maestro-os/solfege/a06a1696929141dacfe9023c059282c00f397d25/tests/fstab/empty -------------------------------------------------------------------------------- /tests/fstab/several: -------------------------------------------------------------------------------- 1 | # First entry 2 | /dev/sda1 / ext4 rw 0 1 3 | 4 | # Second entry 5 | # Random comment :) 6 | LABEL=UEFI / ext4 defaults,rw 0 1 7 | 8 | # Third entry 9 | UUID=5fcd5a6e-a326-43fd-8b39-f6e1238bc54f / ext4 suid,rw 0 1 # Another random comment :) 10 | -------------------------------------------------------------------------------- /tests/fstab/single: -------------------------------------------------------------------------------- 1 | # Hello world! 2 | /dev/sda1 / ext4 rw 0 1 3 | --------------------------------------------------------------------------------