├── .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 |
5 |
6 |
7 |
8 | [](./LICENSE)
9 | 
10 | 
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 |
--------------------------------------------------------------------------------